feat(signing): sign with AWS KMS-held party keys - #303
Conversation
A KMS-backed participant creates the party's Daml key inside AWS KMS, where it cannot be exported, so the export-and-sign path fails and the party cannot deploy its governance contract. Canton offers no API that signs a ledger transaction with a vault key: party signatures must come from the caller. Add AwsKmsSigner, a TransactionSigner backend that calls the AWS KMS Sign API with the key id the vault reports for the key. select_signer now routes keys that carry a kms_key_id to it; exportable vault keys keep the export path unchanged. The signature semantics follow Canton's verify path exactly, confirmed against source: the prepared-transaction hash is a raw 32-byte digest signed as a message (KMS MessageType=RAW, ECDSA_SHA_256 — Canton verifies with SHA256withECDSA, which hashes the input again), and the signature stays in the DER encoding KMS returns (the only format Canton accepts for ECDSA). Each signature is verified locally against the registered public key before submission, so a wrong key or wrong semantics fails here with context instead of at ExecuteSubmission. The operator grants decman's role kms:Sign on the party keys; the new docs/KMS_SIGNING.md covers the setup and failure modes. Refs #264
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
The "rustls" feature on aws-config/aws-sdk-kms selects the legacy connector built on rustls 0.21, which pins rustls-webpki 0.101 and its three open advisories (RUSTSEC-2026-0098/0099/0104). Switch to "default-https-client", the current connector on rustls 0.23; the vulnerable crates leave the lockfile entirely. cargo-audit also flags rkyv 0.7 (RUSTSEC-2026-0235), which enters the lockfile only through rust_decimal's disabled optional feature and is never compiled. Ignore it in .cargo/audit.toml with the reasoning, following the existing convention there.
Apply the persona-review round: - Drop the P-384 arm. No P-384 party key can exist (KMS nodes generate the P-256 default), the arm skipped the local pre-submission verification, and its Canton format rule was never verified. The backend now rejects anything but EC-P256 with a clear error, and sign() reads straight through with no conditional path. - Split the key-policy and IAM-policy snippets in docs/KMS_SIGNING.md. One snippet served both, and following the IAM route with Resource:"*" would grant signing with every key in the account, including the namespace key. The IAM variant now names explicit key ARNs and warns against the wildcard; a concrete ListMyKeys command covers key discovery. - Update the signing module doc that still called vault export the only backend. - Restore alphabetical order in the workspace Cargo.toml and drop the redundant SigningError::Other wrappers in favor of the #[from] conversion.
[personas] Review summary — Gate B, round 1 (fixes in bc054c3)Three persona lenses reviewed this PR: security, PM-against-spec, pragmatic. Ten findings. Six fixed in
By category: security 4 (1 blocking, fixed; 1 removed; 1 filed; 1 no-change) · functionality 3 (all fixed) · cosmetic 3 (all fixed). Pre-existing behavior outside this PR, filed: #305 ( The PM lens confirms requirement coverage end-to-end: the execute step copies signatures verbatim (no Ed25519 assumption), JCE parties are untouched, and the docs match the code. The live devnet run (IAM grant + governance-core deployment) remains the close condition for #264 — green CI does not stand in for it. Next: Copilot review loop (Gate C) runs on this improved code; a final targeted persona re-check follows if Copilot changes anything. The merge decision stays with the human gate. |
There was a problem hiding this comment.
Pull request overview
Adds an AWS KMS–backed signing backend to decman so parties whose Daml keys are non-exportable (KMS-backed participants) can still sign interactive-submission prepared hashes without extracting private material from the participant vault.
Changes:
- Introduces
AwsKmsSignerimplementingTransactionSigner, calling AWS KMSSignand locally verifying signatures withp256before submission. - Updates signer selection (
select_signer) to route to KMS signing when the party key metadata includeskms_key_id. - Adds AWS SDK +
p256dependencies, plus operator documentation and a Cargo Audit ignore entry for a lockfile-only advisory.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/KMS_SIGNING.md | New operator guide for IAM + runtime setup and troubleshooting for KMS-backed party signing. |
| crates/decman/src/signing/signer.rs | Routes signing backend selection to AWS KMS when kms_key_id is present. |
| crates/decman/src/signing/mod.rs | Registers the new aws_kms signing module and updates module-level docs. |
| crates/decman/src/signing/error.rs | Adds a typed SigningError::Kms variant for KMS signing failures. |
| crates/decman/src/signing/aws_kms.rs | New AWS KMS signing backend implementation + key-spec→algorithm mapping tests. |
| crates/decman/Cargo.toml | Adds aws-config, aws-sdk-kms, and p256 to the decman crate dependencies. |
| Cargo.toml | Adds workspace dependency definitions for AWS SDK crates and p256 with selected features. |
| Cargo.lock | Locks new transitive dependencies from the AWS SDK and RustCrypto additions. |
| .cargo/audit.toml | Ignores a specific RustSec advisory deemed unreachable due to disabled features. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The guide referred to the field as kms_key_id in one place and kmsKeyId in another. State once that grpcurl's JSON output renders the proto field kms_key_id as kmsKeyId, so operators can match the two.
[copilot-loop] Review summary — Gate C, round 1 (converged)Copilot reviewed the post-persona code (
By category: cosmetic 1 (fixed). No security or functionality findings. The round converged in one pass. Copilot changed no code itself, and the fix is one documentation sentence, so the terminal persona re-check has no code delta to review. Gate B and Gate C are both complete; the merge decision stays with the human gate. The live devnet validation (IAM grant + governance-core deployment) remains the close condition for #264. |
# Conflicts: # .cargo/audit.toml
|
Went through the whole diff plus the call site in Checked a couple of the claims rather than trusting them. aws-config feature set drops SSO
IRSA is unaffected, web identity is core and One line to add Region
Cheap version: bail in Tests cover the easy halfThe two tests are on Smaller stuffThe local verify pins the KMS call to the semantics we assume, but it can't check that assumption against Canton. Both sides hash once, so if the Canton reading is wrong the signature still passes locally and gets rejected at
CI note in the PR body is stale. The rkyv ignore went into main on its own in #308 and this branch just merged it in, so it isn't in the diff anymore. Sign calls are sequential, one round trip per hash. Fine at current counts, just noting it. |
What
Adds
AwsKmsSigner, the first non-export signing backend behind theTransactionSignertrait from #291. When the party's Daml key carries akms_key_id(KMS-backed participants),select_signerroutes to it and decman signs the interactive-submission prepared hash through the AWS KMSSignAPI. Exportable vault keys keep the export path unchanged.Why
On a
crypto.provider = kmsparticipant the party's Daml key is created inside AWS KMS and cannot be exported. Canton has no API that signs a ledger transaction with a vault key — party signatures must come from the caller. So the party could be created (#268) but could not deploy its governance-core contract. This closes that gap: decman asks the KMS to sign, and the key never leaves it.Design decision (pilot call): the party key stays in the node's KMS, and the operator grants decman
kms:Sign— security posture is worth the one-time IAM setup. A decman-held local-key backend remains possible later behind the same trait.Signature semantics — verified against Canton source, not assumed
hash.unwrap, no multihash prefix); Execute recomputes and verifies those exact bytes.SHA256withECDSA(message semantics — the verifier hashes the input again). The KMS call is thereforeMessageType=RAW+ECDSA_SHA_256. PassingDIGESTwould silently skip one hash pass and fail.supportedSignatureFormats = {Der}forEcDsaSha256); AWS KMS returns DER, passed through unchanged.signed_by= the fingerprint of the party's registered signing key (Protocol usage), which fix: support KMS-backed nodes in party key generation (spec + fingerprint) #268 made correct for P-256.Each signature is also verified locally (via
p256) against the registered public key before submission — wrong key or wrong semantics fails fast with context instead of an opaqueExecuteSubmissionrejection.Operator setup
docs/KMS_SIGNING.md(new) documents the one-time setup: AWS credentials for decman (IRSA on EKS) andkms:Signon the party keys, plus the failure-mode table. Key discovery, algorithm selection, and format are automatic.New dependencies
aws-config/aws-sdk-kms— the official AWS SDK; needed to call KMSSign. rustls, no default features.p256(RustCrypto) — parses the SPKI public key and verifies each signature locally before submission.CI note
.cargo/audit.tomlgains an ignore forRUSTSEC-2026-0235(rkyv): the crate enters the lockfile only through rust_decimal's disabled optional feature and is never compiled; graph-aware cargo-deny agrees. This keeps the lockfile-scanning Cargo Audit gate green and is unrelated to KMS.How tested
Signcall itself cannot be unit-tested without AWS; live validation is the next step: grant decman-6's rolekms:Signon the devnet party keys and deploy governance-core with thetest-kms-2party. The local verification step means a semantics error surfaces in decman logs, not as a ledger rejection.Scope notes
kms_key_id; its Sign call fails with a clear KMS error until a dedicated backend exists (their client API is still an open question with MPCH).kms_key_id, same export path as before.Refs #264