diff --git a/.env.example b/.env.example index 4dededc..7ec20e7 100644 --- a/.env.example +++ b/.env.example @@ -8,13 +8,10 @@ MAPLE_PORT=8080 # Maple Backend Configuration # Production: https://enclave.trymaple.ai # Development: https://enclave.secretgpt.ai -# Local: http://localhost:3000 +# Local: use `just run-local`; plain HTTP mock attestation is compile-time +# gated and is never enabled in release, Docker, or embedded Maple builds. MAPLE_BACKEND_URL=https://enclave.trymaple.ai -# PCR0 trust roots must match the selected backend environment. -# Use development only with the development enclave; production is the default. -MAPLE_PCR0_ENVIRONMENT=production - # Authentication # Your Maple API key - get this from https://trymaple.ai MAPLE_API_KEY=your-maple-api-key-here diff --git a/CLAUDE.md b/CLAUDE.md index 4ea10ad..4d9acb3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,11 +55,40 @@ Maple Proxy is a lightweight OpenAI-compatible proxy server that forwards reques - Handles streaming responses for chat completions - Transforms responses to OpenAI format +### TEE attestation boundary + +TEE measurement authorization is owned by the OpenSecret SDK. Maple Proxy +should only call `perform_attestation_handshake`; do not add a proxy-specific +PCR allowlist or Sigstore/Rekor verifier. + +The intended SDK handshake authenticates the AWS Nitro attestation document, +compares the complete PCR0/PCR1/PCR2 tuple with a release snapshot embedded in +the SDK, and only then accepts the enclave public key and performs key exchange. +There is no runtime Sigstore/Rekor or release-metadata network lookup. At SDK +update time, the updater verifies the release manifest and Cosign bundle, +including the expected signing identity and Rekor evidence, before generating +the embedded snapshot. + +Mock attestation is available only through the explicitly named +`insecure-local-mock-attestation` Cargo feature. Keep it disabled in release, +Docker, and embedded Maple builds; only `just run-local` opts in. + +Rekor supplies tamper-evident transparency-log evidence for the signed release +statement used to generate that snapshot. It does not itself prove Nix +reproducibility, release freshness, rollback prevention, or revocation. + +This integration branch pins an exact reviewed SDK commit with default features +disabled. That staging commit deliberately contains an empty release snapshot, +so remote handshakes fail closed. Before release, replace the pin with a +snapshot-bearing reviewed commit or published crate; do not invent an +unpublished version or vendor the SDK here. + ### Request Flow 1. Client sends OpenAI-compatible request to proxy 2. Proxy extracts API key (from header or default config) -3. Creates OpenSecret client and performs TEE attestation +3. Creates an OpenSecret client and delegates TEE authentication, release + authorization, and key exchange to the SDK 4. Forwards request to Maple backend (enclave.trymaple.ai or configured URL) 5. Streams response back to client in OpenAI format diff --git a/Cargo.lock b/Cargo.lock index 838d2f0..a49ccc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1638,8 +1638,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "opensecret" version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d9a35e5dd1ee761d3449d9e2db722eb1ebbab0eef02d3bb8601fd6b23d6bd9" +source = "git+https://github.com/OpenSecretCloud/OpenSecret-SDK?rev=8ecd1a31803f45753ed6ac457a4d8553389b2336#8ecd1a31803f45753ed6ac457a4d8553389b2336" dependencies = [ "aes-gcm", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index fea32e6..b02bba9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,9 +27,15 @@ path = "src/lib.rs" name = "maple-proxy" path = "src/main.rs" +[features] +default = [] +# Explicit development-only opt-in. Release, Docker, and embedded Maple builds +# use default features and cannot accept mock attestation documents. +insecure-local-mock-attestation = ["opensecret/mock-attestation"] + [dependencies] # OpenSecret SDK -opensecret = "3.6.1" +opensecret = { git = "https://github.com/OpenSecretCloud/OpenSecret-SDK", rev = "8ecd1a31803f45753ed6ac457a4d8553389b2336", default-features = false } # Web server axum = { version = "0.8.4", features = ["http2", "macros"] } diff --git a/Dockerfile b/Dockerfile index 034248b..3be180f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,6 @@ USER maple ENV MAPLE_HOST=0.0.0.0 \ MAPLE_PORT=8080 \ MAPLE_BACKEND_URL=https://enclave.trymaple.ai \ - MAPLE_PCR0_ENVIRONMENT=production \ MAPLE_DEBUG=false \ MAPLE_ENABLE_CORS=true \ MAPLE_REQUEST_TIMEOUT_SECS=300 \ diff --git a/README.md b/README.md index c423547..d011472 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ Environment (TEE) processing. ## ๐Ÿš€ Features - **OpenAI-Compatible Surface** - Models, chat completions, and embeddings endpoints -- **Secure TEE Processing** - All requests processed in secure enclaves +- **Attested TEE Transport** - The OpenSecret SDK establishes an attested, + encrypted channel before inference requests are forwarded - **Lossless Chat Parameters** - Provider-specific request fields pass through unchanged - **Streaming and Non-Streaming** - Supports both chat completion response modes - **Flexible Authentication** - Environment variables or per-request API keys @@ -44,8 +45,7 @@ Set environment variables or use command-line arguments: # Environment Variables export MAPLE_HOST=127.0.0.1 # Server host (default: 127.0.0.1) export MAPLE_PORT=8080 # Server port (default: 8080) -export MAPLE_BACKEND_URL=http://localhost:3000 # Maple backend URL (prod: https://enclave.trymaple.ai) -export MAPLE_PCR0_ENVIRONMENT=production # PCR0 trust roots: production (default) or development +export MAPLE_BACKEND_URL=https://enclave.trymaple.ai # Maple backend URL export MAPLE_API_KEY=your-maple-api-key # Default API key (optional) export MAPLE_DEBUG=true # Enable debug logging export MAPLE_ENABLE_CORS=true # Enable CORS @@ -56,11 +56,12 @@ export MAPLE_STREAM_IDLE_TIMEOUT_SECS=300 # Streaming idle timeout between Or use CLI arguments: ```bash cargo run -- --host 0.0.0.0 --port 8080 --backend-url https://enclave.trymaple.ai - -# Development enclaves must be selected explicitly -cargo run -- --backend-url https://enclave.secretgpt.ai --pcr0-environment development ``` +For an unsigned local backend, use `just run-local`. That recipe alone enables +the explicitly named `insecure-local-mock-attestation` Cargo feature. Generic, +release, Docker, and embedded Maple builds leave the feature disabled. + ## ๐Ÿ› ๏ธ Usage ### Using as a Binary @@ -123,7 +124,7 @@ curl http://localhost:8080/v1/embeddings \ You can also embed Maple Proxy in your own Rust application: ```rust -use maple_proxy::{Config, Pcr0Environment, create_app}; +use maple_proxy::{Config, create_app}; use tokio::net::TcpListener; #[tokio::main] @@ -137,7 +138,6 @@ async fn main() -> Result<(), Box> { 8081, // Custom port "https://enclave.trymaple.ai".to_string(), ) - .with_pcr0_environment(Pcr0Environment::Production) .with_api_key("your-api-key-here".to_string()) .with_debug(true) .with_cors(true); @@ -458,9 +458,49 @@ cargo run ``` 1. **Client** makes standard OpenAI API calls to localhost -2. **Maple Proxy** handles authentication and TEE handshake -3. **Requests** are securely forwarded to Maple's TEE infrastructure -4. **Responses** are streamed back to the client in OpenAI format +2. **Maple Proxy** handles authentication and asks the OpenSecret SDK to + establish the TEE channel +3. **OpenSecret SDK** authenticates and authorizes the enclave before accepting + its key and completing key exchange +4. **Requests** are encrypted and forwarded to Maple's TEE infrastructure +5. **Responses** are streamed back to the client in OpenAI format + +### TEE release authorization + +The Sigstore/Rekor release-authorization work belongs in the OpenSecret SDK, +not in Maple Proxy. For each non-local backend, the SDK is expected to: + +1. verify the AWS Nitro attestation document, certificate chain, nonce, and + signature; +2. extract and validate the complete PCR0/PCR1/PCR2 measurement tuple; +3. compare that tuple with the release snapshot embedded in the SDK; and +4. accept the enclave public key and perform key exchange only after the tuple + is present in that snapshot. + +Maple Proxy continues to call `perform_attestation_handshake`; it neither +maintains a second PCR allowlist nor implements a separate Sigstore verifier. +Keeping this policy in the SDK gives every Rust SDK consumer the same +fail-closed authorization boundary before application data is sent. + +There is no Sigstore, Rekor, or other release-metadata network lookup during a +runtime handshake. At SDK update time, the release-snapshot updater verifies +the release manifest and Cosign bundle, including the expected signing identity +and Rekor evidence, before generating the embedded snapshot. Consumers then +review and pin the SDK release containing that generated snapshot. + +Sigstore makes a release statement and its signing identity tamper-evident in +an append-only transparency log. It does **not** prove that an artifact was +reproducibly built, and it does **not** make an old, previously authorized +release fresh. Reproducibility remains a separate Nix rebuild/compare property; +rollback prevention, revocation, or minimum-version policy must also be handled +separately. + +> **Integration status:** this branch pins the exact reviewed SDK integration +> commit with default features disabled. Its embedded release snapshot is +> intentionally empty, so remote handshakes fail closed. Update the pin to a +> reviewed snapshot-bearing commit or published crate after the first signed +> backend release; do not merge or publish this staging state as a working +> production proxy. ## ๐Ÿ“ License diff --git a/docker-compose.yml b/docker-compose.yml index 4e347ef..113fdcc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,6 @@ services: # Backend configuration (defaults to production) - MAPLE_BACKEND_URL=${MAPLE_BACKEND_URL:-https://enclave.trymaple.ai} - - MAPLE_PCR0_ENVIRONMENT=${MAPLE_PCR0_ENVIRONMENT:-production} # Authentication - Uncomment ONLY for private/internal deployments! # For public deployments: Keep this commented out - clients will pass their own API keys diff --git a/examples/library_usage.rs b/examples/library_usage.rs index a44c945..28ed951 100644 --- a/examples/library_usage.rs +++ b/examples/library_usage.rs @@ -1,4 +1,4 @@ -use maple_proxy::{create_app, Config, Pcr0Environment}; +use maple_proxy::{create_app, Config}; use tokio::net::TcpListener; #[tokio::main] @@ -12,7 +12,6 @@ async fn main() -> Result<(), Box> { 8081, // Custom port "https://enclave.trymaple.ai".to_string(), ) - .with_pcr0_environment(Pcr0Environment::Production) .with_api_key("your-api-key-here".to_string()) .with_debug(true) .with_cors(true); diff --git a/flake.nix b/flake.nix index f9fec1a..e314786 100644 --- a/flake.nix +++ b/flake.nix @@ -41,6 +41,9 @@ clang libclang + # TypeScript / OpenClaw plugin + nodejs_22 + # Useful tools jq just diff --git a/justfile b/justfile index fd809f7..b3e8888 100644 --- a/justfile +++ b/justfile @@ -61,7 +61,8 @@ run-with-backend url: # Run pointing to local backend run-local: - @just run-with-backend "http://localhost:3000" + @echo "๐Ÿšง Starting with development-only mock attestation enabled" + @bash -c 'set -a; source .env 2>/dev/null; set +a; MAPLE_BACKEND_URL=http://localhost:3000 cargo run --features insecure-local-mock-attestation' # Run pointing to production backend run-prod: @@ -163,7 +164,6 @@ env: @echo "MAPLE_HOST: ${MAPLE_HOST:-127.0.0.1}" @echo "MAPLE_PORT: ${MAPLE_PORT:-8080}" @echo "MAPLE_BACKEND_URL: ${MAPLE_BACKEND_URL:-https://enclave.trymaple.ai}" - @echo "MAPLE_PCR0_ENVIRONMENT: ${MAPLE_PCR0_ENVIRONMENT:-production}" @echo "MAPLE_API_KEY: ${MAPLE_API_KEY:-[not set]}" @echo "MAPLE_DEBUG: ${MAPLE_DEBUG:-false}" @echo "MAPLE_ENABLE_CORS: ${MAPLE_ENABLE_CORS:-false}" @@ -183,7 +183,6 @@ docker-run: -p ${MAPLE_PORT:-8080}:8080 \ -e MAPLE_API_KEY=${MAPLE_API_KEY} \ -e MAPLE_BACKEND_URL=${MAPLE_BACKEND_URL:-https://enclave.trymaple.ai} \ - -e MAPLE_PCR0_ENVIRONMENT=${MAPLE_PCR0_ENVIRONMENT:-production} \ -e MAPLE_DEBUG=${MAPLE_DEBUG:-false} \ -e MAPLE_ENABLE_CORS=${MAPLE_ENABLE_CORS:-true} \ -e MAPLE_REQUEST_TIMEOUT_SECS=${MAPLE_REQUEST_TIMEOUT_SECS:-300} \ @@ -198,7 +197,6 @@ docker-run-detached: -p ${MAPLE_PORT:-8080}:8080 \ -e MAPLE_API_KEY=${MAPLE_API_KEY} \ -e MAPLE_BACKEND_URL=${MAPLE_BACKEND_URL:-https://enclave.trymaple.ai} \ - -e MAPLE_PCR0_ENVIRONMENT=${MAPLE_PCR0_ENVIRONMENT:-production} \ -e MAPLE_DEBUG=${MAPLE_DEBUG:-false} \ -e MAPLE_ENABLE_CORS=${MAPLE_ENABLE_CORS:-true} \ -e MAPLE_REQUEST_TIMEOUT_SECS=${MAPLE_REQUEST_TIMEOUT_SECS:-300} \ @@ -266,5 +264,50 @@ ghcr-pull tag="latest": @{{container}} pull ghcr.io/opensecretcloud/maple-proxy:{{tag}} @echo "โœ… Pulled ghcr.io/opensecretcloud/maple-proxy:{{tag}}" -# Compatibility alias for the complete repository check. -check-all: check +# === OpenClaw Plugin === + +# Install plugin dependencies +plugin-install: + @echo "๐Ÿ“ฆ Installing plugin dependencies..." + @cd openclaw-plugin && npm install + @echo "โœ… Plugin dependencies installed" + +# Build plugin (TypeScript -> JS) +plugin-build: + @echo "๐Ÿ”จ Building OpenClaw plugin..." + @cd openclaw-plugin && npm run build + @echo "โœ… Plugin built" + +# Lint plugin +plugin-lint: + @echo "๐Ÿ” Linting plugin..." + @cd openclaw-plugin && npm run lint + @echo "โœ… Plugin linted" + +# Test plugin +plugin-test: + @echo "๐Ÿงช Testing plugin..." + @cd openclaw-plugin && npm test + @echo "โœ… Plugin tests passed" + +# Check all (Rust + plugin) +check-all: check plugin-lint plugin-test + @echo "โœ… All checks passed (Rust + Plugin)" + +# Link plugin locally for OpenClaw development +plugin-link: + @echo "๐Ÿ”— Linking plugin to OpenClaw extensions..." + @openclaw plugins install -l ./openclaw-plugin + @echo "โœ… Plugin linked" + +# Pack plugin for npm publishing +plugin-pack: + @echo "๐Ÿ“ฆ Packing plugin for npm..." + @cd openclaw-plugin && npm pack + @echo "โœ… Plugin packed" + +# Publish plugin to npm +plugin-publish: + @echo "๐Ÿš€ Publishing plugin to npm..." + @cd openclaw-plugin && npm publish --access public + @echo "โœ… Plugin published" diff --git a/src/config.rs b/src/config.rs index b0e1126..f0024d5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,4 @@ use clap::Parser; -use opensecret::Pcr0Environment; use serde::Serialize; use std::{net::SocketAddr, time::Duration}; @@ -26,15 +25,6 @@ pub struct Config { )] pub backend_url: String, - /// PCR0 trust-root environment for backend attestation - #[arg( - long, - env = "MAPLE_PCR0_ENVIRONMENT", - default_value = "production", - value_parser = parse_pcr0_environment - )] - pub pcr0_environment: Pcr0Environment, - /// Default API key for Maple/OpenSecret (can be overridden by client Authorization header) #[arg(long, env = "MAPLE_API_KEY")] pub default_api_key: Option, @@ -86,7 +76,6 @@ impl Config { host, port, backend_url, - pcr0_environment: Pcr0Environment::Production, default_api_key: None, debug: false, enable_cors: false, @@ -103,12 +92,6 @@ impl Config { Duration::from_secs(self.stream_idle_timeout_secs) } - /// Builder-style method to select the backend PCR0 trust-root environment - pub fn with_pcr0_environment(mut self, pcr0_environment: Pcr0Environment) -> Self { - self.pcr0_environment = pcr0_environment; - self - } - /// Builder-style method to set the API key pub fn with_api_key(mut self, api_key: String) -> Self { self.default_api_key = Some(api_key); @@ -140,14 +123,6 @@ impl Config { } } -fn parse_pcr0_environment(value: &str) -> Result { - match value { - "production" => Ok(Pcr0Environment::Production), - "development" => Ok(Pcr0Environment::Development), - _ => Err("PCR0 environment must be 'production' or 'development'".to_string()), - } -} - #[derive(Debug, Serialize)] pub(crate) struct OpenAIError { error: OpenAIErrorDetails, @@ -187,28 +162,6 @@ impl OpenAIError { mod tests { use super::*; use clap::{error::ErrorKind, Parser}; - use std::sync::Mutex; - - static PCR0_ENVIRONMENT_LOCK: Mutex<()> = Mutex::new(()); - - fn with_pcr0_environment_env(value: Option<&str>, run: impl FnOnce() -> T) -> T { - let _guard = PCR0_ENVIRONMENT_LOCK.lock().unwrap(); - let previous = std::env::var_os("MAPLE_PCR0_ENVIRONMENT"); - - match value { - Some(value) => std::env::set_var("MAPLE_PCR0_ENVIRONMENT", value), - None => std::env::remove_var("MAPLE_PCR0_ENVIRONMENT"), - } - - let result = run(); - - match previous { - Some(previous) => std::env::set_var("MAPLE_PCR0_ENVIRONMENT", previous), - None => std::env::remove_var("MAPLE_PCR0_ENVIRONMENT"), - } - - result - } #[test] fn config_new_uses_timeout_defaults() { @@ -219,7 +172,6 @@ mod tests { ); assert_eq!(config.request_timeout_secs, DEFAULT_REQUEST_TIMEOUT_SECS); - assert_eq!(config.pcr0_environment, Pcr0Environment::Production); assert_eq!( config.stream_idle_timeout_secs, DEFAULT_STREAM_IDLE_TIMEOUT_SECS @@ -234,51 +186,6 @@ mod tests { ); } - #[test] - fn pcr0_environment_defaults_to_production_for_cli() { - let config = - with_pcr0_environment_env(None, || Config::try_parse_from(["maple-proxy"]).unwrap()); - - assert_eq!(config.pcr0_environment, Pcr0Environment::Production); - } - - #[test] - fn pcr0_environment_accepts_explicit_development_cli_value() { - let config = - Config::try_parse_from(["maple-proxy", "--pcr0-environment", "development"]).unwrap(); - - assert_eq!(config.pcr0_environment, Pcr0Environment::Development); - } - - #[test] - fn pcr0_environment_builder_selects_development() { - let config = Config::new( - "127.0.0.1".to_string(), - 8080, - "https://enclave.secretgpt.ai".to_string(), - ) - .with_pcr0_environment(Pcr0Environment::Development); - - assert_eq!(config.pcr0_environment, Pcr0Environment::Development); - } - - #[test] - fn pcr0_environment_accepts_explicit_development_env_value() { - let config = with_pcr0_environment_env(Some("development"), || { - Config::try_parse_from(["maple-proxy"]).unwrap() - }); - - assert_eq!(config.pcr0_environment, Pcr0Environment::Development); - } - - #[test] - fn pcr0_environment_rejects_unknown_values() { - let error = - Config::try_parse_from(["maple-proxy", "--pcr0-environment", "staging"]).unwrap_err(); - - assert_eq!(error.kind(), ErrorKind::ValueValidation); - } - #[test] fn timeout_builder_methods_override_defaults() { let config = Config::new( diff --git a/src/lib.rs b/src/lib.rs index 2650eae..41d3f9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ mod config; mod proxy; pub use config::Config; -pub use opensecret::Pcr0Environment; use proxy::{health_check, proxy_openai_request, ProxyState}; use axum::{ diff --git a/src/main.rs b/src/main.rs index 852d57a..42d624a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,6 @@ async fn main() -> anyhow::Result<()> { info!("Starting Maple Proxy Server"); info!("Version: {}", env!("CARGO_PKG_VERSION")); info!("Backend URL: {}", config.backend_url); - info!("PCR0 environment: {:?}", config.pcr0_environment); info!("Binding to: {}", config.socket_addr()?); if config.default_api_key.is_some() { diff --git a/src/proxy.rs b/src/proxy.rs index c13b76f..95378d4 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -107,7 +107,6 @@ impl ProxyState { let cache_key = api_key.to_string(); let client_entry = self.client_entry_for_api_key(&cache_key); let backend_url = self.config.backend_url.clone(); - let pcr0_environment = self.config.pcr0_environment; let request_timeout = self.config.request_timeout(); let init_api_key = cache_key.clone(); @@ -118,14 +117,9 @@ impl ProxyState { "Creating OpenSecret client for API key: {}...", &init_api_key[..8.min(init_api_key.len())] ); - create_client_with_auth( - &backend_url, - &init_api_key, - pcr0_environment, - request_timeout, - ) - .await - .map(Arc::new) + create_client_with_auth(&backend_url, &init_api_key, request_timeout) + .await + .map(Arc::new) }) .await; @@ -209,15 +203,10 @@ fn extract_api_key( async fn create_client_with_auth( backend_url: &str, api_key: &str, - pcr0_environment: opensecret::Pcr0Environment, request_timeout: Duration, ) -> Result { - let client = OpenSecretClient::new_with_api_key_and_pcr0_environment( - backend_url, - api_key.to_string(), - pcr0_environment, - ) - .map_err(|e| transport_error_response("OpenSecret client creation", &e))?; + let client = OpenSecretClient::new_with_api_key(backend_url, api_key.to_string()) + .map_err(|e| transport_error_response("OpenSecret client creation", &e))?; // Perform attestation handshake tokio::time::timeout(request_timeout, client.perform_attestation_handshake()) @@ -435,7 +424,6 @@ mod tests { host: "127.0.0.1".to_string(), port: 0, backend_url: "http://localhost:3000".to_string(), - pcr0_environment: opensecret::Pcr0Environment::Production, default_api_key: None, debug: false, enable_cors: false,