Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
db2a6fd
chore: bump qos crates to 0.12.2 and add qos_json workspace pin
emostov Jul 15, 2026
d4e1c90
feat: add tvc-utils crate with fake manifest helpers for tests
emostov Jul 15, 2026
b61568e
feat: add tvc-axum crate with signed responses and attestation/manife…
emostov Jul 15, 2026
412eaa7
refactor(tvc-utils): move fake manifest helpers into a fake module
emostov Jul 16, 2026
ca240eb
feat(tvc-utils): support manifest-set approvals in fake manifest builder
emostov Jul 16, 2026
9b0206d
feat(tvc-utils): add attestation and manifest trust chain verifier
emostov Jul 16, 2026
33cb3a8
test(tvc-axum): verify emitted trust chain with the tvc-utils verifier
emostov Jul 16, 2026
f793d86
build: patch qos_* crates to qos branch with manifest fake/verify hel…
emostov Jul 16, 2026
f8fcd81
test(tvc-axum): use qos_core fake and verify helpers in tests
emostov Jul 16, 2026
de1ac18
refactor!: remove tvc-utils crate, superseded by qos_core helpers
emostov Jul 16, 2026
56c6463
fix(tvc-axum): adapt tests to qos verify API changes
emostov Jul 16, 2026
e504850
feat(tvc-axum): client-side response verification helpers
emostov Jul 16, 2026
1aeed56
test(tvc-axum): verify signed responses with the client helpers
emostov Jul 16, 2026
3069918
refactor(tvc-axum): isolate response signing
emostov Jul 16, 2026
732365a
test(tvc-axum): trim redundant coverage
emostov Jul 16, 2026
c051ffc
refactor(tvc-axum): keep signing context local
emostov Jul 16, 2026
f0dcde2
fix(tvc-axum): bind signatures to request query
emostov Jul 23, 2026
28a791b
fix(tvc-axum): validate manifest quorum key
emostov Jul 23, 2026
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
99 changes: 80 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 29 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ members = [
"examples",
# Contains utilities to work with Turnkey proofs (boot proofs, app proofs)
"proofs",
"tvc"
"tvc",
# Axum middleware for Turnkey Verifiable Cloud apps (signed responses)
"tvc-axum"
]
resolver = "2"

[workspace.dependencies]
# QuorumOS
qos_core = { version = "0.12.1", default-features = false }
qos_crypto = { version = "0.12.1", default-features = false }
qos_nsm = { version = "0.12.1", default-features = false }
qos_p256 = { version = "0.12.1", default-features = false }
qos_core = { version = "0.12.2", default-features = false }
qos_crypto = { version = "0.12.2", default-features = false }
qos_hex = { version = "0.12.2", default-features = false }
qos_json = { version = "0.12.2", default-features = false }
qos_nsm = { version = "0.12.2", default-features = false }
qos_p256 = { version = "0.12.2", default-features = false }

# Encoding and serialization
base64 = { version = "0.22.0", default-features = false, features = ["std"] }
Expand Down Expand Up @@ -50,8 +54,11 @@ tracing = { version = "0.1.41", default-features = false, features = ["std"] }
tracing-subscriber = { version = "0.3.23", default-features = false, features = ["ansi", "env-filter", "fmt"] }

# Web/HTTP
axum = { version = "0.8", default-features = false }
http-body-util = { version = "0.1", default-features = false }
mime = { version = "0.3.17", default-features = false }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "http2"] }
tower = { version = "0.5", default-features = false }

# Async runtime
tokio = { version = "1.47.1", default-features = false }
Expand Down Expand Up @@ -110,3 +117,20 @@ turnkey_api_key_stamper = { path = "api_key_stamper", version = "0.11.0" }
turnkey_client = { path = "client", version = "0.11.0" }
turnkey_enclave_encrypt = { path = "enclave_encrypt", version = "0.11.0" }
turnkey_proofs = { path = "proofs", version = "0.11.0" }

# TODO: remove this patch section once qos PR #765
# (https://github.com/tkhq/qos/pull/765) merges and a new qos_core version is
# published to crates.io, then bump the qos_* version pins above.
#
# The fake manifest builder and trust chain verifier used by tvc-axum live in
# qos_core on that branch but are not yet in a published release. All qos_*
# crates are patched to the same git revision because qos_core depends on the
# other qos crates via in-repo path dependencies; patching only qos_core would
# produce duplicate crate sources (crates.io + git) and type mismatches.
[patch.crates-io]
qos_core = { git = "https://github.com/tkhq/qos", branch = "zeke/manifest-fake-and-verify-helpers" }
qos_crypto = { git = "https://github.com/tkhq/qos", branch = "zeke/manifest-fake-and-verify-helpers" }
qos_hex = { git = "https://github.com/tkhq/qos", branch = "zeke/manifest-fake-and-verify-helpers" }
qos_json = { git = "https://github.com/tkhq/qos", branch = "zeke/manifest-fake-and-verify-helpers" }
qos_nsm = { git = "https://github.com/tkhq/qos", branch = "zeke/manifest-fake-and-verify-helpers" }
qos_p256 = { git = "https://github.com/tkhq/qos", branch = "zeke/manifest-fake-and-verify-helpers" }
31 changes: 31 additions & 0 deletions tvc-axum/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "tvc-axum"
version = "0.11.0"
edition = "2024"
license = "Apache-2.0"
description = "Axum middleware for Turnkey Verifiable Cloud apps: signed responses with attestation and manifest headers"
repository = "https://github.com/tkhq/rust-sdk"
homepage = "https://turnkey.com"
readme = "README.md"
documentation = "https://docs.rs/tvc-axum"
keywords = ["turnkey", "tvc", "enclaves", "axum", "attestation"]
categories = ["cryptography", "web-programming"]

[dependencies]
axum = { workspace = true }
base64 = { workspace = true }
http-body-util = { workspace = true }
qos_core = { workspace = true }
qos_json = { workspace = true }
qos_nsm = { workspace = true }
qos_p256 = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tower = { workspace = true }

[dev-dependencies]
qos_core = { workspace = true, features = ["mock"] }
qos_nsm = { workspace = true, features = ["mock"] }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
tower = { workspace = true, features = ["util"] }
41 changes: 41 additions & 0 deletions tvc-axum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# tvc-axum

Axum middleware for Turnkey Verifiable Cloud applications.

`ResponseSigningLayer` signs every HTTP response with the enclave's ephemeral
key (and optionally the quorum key) using RFC 9421 HTTP Message Signatures
and an RFC 9530 `Content-Digest`, and attaches the NSM attestation document
and QOS manifest envelope as response headers so clients can verify the full
trust chain: manifest → attestation document → response signature.

## Response headers

| Header | Contents |
| --- | --- |
| `Content-Digest` | `sha-256=:BASE64(sha256(body)):` (RFC 9530) |
| `Signature-Input` | `ephemeral=("@method";req "@path";req "@query";req "@status" "content-digest");created=UNIX;keyid="ephemeral";alg="ecdsa-p256-sha256"` plus a `quorum=(...)` entry when a quorum key is configured (RFC 9421) |
| `Signature` | `ephemeral=:BASE64:` plus `quorum=:BASE64:` when configured (RFC 9421) |
| `x-tvc-attestation-doc` | base64 NSM attestation document; `user_data` = manifest hash, `public_key` = ephemeral public key |
| `x-tvc-manifest-envelope` | base64 canonical manifest envelope bytes (`VersionedManifestEnvelope::to_storage_vec`) |

The ephemeral public key is intentionally never sent as its own header:
verifiers must extract it from the attestation document.

## Usage

```rust,ignore
let layer = ResponseSigningLayer::builder()
.ephemeral_key(ephemeral_pair) // Arc<qos_p256::P256Pair>, required
.quorum_key(quorum_pair) // optional
.nsm(nsm) // Arc<dyn qos_nsm::NsmProvider + Send + Sync>, required
.manifest_envelope(envelope) // qos_core VersionedManifestEnvelope, required
.build()?;

let app = Router::new().route("/hello", get(hello)).layer(layer);
```

Use `QosJson<T>` in handlers to serialize response bodies with `qos_json`
canonical JSON.

See `tests/response_signing.rs` for a complete verification walkthrough
against an NSM.
Loading