feat!: sign and verify data as first-class ceremony steps - #166
Open
lomigmegard wants to merge 1 commit into
Open
feat!: sign and verify data as first-class ceremony steps#166lomigmegard wants to merge 1 commit into
lomigmegard wants to merge 1 commit into
Conversation
Signing arbitrary data needed a smart card (`piv_sign`), and verifying a signature happened only as a side effect of `issue_certificate` checking a CSR. A ceremony that wanted to sign a release manifest with a software or HSM key, or to check a signature it was handed, had no step for it. This adds `sign_data` and `verify_signature`, plus the groundwork they turned out to need. ## Signature algorithms gain DSL names `SignAlgorithm` was the only algorithm enum without `Display`/`FromStr`, so its DSL spelling came from serde's snake_case derive and a hand-rolled table in `piv_sign`. That put `algorithm: ecdsa_sha256` next to `algorithm: ECDSA-P256` in the same ceremony file. It now spells the same way as `KeyAlgorithm` and `WrapAlgorithm`, with a wire-contract test pinning the eight strings, and the ML-DSA parameter sets gain a DSL spelling for the first time. `SignAlgorithm::accepts_key` replaces the compatibility guard each backend arm reinvented. `key_algorithm()` could not serve as that predicate: it is lossy for RSA, mapping both schemes to `Rsa2048`, so an RSA-4096 key failed its own equality check. ## Software verification consolidates onto OpenSSL It ran on three mechanisms at once: the `rsa` crate for RSA CSRs, `p256` for ECDSA, and OpenSSL for ML-DSA, while OpenSSL produced all three signatures. - `rite_openssl::verify_signature` covers every family, and `sign`/`verify` share its primitives, so the backend has one signing path and one verification path rather than a match arm each. - `rite-stdlib`'s `signatures` module is the seam actions go through, so swapping the provider behind it touches one file. - `rsa` and `p256` leave the workspace. They remain only as transitive dependencies of `yubikey`, absent from default and musl builds, so the RUSTSEC-2023-0071 rationale is rewritten around where the crate now actually appears. `pki` and `crypto` gain `openssl`, which they verify through. Verification is told which algorithm to use rather than inferring one from the key, so a CSR naming ECDSA while carrying an RSA key is refused instead of quietly verified as RSA. The CSR allowlist stays, folded into the same table as the identifiers Rite emits, so Rite cannot generate a CSR its own `issue_certificate` refuses. Ed25519 and ECDSA-P384 were declared in both algorithm enums and implemented nowhere: `generate_keypair` accepted them and the OpenSSL backend then refused. Both now work end to end, with their RFC 5480 and RFC 8410 signature identifiers. ## The actions `sign_data` is the generic counterpart to `piv_sign`: any backend implementing `SignBackend`, key taken by artifact reference rather than device slot. The algorithm follows from the key, with an `algorithm:` override for the one case where a key does not determine it, an RSA key choosing between PKCS#1 v1.5 and PSS. An override the key cannot perform is refused up front rather than at the backend. `verify_signature` needs no backend, which is the point. Verification takes only a public key, so the step works on evidence the ceremony did not produce: a signature made on a card that will never expose its key, or one that arrived with a document from outside. Naming a `backend:` delegates the check instead; doing so with a key that backend does not hold is an error rather than a silent fall back to software, which would misreport who checked the evidence. A failed check fails the step. That made `ActionType::requires_backend()` too coarse, since an action can also *accept* a backend without needing one. It becomes `backend_usage() -> BackendUsage` with three states, so `rite check` no longer warns that a delegated verification "does not use a backend". `examples/showcase/sign_and_verify.rite.yaml` signs and verifies a manifest, contrasting the two step shapes. Its assertion is that verification succeeded, never that the signature equals fixed bytes, since ML-DSA signing is hedged. The negative cases are integration tests. ## Also - `docs/development/cryptographic-dependencies.md` states which library performs which class of work, so a contributor adding an algorithm has a rule to follow rather than a guess. The OpenSSL 3.5 requirement moves into CONTRIBUTING.md, where someone hits it. - `ActionType::ALL` ties the editor's action catalogue to the enum. Nothing did before, so a new action was invisible in completion while working perfectly at run time; the sync test caught `gather_entropy`, missing since it shipped. BREAKING CHANGE: `piv_sign` takes `ECDSA-SHA256`, `ECDSA-SHA384`, and `RSA-PKCS1-SHA256`; the snake_case spellings are no longer accepted. `rite_openssl::verify_ml_dsa_signature` is replaced by `verify_signature`, which takes a `SignAlgorithm`. `ActionType::requires_backend` is replaced by `ActionType::backend_usage`, returning `BackendUsage`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Signing arbitrary data needed a smart card (
piv_sign), and verifying a signature happened only as a side effect ofissue_certificatechecking a CSR. A ceremony that wanted to sign a release manifest with a software or HSM key, or to check a signature it was handed, had no step for it.This adds
sign_dataandverify_signature, plus the groundwork they turned out to need.Signature algorithms gain DSL names
SignAlgorithmwas the only algorithm enum withoutDisplay/FromStr, so its DSL spelling came from serde's snake_case derive and a hand-rolled table inpiv_sign. That putalgorithm: ecdsa_sha256next toalgorithm: ECDSA-P256in the same ceremony file. It now spells the same way asKeyAlgorithmandWrapAlgorithm, with a wire-contract test pinning the eight strings, and the ML-DSA parameter sets gain a DSL spelling for the first time.SignAlgorithm::accepts_keyreplaces the compatibility guard each backend arm reinvented.key_algorithm()could not serve as that predicate: it is lossy for RSA, mapping both schemes toRsa2048, so an RSA-4096 key failed its own equality check.Software verification consolidates onto OpenSSL
It ran on three mechanisms at once: the
rsacrate for RSA CSRs,p256for ECDSA, and OpenSSL for ML-DSA, while OpenSSL produced all three signatures.rite_openssl::verify_signaturecovers every family, andsign/verifyshare its primitives, so the backend has one signing path and one verification path rather than a match arm each.rite-stdlib'ssignaturesmodule is the seam actions go through, so swapping the provider behind it touches one file.rsaandp256leave the workspace. They remain only as transitive dependencies ofyubikey, absent from default and musl builds, so the RUSTSEC-2023-0071 rationale is rewritten around where the crate now actually appears.pkiandcryptogainopenssl, which they verify through.Verification is told which algorithm to use rather than inferring one from the key, so a CSR naming ECDSA while carrying an RSA key is refused instead of quietly verified as RSA. The CSR allowlist stays, folded into the same table as the identifiers Rite emits, so Rite cannot generate a CSR its own
issue_certificaterefuses.Ed25519 and ECDSA-P384 were declared in both algorithm enums and implemented nowhere:
generate_keypairaccepted them and the OpenSSL backend then refused. Both now work end to end, with their RFC 5480 and RFC 8410 signature identifiers.The actions
sign_datais the generic counterpart topiv_sign: any backend implementingSignBackend, key taken by artifact reference rather than device slot. The algorithm follows from the key, with analgorithm:override for the one case where a key does not determine it, an RSA key choosing between PKCS#1 v1.5 and PSS. An override the key cannot perform is refused up front rather than at the backend.verify_signatureneeds no backend, which is the point. Verification takes only a public key, so the step works on evidence the ceremony did not produce: a signature made on a card that will never expose its key, or one that arrived with a document from outside. Naming abackend:delegates the check instead; doing so with a key that backend does not hold is an error rather than a silent fall back to software, which would misreport who checked the evidence. A failed check fails the step.That made
ActionType::requires_backend()too coarse, since an action can also accept a backend without needing one. It becomesbackend_usage() -> BackendUsagewith three states, sorite checkno longer warns that a delegated verification "does not use a backend".examples/showcase/sign_and_verify.rite.yamlsigns and verifies a manifest, contrasting the two step shapes. Its assertion is that verification succeeded, never that the signature equals fixed bytes, since ML-DSA signing is hedged. The negative cases are integration tests.Also
docs/development/cryptographic-dependencies.mdstates which library performs which class of work, so a contributor adding an algorithm has a rule to follow rather than a guess. The OpenSSL 3.5 requirement moves into CONTRIBUTING.md, where someone hits it.ActionType::ALLties the editor's action catalogue to the enum. Nothing did before, so a new action was invisible in completion while working perfectly at run time; the sync test caughtgather_entropy, missing since it shipped.BREAKING CHANGE:
piv_signtakesECDSA-SHA256,ECDSA-SHA384, andRSA-PKCS1-SHA256; the snake_case spellings are no longer accepted.rite_openssl::verify_ml_dsa_signatureis replaced byverify_signature, which takes aSignAlgorithm.ActionType::requires_backendis replaced byActionType::backend_usage, returningBackendUsage.