Skip to content

feat(cms)!: encrypt content to an EC public key with ECDH key agreement - #291

Merged
lomigmegard merged 2 commits into
mainfrom
feat/cms-kari-recipient
Nov 1, 2026
Merged

lomigmegard merged 2 commits into
mainfrom
feat/cms-kari-recipient

Conversation

@lomigmegard

@lomigmegard lomigmegard commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

encrypt_data seals content to a P-256 or P-384 public key through a CMS KeyAgreeRecipientInfo: ECDH with an ephemeral key drawn from the step's backend, the X9.63 KDF over SHA-256 or SHA-384 (RFC 5753), and AES-256 key wrap. decrypt_data opens it, and opens one sealed by openssl cms with ecdh_kdf_md. Both directions are checked against the OpenSSL CLI.

Opening goes through a new KeyAgreementBackend trait: the backend agrees the secret with the key it holds and returns it as an AgreedSecret, and the host runs the KDF and the unwrap. The OpenSSL backend implements it; the PIV backend is a follow-up.

Breaking:

  • an EC key that opens what is sealed to it needs usages: [derive] rather than unwrap, in decrypt_data and unwrap_key;
  • a compressed EC point is refused wherever a public key is read.

Closes #202

encrypt_data reads a P-256 or P-384 key as `recipient:`. The container
carries a KeyAgreeRecipientInfo: an ephemeral key on the recipient's
curve, the X9.63 KDF over SHA-256 for P-256 and SHA-384 for P-384
(RFC 5753), and the content-encryption key under AES-256 key wrap. The
recipient is named by its fingerprint, as a ktri's is. The ephemeral
private key is drawn from the step's backend, as the content key is,
since whoever knows it derives the key-encryption key.

Opening is split where a card splits it. A new KeyAgreementBackend
trait runs the Diffie-Hellman agreement with the key the backend holds
and returns an AgreedSecret, and the host derives the wrap key and
unwraps, so the trait stays the same whatever container format runs
its KDF over the secret. Its doc is the contract a PIV, PKCS#11 or TPM
backend implements against. The OpenSSL backend implements it.

An EC key opens by key agreement, which PKCS#11 permits under
CKA_DERIVE, so agree and unwrap_key under an EC key require the derive
usage. KeyUsages::to_open names the usage per algorithm, and a refused
usage says which one the step needs.

cms::Envelope carries a RecipientInfo enum, one variant per recipient
form with that form's fields, in place of a protection, an identifier
and a protected key that had to agree. A provider-defined protection
cannot be put in a container, and decrypt_data matches on the form to
know what opening takes.

PublicKeyDer refuses an EC key whose point is compressed: the same key
under another encoding would have another fingerprint, and converting
it takes curve arithmetic rite-sdk does not carry. A container whose
ephemeral point is compressed is refused when opened, by name.

Both directions are checked against openssl cms with ecdh_kdf_md, and
the CLI's default SHA-1 derivation is refused by name. A kari naming
several recipient keys is described by its first and refused when
opened.

BREAKING CHANGE: an EC key that opens what wrap_key or encrypt_data
sealed to it needs `usages: [derive]`; `unwrap` does not permit it.
PublicKeyDer refuses compressed EC points, so a key, certificate or
CSR carrying one is refused wherever it is read.

Closes #202
encrypt_and_decrypt seals the runbook a second time, to the public key of a P-256 custodian key granted the derive usage, opens it under that key, and compares the digests. rite verify checks the container against the recorded recipient. The wrap_and_unwrap comment on EC transport keys names the usages such a key needs.
@lomigmegard
lomigmegard merged commit 4476b27 into main Nov 1, 2026
13 checks passed
@lomigmegard
lomigmegard deleted the feat/cms-kari-recipient branch November 1, 2026 12:28
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.

encrypt_data cannot address content to a public key

1 participant