CBOR Object Signing and Encryption (COSE, RFC 9052) and CBOR Web Token
(CWT, RFC 8392) for Rust, built on cbor2 and its
#[derive(cbor2::Cbor)] macro.
cose2 models the COSE wire structures and CWT claims and leaves the
cryptography to you: signing, verification, MAC and content encryption are
supplied through the Signer, Verifier, Macer and Encryptor
traits, so the default build carries no cryptographic dependencies.
Pick any crypto library (e.g. ed25519-dalek, p256, aes-gcm, hmac) and
implement the relevant trait.
Enable the optional crypto-ring feature (or the aggregate crypto feature)
to use the built-in crypto module backed by [ring], or crypto-aws-lc-rs
to back it with aws-lc-rs instead. Both backends expose the same providers
and implement Ed25519, ES256, ES384, RS256/384/512, PS256/384/512, HMAC
256/64, HMAC 256/256, HMAC 384/384, HMAC 512/512, A128GCM, A256GCM and
ChaCha20/Poly1305. Algorithms outside the backend's support are rejected at
provider construction. When both backend features are enabled, crypto-ring
takes precedence.
Alternatively, enable crypto-ed25519-dalek for a standalone Ed25519
Signer/Verifier (module ed25519) backed by ed25519-dalek, or
crypto-aes-gcm for an AES-GCM (A128GCM/A256GCM) Encryptor (module
aes_gcm) backed by aes-gcm — both without a ring/aws-lc-rs
dependency.
- Typescript version: https://github.com/ldclabs/cose-ts
- Golang version: https://github.com/ldclabs/cose
- Messages —
COSE_Sign1,COSE_Sign,COSE_Mac0,COSE_Mac,COSE_Encrypt0,COSE_Encrypt,COSE_recipient. - Keys —
COSE_Keyobjects (Key) and non-empty key sets (KeySet) with typed accessors forkty,kid,alg,key_opsandBase IV. - Headers — protected/unprotected
Headermaps with integer or text labels and the full IANA parameter registry underiana. - CWT — typed
cwt::Claims, a label-keyedcwt::ClaimsMap, and acwt::Validatorfor expiry, not-before, issued-at, issuer and audience. - SD-CWT — the workspace also includes
sd-cwt, a companion crate for selective-disclosure claims,simple(59), tag-60 redacted elements,sd_claims, and Holder/Verifier restoration. - KDF context —
KdfContext,PartyInfo,SuppPubInfo(RFC 9053 §5.2). - Tagging — tagged or untagged messages, with optional CWT and
self-described CBOR prefixes handled transparently. Newly encoded COSE
messages and CWT claims use their registered CBOR tags through
#[derive(cbor2::Cbor)]; useto_untagged_vecwhen a peer expects the untagged wire body. - Optional crypto —
crypto-ringorcrypto-aws-lc-rsprovidesRingSigner,RingVerifier,RingMacerandRingEncryptorimplementations behind a feature flag, backed byringoraws-lc-rsrespectively.crypto-ed25519-dalekadds anEd25519Signer/Ed25519Verifierbacked byed25519-dalek, andcrypto-aes-gcmadds anAesGcmEncryptorbacked byaes-gcm.
use cose2::{iana, Sign1Message, Signer, Verifier, Error};
// Plug in your own crypto. (This toy "signer" is for illustration only.)
struct MySigner;
impl Signer for MySigner {
fn alg(&self) -> Option<cose2::Label> { Some(iana::AlgorithmEdDSA.into()) }
fn kid(&self) -> Option<&[u8]> { Some(b"key-1") }
fn sign(&self, data: &[u8]) -> Result<Vec<u8>, Error> { /* ... */ Ok(data.to_vec()) }
}
struct MyVerifier;
impl Verifier for MyVerifier {
fn alg(&self) -> Option<cose2::Label> { Some(iana::AlgorithmEdDSA.into()) }
fn verify(&self, data: &[u8], sig: &[u8]) -> Result<(), Error> {
if sig == data { Ok(()) } else { Err(Error::verify("bad signature")) }
}
}
let mut msg = Sign1Message::new(Some(b"This is the content".to_vec()));
let encoded = msg.sign_and_encode(&MySigner, None)?;
let verified = Sign1Message::verify_and_decode(&MyVerifier, &encoded, None)?;
assert_eq!(verified.payload.as_deref(), Some(&b"This is the content"[..]));
# Ok::<(), cose2::Error>(())| Task | Start with | Notes |
|---|---|---|
| Sign one embedded payload | Sign1Message::sign_and_encode / Sign1Message::verify_and_decode |
The common COSE_Sign1 flow. |
| Sign one detached payload | Sign1Message::sign_detached_and_encode / Sign1Message::verify_detached_and_decode |
The wire payload is nil; carry the payload out of band. |
| Sign with multiple signers | SignMessage |
Each signer has its own Signature. |
| MAC one payload | Mac0Message::compute_and_encode / Mac0Message::verify_and_decode |
Symmetric authentication without recipients. |
| MAC with recipients | MacMessage |
Recipient-layer cryptography stays application-owned. |
| Encrypt one payload | Encrypt0Message::encrypt_and_encode / Encrypt0Message::decrypt_and_decode |
Requires a full IV, or Partial IV plus Encryptor::base_iv. |
| Encrypt with recipients | EncryptMessage |
cose2 validates recipient structure but does not wrap or agree the CEK. |
| Async/remote signing | prepare_signature / prepare_signatures, then set_signature / set_signatures |
Sign the returned Sig_structure bytes outside the synchronous trait. |
| Async/remote MAC | prepare_tag / prepare_detached_tag, then set_tag |
MAC the returned MAC_structure bytes outside the synchronous trait. |
| Async/remote encryption | prepare_encryption then set_ciphertext |
Encrypt with the returned nonce and Enc_structure AAD. |
| Work with CWT claims | cwt::Claims, cwt::ClaimsMap, cwt::Validator |
Claims::extra preserves custom claims; use ClaimsMap for map-only use. |
| Work with SD-CWT claims | sd-cwt companion crate |
Issue tag-58/62 pre-issuance claims, write sd_claims, and restore Holder/Verifier presentations. |
| Work with COSE keys | Key, KeySet |
KeySet::lookup(kid) returns all matches because kid is not unique. |
These examples are intended as copy/paste starting points for humans and AI agents:
cargo run --example custom_crypto_traits
cargo run --example detached_payload
cargo run --example cwt_sign1
cargo run --example sign1_ring --features crypto-ring
cargo run --example mac0_ring --features crypto-ring
cargo run --example encrypt0_ring --features crypto-ring
cargo run -p sd-cwt --example basicFor a compact decision checklist — including a crypto-ring algorithm
recipe table mapping each algorithm to its required COSE key
parameters — see the Agent guide for cose2. Agents modifying this
crate's source should start from AGENTS.md.
For selective-disclosure credentials, start with the
sd-cwt README and its runnable basic example. The
example shows pre-issuance redaction requests, issuer signing with
Sign1Message, Holder validation, and Verifier presentation with a disclosure
subset.
- Header, key and claim maps share one ordered type,
CoseMap, keyed byLabel(int/tstr). Values are [cbor2::Value]. Headeris aCoseMapnewtype with typed accessors for common COSE parameters (alg,crit,kid,iv,Partial IV) while still dereferencing to the underlying map for custom labels. Message and recipient decoding rejects malformedcritvalues and protected/unprotected bucket label collisions.Keyrequireskty;KeySetencodes/decodes as a non-empty COSE_KeySet andlookupreturns all keys with a matchingkid, since COSE key identifiers are not unique.algvalues in crypto traits areOption<Label>, so both registered integer algorithms and private text-string algorithms are representable.- The default build has no crypto dependency. The
crypto-ringandcrypto-aws-lc-rsfeatures offer ready-to-use providers for the algorithms listed above, while unsupported algorithms return an explicit error instead of falling back to a mismatched primitive. - The protected header is captured as raw bytes on decode and reused verbatim
in the
Sig_structure/MAC_structure/Enc_structure, so signatures made with non-canonical encodings still verify. - Top-level COSE message wire types use named Rust structs with
#[cbor(tag = ..., array)], preserving the COSE array wire shape while declaring their IANA CBOR tags. CWT claims declare their IANA CBOR tag with#[cbor(tag = 61)]. Decoders still accept untagged COSE messages and untagged claim maps for compatibility, and tagged types provideto_untagged_vecfor tagless canonical encoding. - Detached payloads are explicit: use
sign_detached*,compute_detached*,verify_detached*, orverify_detached_and_decode. - Detached ciphertext is explicit: use
encrypt_detached*anddecrypt_detached*for COSE_Encrypt/COSE_Encrypt0 messages whose ciphertext field is encoded asnil. Recipientvalidates RFC 9052 recipient-layer structure for registered direct, key-wrap, key-transport and key-agreement algorithms. Actual key wrapping/agreement cryptography remains delegated to application code.- Encryption requires an explicit plaintext payload; use
Some(Vec::new())for an empty plaintext. - Newly built protected headers and keys serialize with canonical (deterministic) CBOR (RFC 8949 §4.2.1).
- Nonces are taken from the unprotected
IV, or derived fromPartial IVby XORing the left-padded partial value with [Encryptor::base_iv]. This crate generates no randomness.
cargo test runs the unit, integration and doc tests, including a byte-exact
reproduction of the RFC 9052 Appendix C.4.1 COSE_Encrypt0 vector.
Coverage measured with cargo llvm-cov is 100% of lines and functions;
the remaining uncovered regions are unreachable error-propagation arms on
serialization that cannot fail.
Licensed under the MIT License.