Skip to content

Am/example/tvc cosign#2

Open
am-hernandez wants to merge 16 commits into
mainfrom
am/example/tvc-cosign
Open

Am/example/tvc cosign#2
am-hernandez wants to merge 16 commits into
mainfrom
am/example/tvc-cosign

Conversation

@am-hernandez

Copy link
Copy Markdown
Contributor

Add tvc-cosign example

tvc-cosign is a Rust service, its core endpoint being POST /cosign: it parses an unsigned EVM transaction, classifies it against a baked-in ruleset (PROGRAMMATIC | ADMIN | REJECT), and returns a stamped SIGN_TRANSACTION_V2 activity signed by one of two quorum-key-derived API keys (programmatic / admin). The caller submits the activity to Turnkey, where policies either auto-complete it (programmatic) or hold it for human approval (admin). It also exposes GET /health (liveness) and GET /pubkeys (the two stamping public keys to register as API users).

The README covers the reproducible build, CLI deploy, Turnkey org setup (users + policies), and the integration flow.

Closes TVC-176.

Testing

  • Unit tests (cargo test, 35 tests): transaction parsing (EIP-1559 and legacy), classification across the programmatic / admin / reject cases, stamp construction, activity-body shape, and App Proof generation. Crypto is pinned with known-answer tests, including the enclave key derivation byte-checked against the QOS reference.
  • End-to-end against Turnkey: deployed to a TVC enclave and ran the full flow. GET /pubkeys matched the registered API users. An allowlisted ERC-20 transfer classified PROGRAMMATIC, was stamped, and submitting the activity to Turnkey returned ACTIVITY_STATUS_COMPLETED (policy auto-approved). An admin-selector approve classified ADMIN, returned ACTIVITY_STATUS_CONSENSUS_NEEDED, and completed only after a non-root human approver signed off via passkey, confirming the human-consensus policy gated it.
  • Each /cosign response carried an App Proof signed by the enclave's ephemeral key.

Note

Uses the current static/well-known TVC quorum key, so the derived stamping keys are not yet secret. Documented in the README's limitations.

@socket-security

socket-security Bot commented Jul 10, 2026

Copy link
Copy Markdown

@socket-security

socket-security Bot commented Jul 10, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: cargo hyper-util is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/axum@0.8.9cargo/hyper-util@0.1.20

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/hyper-util@0.1.20. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo libc is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/sha2@0.11.0cargo/hkdf@0.13.0cargo/tokio@1.52.3cargo/alloy-primitives@1.6.0cargo/p256@0.14.0cargo/alloy-consensus@2.1.1cargo/alloy-eips@2.1.1cargo/libc@0.2.186

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/libc@0.2.186. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo tokio is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: tvc-cosign/Cargo.lockcargo/tokio@1.52.3

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/tokio@1.52.3. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: cargo zerocopy is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/alloy-primitives@1.6.0cargo/alloy-consensus@2.1.1cargo/zerocopy@0.8.53

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/zerocopy@0.8.53. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

git-subtree-dir: tvc-cosign
git-subtree-mainline: 55e3e1b
git-subtree-split: 7ea2fa1

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new tvc-cosign Rust example service demonstrating a TVC enclave “transaction cosigner” that classifies unsigned EVM transactions and returns a Turnkey-stampable SIGN_TRANSACTION_V2 request plus an enclave-signed App Proof.

Changes:

  • Introduces an Axum HTTP service with /health, /pubkeys, and /cosign endpoints, including request stamping + App Proof generation.
  • Implements unsigned-EVM transaction parsing and a TOML-driven rules engine for PROGRAMMATIC | ADMIN | REJECT classification.
  • Adds reproducible-build packaging (Dockerfile) and extensive documentation (README) describing deployment + verification flow.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tvc-cosign/src/main.rs Axum server entrypoint, CLI parsing, endpoints, and response shapes.
tvc-cosign/src/activity.rs Deterministic construction of Turnkey SIGN_TRANSACTION_V2 request bodies.
tvc-cosign/src/tx.rs Minimal parser for unsigned EVM transaction signing payloads (typed + legacy).
tvc-cosign/src/rules.rs TOML-backed ruleset parsing + transaction classification logic.
tvc-cosign/src/stamp.rs Turnkey request stamp construction (P-256 ECDSA, base64url envelope).
tvc-cosign/src/proof.rs App Proof envelope + payload construction and signing with ephemeral key.
tvc-cosign/src/keys.rs Quorum-key HKDF derivation of API keys + QOS ephemeral key reconstruction.
tvc-cosign/src/config.rs Startup configuration loading (org id + ruleset selection).
tvc-cosign/README.md End-to-end documentation: setup, reproducible build, deploy, verify proofs.
tvc-cosign/rules.example.toml Example ruleset schema and documented defaults.
tvc-cosign/Dockerfile Reproducible build + minimal runtime image, prints expectedPivotDigest.
tvc-cosign/Cargo.toml Crate manifest and dependencies.
tvc-cosign/Cargo.lock Locked dependency graph for reproducible builds.
tvc-cosign/.gitignore Ignores build artifacts and local per-deployment rules files.
tvc-cosign/.dockerignore Minimizes docker build context for deterministic image digests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tvc-cosign/src/rules.rs
Comment on lines +18 to +21
/// Ruleset compiled into the binary at build time. TVC runs only the pivot binary,
/// so the ruleset ships inside it rather than as a file in the image.
const EMBEDDED_RULES_TOML: &str = include_str!("../rules.toml");

Comment thread tvc-cosign/src/rules.rs
Comment on lines +63 to +71
pub fn classify(signer: Address, tx: &ParsedTx, rules: &Ruleset) -> Classification {
if !rules.allowed_signers.contains(&signer) {
return Classification::Reject;
}

let Some(selector) = tx.selector() else {
// No calldata (e.g. a native transfer) — out of scope for this POC.
return Classification::Reject;
};
Comment thread tvc-cosign/src/rules.rs
Comment on lines +106 to +116
fn decode_transfer_args(input: &[u8]) -> Option<(Address, U256)> {
// 4 (selector) + 32 (address) + 32 (amount)
let args = input.get(4..68)?;
let (addr_word, amount_word) = args.split_at(32);
if addr_word[..12].iter().any(|&b| b != 0) {
return None; // address must be zero-padded in its 32-byte word
}
let recipient = Address::from_slice(&addr_word[12..]);
let amount = U256::from_be_slice(amount_word);
Some((recipient, amount))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, the copilot suggestion is a little more explicit... but 🤷 , I don't think it matters

Comment thread tvc-cosign/src/main.rs
Comment on lines +60 to +76
while let Some(flag) = iter.next() {
match flag.as_str() {
"--organization-id" => args.organization_id = iter.next(),
"--rules-path" => args.rules_path = iter.next(),
"--port" => {
if let Some(v) = iter.next() {
match v.parse() {
Ok(p) => args.port = p,
Err(_) => {
eprintln!("args: WARNING invalid --port {v:?}, using {DEFAULT_PORT}")
}
}
}
}
other => eprintln!("args: WARNING ignoring unknown argument {other:?}"),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with copilot here, but you should make sure that the value doesn't start with -- as a minimal check

Comment thread tvc-cosign/src/tx.rs
Comment on lines +116 to +118
data.extend_from_slice(&[0u8; 12]);
data.extend_from_slice(recipient.as_slice()); // right-padded to 32 bytes
data.extend_from_slice(&U256::from(amount).to_be_bytes::<32>());
#[serde(rename_all = "camelCase")]
struct SignTransactionActivity {
#[serde(rename = "type")]
activity_type: &'static str,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the off chance you didn't know about this, you can do this:

Suggested change
activity_type: &'static str,
r#type: &'static str,

(though I prefer what you did)

Comment thread tvc-cosign/src/config.rs
Comment on lines +39 to +40
let organization_id = cli_organization_id
.map(str::to_string)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, best practice is to pass in owned types if you need owned types

Comment thread tvc-cosign/src/main.rs
}
}
}
other => eprintln!("args: WARNING ignoring unknown argument {other:?}"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe do a small help text too?

Comment thread tvc-cosign/src/main.rs
Comment on lines +60 to +76
while let Some(flag) = iter.next() {
match flag.as_str() {
"--organization-id" => args.organization_id = iter.next(),
"--rules-path" => args.rules_path = iter.next(),
"--port" => {
if let Some(v) = iter.next() {
match v.parse() {
Ok(p) => args.port = p,
Err(_) => {
eprintln!("args: WARNING invalid --port {v:?}, using {DEFAULT_PORT}")
}
}
}
}
other => eprintln!("args: WARNING ignoring unknown argument {other:?}"),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with copilot here, but you should make sure that the value doesn't start with -- as a minimal check

Comment thread tvc-cosign/src/main.rs
Comment on lines +96 to +103
println!(
"keys: programmatic pubkey = {}",
keys.programmatic.public_key_hex()
);
println!(
"keys: admin pubkey = {}",
keys.admin.public_key_hex()
);

@richardpringle richardpringle Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
println!(
"keys: programmatic pubkey = {}",
keys.programmatic.public_key_hex()
);
println!(
"keys: admin pubkey = {}",
keys.admin.public_key_hex()
);
println!(
r#"keys:
programmatic pubkey = {}
admin pubkey = {}"#,
keys.programmatic.public_key_hex(),
keys.admin.public_key_hex(),
);

@richardpringle richardpringle left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Top notch 🥇

The only comment that I'm leaving in this review that I do feel strongly about is the mermaid diagram comment. The rest are even less minor than nits, take them or leave them... probably leave most of them 😅

Comment thread tvc-cosign/src/tx.rs
Comment on lines +48 to +58
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::Empty => write!(f, "empty transaction"),
ParseError::UnsupportedType(b) => {
write!(f, "unsupported transaction type byte {b:#04x}")
}
ParseError::Rlp(e) => write!(f, "malformed RLP: {e}"),
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use thiserror for this instead, I don't think it'll hurt compiles times that much and makes the code surface area a little smaller for someone reading

Comment thread tvc-cosign/README.md
Turnkey policies evaluate the stamping API user:
• TVC programmatic → policy ALLOW → COMPLETED
• TVC admin → require human consensus → CONSENSUS_NEEDED
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would look nicer as a mermaid diagram

Comment thread tvc-cosign/README.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just have the one comment about the mermaid diagram, otherwise, 🥇

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread tvc-cosign/src/rules.rs
Comment on lines +106 to +116
fn decode_transfer_args(input: &[u8]) -> Option<(Address, U256)> {
// 4 (selector) + 32 (address) + 32 (amount)
let args = input.get(4..68)?;
let (addr_word, amount_word) = args.split_at(32);
if addr_word[..12].iter().any(|&b| b != 0) {
return None; // address must be zero-padded in its 32-byte word
}
let recipient = Address::from_slice(&addr_word[12..]);
let amount = U256::from_be_slice(amount_word);
Some((recipient, amount))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, the copilot suggestion is a little more explicit... but 🤷 , I don't think it matters

Comment thread tvc-cosign/src/rules.rs
Comment on lines +52 to +54
let raw: RawRuleset = toml::from_str(EMBEDDED_RULES_TOML)
.map_err(|e| format!("parse embedded rules.toml: {e}"))?;
raw.into_ruleset()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a LazyLock here so that you're not deserializing every time.

Comment thread tvc-cosign/src/keys.rs
/// sub-derived with the exact [`QOS_SIGN_PATH`]/[`QOS_ENCRYPT_PATH`] construction
/// QOS uses. Each of a TVC's replicas boots its own ephemeral key.
pub struct EphemeralKey {
signing_key: SigningKey,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using Zeroize here just to promote best practices with keys?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants