Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# attestation-server

A Go HTTP server for serving TEE (Trusted Execution Environment) attestation documents. The server runs behind an Envoy reverse proxy that terminates TLS — Envoy uses the private certificate for service-to-service mTLS (setting the XFCC header with the client cert hash) and optionally the public certificate for Internet-facing ingress without client certificates.
A Go HTTP server for serving TEE (Trusted Execution Environment) attestation documents. The server runs behind an Envoy reverse proxy that terminates TLS — Envoy uses the private certificate for service-to-service mTLS (setting the XFCC header with the client cert hash) and optionally the public certificate for Internet-facing ingress without client certificates. The private certificate is only required when dependency endpoints are configured (mTLS for TEE-to-TEE communication) or when no public certificate is set; a TEE with only a public certificate can serve attestation reports to external clients without maintaining private TLS infrastructure.

## Tech stack

Expand All @@ -15,7 +15,7 @@ main.go # entry point
cmd/root.go # cobra root command; initializes config, logger, and starts server
internal/attestation.go # GET /api/v1/attestation handler and helpers (package app)
internal/config.go # Config struct and LoadConfig() (package app)
internal/dependencies.go # Transitive dependency attestation: parallel fetch, verify, cycle detection (package app)
internal/dependencies.go # Transitive dependency attestation: parallel fetch, verify, cycle detection, server cert validation (package app)
internal/cosign.go # Cosign signature verification: bundle fetch, Sigstore/Rekor verification, Fulcio OID extraction + validation (package app)
internal/endorsements.go # Endorsement document fetching, DNSSEC, measurement validation, cosign integration (package app)
internal/fetch.go # Generic HTTP fetch with retry, per-attempt WARN logging, cache (ristretto), TTL parsing, cachedHTTPSGetter for TDX collateral — shared by endorsements, cosign, and TDX (package app)
Expand Down Expand Up @@ -129,7 +129,7 @@ skip_verify = false
[tls.private]
cert_path = ""
key_path = ""
ca_path = "" # required
ca_path = "" # required when private cert is configured
```

### CLI flags
Expand Down Expand Up @@ -158,9 +158,9 @@ All settings can be configured via environment variables prefixed with `ATTESTAT
| `ATTESTATION_SERVER_TLS_PUBLIC_CERT_PATH` | `tls.public.cert_path` | — | Path to public TLS certificate (PEM) |
| `ATTESTATION_SERVER_TLS_PUBLIC_KEY_PATH` | `tls.public.key_path` | — | Path to public TLS private key (PEM) |
| `ATTESTATION_SERVER_TLS_PUBLIC_SKIP_VERIFY` | `tls.public.skip_verify` | `false` | Skip system/Mozilla root CA chain verification for the public certificate |
| `ATTESTATION_SERVER_TLS_PRIVATE_CERT_PATH` | `tls.private.cert_path` | — | **Required.** Path to private TLS certificate (PEM) |
| `ATTESTATION_SERVER_TLS_PRIVATE_KEY_PATH` | `tls.private.key_path` | — | **Required.** Path to private TLS private key (PEM) |
| `ATTESTATION_SERVER_TLS_PRIVATE_CA_PATH` | `tls.private.ca_path` | — | **Required.** PEM CA bundle — all private certs in the dependency chain must be issued by this CA |
| `ATTESTATION_SERVER_TLS_PRIVATE_CERT_PATH` | `tls.private.cert_path` | — | Path to private TLS certificate (PEM). Required when dependency endpoints are configured or no public certificate is set |
| `ATTESTATION_SERVER_TLS_PRIVATE_KEY_PATH` | `tls.private.key_path` | — | Path to private TLS private key (PEM). Required when dependency endpoints are configured or no public certificate is set |
| `ATTESTATION_SERVER_TLS_PRIVATE_CA_PATH` | `tls.private.ca_path` | — | PEM CA bundle — all private certs in the dependency chain must be issued by this CA. Required when private cert is configured |
| `ATTESTATION_SERVER_REPORT_EVIDENCE_NITRONSM` | `report.evidence.nitronsm` | `false` | Enable Nitro NSM evidence (exclusive: cannot combine with others) |
| `ATTESTATION_SERVER_REPORT_EVIDENCE_NITROTPM` | `report.evidence.nitrotpm` | `false` | Enable Nitro TPM evidence |
| `ATTESTATION_SERVER_REPORT_EVIDENCE_SEVSNP` | `report.evidence.sevsnp` | `false` | Enable SEV-SNP evidence |
Expand All @@ -176,7 +176,7 @@ All settings can be configured via environment variables prefixed with `ATTESTAT
| `ATTESTATION_SERVER_RATELIMIT_STALL_TIMEOUT` | `ratelimit.stall_timeout` | `10s` | Max time an over-limit request is stalled before receiving 429; IP extracted from `X-Envoy-Original-IP` > `X-Forwarded-For` > connection IP |
| `ATTESTATION_SERVER_SECURE_BOOT_ENFORCE` | `secure_boot.enforce` | `false` | Enforce UEFI Secure Boot; exit on startup if not enabled. UEFI secure boot detection is skipped when NitroNSM evidence is enabled (enclaves have no EFI firmware; boot integrity is proven by NSM PCR measurements) |
| `ATTESTATION_SERVER_REPORT_USER_DATA_ENV` | `report.user_data.env` | `[]` | Comma-separated environment variable names to include in report (unique) |
| `ATTESTATION_SERVER_DEPENDENCIES_ENDPOINTS` | `dependencies.endpoints` | `[]` | Comma-separated URLs of dependency attestation servers. HTTPS endpoints are verified against the private CA bundle (mTLS); HTTP endpoints are a design decision for transparent proxy configurations where Envoy diverts traffic through mTLS on non-loopback interfaces — the e2e encryption proof (XFCC fingerprint check) ensures the connection was mTLS-protected regardless of the URL scheme |
| `ATTESTATION_SERVER_DEPENDENCIES_ENDPOINTS` | `dependencies.endpoints` | `[]` | Comma-separated URLs of dependency attestation servers. HTTPS endpoints are verified against the private CA bundle (mTLS) and the server's TLS certificate fingerprint is matched against `data.tls.private` in the attestation report; HTTP endpoints are a design decision for transparent proxy configurations where Envoy diverts traffic through mTLS on non-loopback interfaces — the e2e encryption proof (XFCC fingerprint check + server cert check for HTTPS) ensures the connection was mTLS-protected regardless of the URL scheme |
| `ATTESTATION_SERVER_ENDORSEMENTS_DNSSEC` | `endorsements.dnssec` | `false` | Require strict DNSSEC validation for endorsement URL hosts |
| `ATTESTATION_SERVER_ENDORSEMENTS_ALLOWED_DOMAINS` | `endorsements.allowed_domains` | `[]` | Comma-separated list of allowed endorsement hostnames (exact match). Empty = unrestricted. Applies to both own and dependency endorsement URLs |
| `ATTESTATION_SERVER_ENDORSEMENTS_CLIENT_TIMEOUT` | `endorsements.client.timeout` | `10s` | Overall timeout for fetching endorsement documents and cosign signatures (with retries) |
Expand Down Expand Up @@ -254,7 +254,12 @@ When `dependencies.endpoints` is configured, the attestation handler fetches and

Each dependency response is parsed as an `AttestationReport`, verified (nonce binding + cryptographic evidence verification for all known TEE types including NitroTPM→SEV-SNP chaining), and embedded as `json.RawMessage` in the `dependencies` field. Raw bytes are stored instead of re-marshaled structs to avoid `goccy/go-json` zero-copy string issues.

After cryptographic verification, the client certificate fingerprint check enforces end-to-end encryption: the dependency's `data.tls.client` must be present and match the SHA-256 fingerprint of our private certificate (which is used as the client cert for outgoing mTLS connections). If missing or mismatched, a descriptive error is logged and an opaque error is returned to the caller.
After cryptographic verification, two certificate fingerprint checks enforce end-to-end encryption:

1. **Client cert check**: the dependency's `data.tls.client` must be present and match the SHA-256 fingerprint of our private certificate (which is used as the client cert for outgoing mTLS connections). This proves the dependency (via its Envoy) saw our client certificate.
2. **Server cert check** (HTTPS only): when the connection is over HTTPS, the server's leaf certificate fingerprint observed during the TLS handshake is compared against the dependency's `data.tls.private`. This binds the attestation report to the actual TLS connection, catching relay proxies that hold a valid CA-signed cert but are not the TEE. Skipped for plain HTTP endpoints where `resp.TLS` is nil (Envoy terminates TLS on the loopback interface).

If either check fails, a descriptive error is logged and an opaque error is returned to the caller.

The dependency HTTP client verifies server certificates against the private CA bundle (`tls.private.ca_path`) and presents the private certificate as the TLS client cert. All private certificates in the dependency chain must be issued by the same CA — Envoy only populates the XFCC header (which provides the client cert fingerprint) when the client cert passes CA verification.

Expand Down Expand Up @@ -306,7 +311,7 @@ When disabled, a startup warning is logged: "certificate revocation checking is

## Error information leakage

The server returns opaque `"internal error"` messages for all 5xx responses to prevent leaking device errors, file paths, and firmware codes to external callers. The real error is logged at ERROR level with `request_id` for debugging. 4xx error messages are preserved since they describe client-fixable problems (bad nonce, missing cert, etc.).
The server preserves handler-controlled error messages for all `fiber.NewError` responses (both 4xx and 5xx). Handler code must never include internal details (device errors, file paths, firmware codes) in these messages — only opaque descriptions like `"attestation failed"` or `"dependency attestation failed"`. Unhandled errors (plain `error` values not wrapped in `fiber.NewError`) fall back to a generic `"internal error"` message. The real error is always logged at ERROR level with `request_id` for debugging.

## XFCC header validation

Expand Down Expand Up @@ -417,7 +422,7 @@ Fixture files:
- `pkg/sevsnp/testdata/sevsnp_attestation_gcp.json`
- `pkg/tdx/testdata/tdx_attestation.json`
- `internal/testdata/nitrotpm_sevsnp_attestation.json` (chained NitroTPM → SEV-SNP)
- `internal/testdata/dependencies_attestation.json` (diamond dependency graph: A → {B, C}, B → C with NitroTPM+SEV-SNP, TDX, and SEV-SNP evidence across services; each dependency has client cert matching caller's private cert)
- `internal/testdata/dependencies_attestation.json` (diamond dependency graph: A → {B, C}, B → C with NitroTPM+SEV-SNP, TDX, and SEV-SNP evidence across services; each dependency has client cert matching caller's private cert; server cert validation is tested via unit tests with synthetic fingerprints since fixtures lack TLS connection state)

## Nix build

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ sevsnp = false # AMD SEV-SNP (combinable with nitrotpm)
tdx = false # Intel TDX (exclusive)

[tls.private]
cert_path = "" # required — private mTLS certificate (ECDSA)
key_path = "" # required — private key
ca_path = "" # required — CA bundle for the dependency chain
cert_path = "" # private mTLS certificate (ECDSA) — required with dependencies or without public cert
key_path = "" # private key
ca_path = "" # CA bundle for the dependency chain

[dependencies]
endpoints = [] # URLs of downstream attestation servers
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ When `dependencies.endpoints` is configured, the server fetches attestation repo
a. Each dependency receives nonce_digest as x-attestation-nonce header
b. Each response is cryptographically verified (evidence + nonce binding)
c. Client certificate fingerprint is checked for e2e encryption proof
c'. For HTTPS: server TLS certificate is matched against data.tls.private
d. Endorsement measurements are validated against dependency evidence
5. Server collects own TEE evidence using nonce_digest
6. Server returns the full report with embedded dependency reports
Expand Down
15 changes: 10 additions & 5 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,18 @@ Private TLS key material is loaded inside the TEE and never leaves it. The certi

### Dependency e2e verification

After cryptographically verifying a dependency's attestation report, the server checks that `data.tls.client` in the dependency's response matches the SHA-256 fingerprint of the private certificate that was presented as the TLS client cert when connecting. This confirms:
After cryptographically verifying a dependency's attestation report, the server performs two certificate fingerprint checks:

1. **Client cert (XFCC)**: `data.tls.client` in the dependency's response must match the SHA-256 fingerprint of the private certificate that was presented as the TLS client cert when connecting. This confirms the dependency saw our specific client certificate (not a proxy's).
2. **Server cert** (HTTPS only): when the dependency was reached over HTTPS, the server's TLS leaf certificate fingerprint observed during the handshake must match the dependency's `data.tls.private`. This binds the attestation report to the actual TLS peer, catching relay proxies that hold a valid CA-signed cert but are not the TEE. This check is independent of Envoy's XFCC forwarding policy. Skipped for plain HTTP endpoints (transparent proxy configurations where Envoy terminates TLS on the loopback interface).

Together these confirm:

- The dependency saw our specific client certificate (not a proxy's)
- The connection was encrypted end-to-end between the two TEEs
- No intermediate proxy stripped or replaced the client certificate
- The attestation report was produced by the server that terminated our TLS connection, not relayed through an intermediary

If the fingerprint is missing or mismatched, a descriptive error is logged but an opaque error is returned to the caller (preventing information leakage about the internal certificate infrastructure).
If either fingerprint is missing or mismatched, a descriptive error is logged but an opaque error is returned to the caller (preventing information leakage about the internal certificate infrastructure).

### XFCC header validation

Expand Down Expand Up @@ -202,8 +207,8 @@ The server distinguishes between internal and external error messages:

- **E2E encryption failures** (missing/mismatched client certificate): a descriptive message is logged for debugging, but an opaque error is returned to the caller
- **Dependency URLs**: not included in error responses to callers
- **Upstream error classification**: timeout errors map to 504, connection errors to 503, everything else to 500 — without exposing internal details
- **5xx responses**: all server errors return a generic `"internal error"` message; the real error is logged at ERROR level with request_id for debugging. 4xx errors preserve their message since they describe client-fixable problems
- **Upstream error classification**: timeout errors map to 504, transport errors (connection refused, reset, DNS) to 503, everything else (including TLS certificate verification failures) to 500 — without exposing internal details
- **5xx responses**: handler-controlled error messages (from `fiber.NewError`) are preserved — these are opaque by design (e.g. `"attestation failed"`, `"dependency attestation failed"`). Unhandled errors (plain `error` values) fall back to `"internal error"`. The real error is logged at ERROR level with request_id for debugging. 4xx errors preserve their message since they describe client-fixable problems

## Build info integrity

Expand Down
1 change: 1 addition & 0 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Each entry in `dependencies` is a complete attestation report with the same stru

- **Nonce binding**: the dependency's nonce should match the nonce digest from the parent's `data`
- **Client certificate**: the dependency's `data.tls.client` should match the parent's private certificate fingerprint (`data.tls.private`), confirming mTLS between the two TEEs
- **Server certificate** (HTTPS only): if the dependency was fetched over HTTPS, the server's TLS leaf certificate fingerprint observed during the handshake should match the dependency's `data.tls.private`. This binds the attestation report to the actual TLS peer, catching relay proxies that hold a valid CA-signed cert but are not the TEE. Skipped for plain HTTP endpoints (transparent proxy configurations)
- **Endorsements**: the dependency's endorsement documents should be fetched and validated independently — each service in the chain has its own build provenance and golden measurements from its own CI/CD pipeline

## Example: minimal Go verifier
Expand Down
16 changes: 8 additions & 8 deletions internal/attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ func TestDependencyAttestation_DiamondGraph(t *testing.T) {
if err := json.Unmarshal(f.Dependencies[0], &report); err != nil {
t.Fatalf("parsing dependency: %v", err)
}
if err := verifyDependencyReportOnly(&report, nonceHex, aPrivateFP, now); err != nil {
if err := verifyDependencyReportOnly(&report, nonceHex, aPrivateFP, "", now); err != nil {
t.Fatalf("verifyDependencyReport() error: %v", err)
}
})
Expand Down Expand Up @@ -627,7 +627,7 @@ func TestDependencyAttestation_DiamondGraph(t *testing.T) {
if err := json.Unmarshal(f.Dependencies[1], &report); err != nil {
t.Fatalf("parsing dependency: %v", err)
}
if err := verifyDependencyReportOnly(&report, nonceHex, aPrivateFP, now); err != nil {
if err := verifyDependencyReportOnly(&report, nonceHex, aPrivateFP, "", now); err != nil {
t.Fatalf("verifyDependencyReport() error: %v", err)
}
})
Expand Down Expand Up @@ -695,7 +695,7 @@ func TestDependencyAttestation_DiamondGraph(t *testing.T) {
if err := json.Unmarshal(deps[0].Dependencies[0], &report); err != nil {
t.Fatalf("parsing nested dependency: %v", err)
}
if err := verifyDependencyReportOnly(&report, bNonceHex, bPrivateFP, now); err != nil {
if err := verifyDependencyReportOnly(&report, bNonceHex, bPrivateFP, "", now); err != nil {
t.Fatalf("verifyDependencyReport() error: %v", err)
}
})
Expand All @@ -708,7 +708,7 @@ func TestDependencyAttestation_DiamondGraph(t *testing.T) {
t.Fatalf("parsing dependency: %v", err)
}
// Nonce check fires before client cert check, so the fingerprint is irrelevant here.
err := verifyDependencyReportOnly(&report, "0000000000000000", aPrivateFP, now)
err := verifyDependencyReportOnly(&report, "0000000000000000", aPrivateFP, "", now)
if err == nil {
t.Fatal("verifyDependencyReport() should fail with wrong nonce")
}
Expand All @@ -724,7 +724,7 @@ func TestDependencyAttestation_DiamondGraph(t *testing.T) {
if err := json.Unmarshal(deps[0].Dependencies[0], &report); err != nil {
t.Fatalf("parsing transitive dependency: %v", err)
}
err := verifyDependencyReportOnly(&report, nonceHex, bPrivateFP, now)
err := verifyDependencyReportOnly(&report, nonceHex, bPrivateFP, "", now)
if err == nil {
t.Fatal("verifyDependencyReport() should fail when using A's nonce for transitive C")
}
Expand All @@ -738,7 +738,7 @@ func TestDependencyAttestation_DiamondGraph(t *testing.T) {
t.Fatalf("parsing dependency: %v", err)
}
wrongFP := "0000000000000000000000000000000000000000000000000000000000000000"
err := verifyDependencyReportOnly(&report, nonceHex, wrongFP, now)
err := verifyDependencyReportOnly(&report, nonceHex, wrongFP, "", now)
if err == nil {
t.Fatal("expected error for wrong client cert FP")
}
Expand All @@ -762,7 +762,7 @@ func TestDependencyAttestation_DiamondGraph(t *testing.T) {
depReport.Data = json.RawMessage(tamperedData)

// Nonce will still match but client cert is missing — e2e error.
err := verifyDependencyReportOnly(&depReport, nonceHex, aPrivateFP, now)
err := verifyDependencyReportOnly(&depReport, nonceHex, aPrivateFP, "", now)
if err == nil {
t.Fatal("expected error for missing client cert in tampered dep")
}
Expand All @@ -788,7 +788,7 @@ func TestDependencyAttestation_DiamondGraph(t *testing.T) {
tamperedDataJSON, _ := json.Marshal(depData)
depReport.Data = json.RawMessage(tamperedDataJSON)

err := verifyDependencyReportOnly(&depReport, nonceHex, tamperedFP, now)
err := verifyDependencyReportOnly(&depReport, nonceHex, tamperedFP, "", now)
if err == nil {
t.Fatal("expected crypto verification failure for tampered data")
}
Expand Down
Loading
Loading