bitwarden_crypto/cose/thumbprint.rs
1//! [RFC 9679](https://www.rfc-editor.org/rfc/rfc9679) COSE Key Thumbprints.
2//!
3//! A COSE Key Thumbprint is a deterministic hash of a COSE_Key — the COSE analogue of the JWK
4//! thumbprint ([RFC 7638](https://www.rfc-editor.org/rfc/rfc7638)). It is computed by:
5//!
6//! 1. Collecting **only** the parameters required for that key type (e.g. for an OKP key `kty`,
7//! `crv`, and `x`), excluding `kid`, `key_ops`, and any private material.
8//! 2. Encoding those parameters as a CBOR map using the deterministic encoding of [RFC 8949 §4.2.1](https://www.rfc-editor.org/rfc/rfc8949#section-4.2.1)
9//! (definite-length map, shortest-form integers, map keys sorted bytewise-lexicographically by
10//! their encoded bytes).
11//! 3. Hashing the result. This implementation only supports the RFC 9679 default hash, SHA-256.
12//!
13//! Because the thumbprint is computed over public material for asymmetric keys, a private key and
14//! its corresponding public key produce the same thumbprint. The same applies for signature key
15//! pairs / their verification key.
16
17use ciborium::{Value, value::Integer};
18use sha2::{Digest, Sha256};
19
20/// An [RFC 9679](https://www.rfc-editor.org/rfc/rfc9679) COSE Key Thumbprint.
21///
22/// ```no_run
23/// use bitwarden_crypto::{CoseKeyThumbprintExt, SignatureAlgorithm, SigningKey};
24///
25/// let key = SigningKey::make(SignatureAlgorithm::Ed25519);
26/// let thumbprint = key.thumbprint().expect("Ed25519 keys are always COSE-representable");
27/// println!("{}", thumbprint); // e.g. "SHA256:50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c"
28/// ```
29#[derive(Clone, PartialEq, Eq)]
30pub struct CoseKeyThumbprint([u8; 32]);
31
32impl CoseKeyThumbprint {
33 /// The raw 32-byte SHA-256 digest.
34 pub fn as_bytes(&self) -> &[u8; 32] {
35 &self.0
36 }
37
38 /// The thumbprint as a lowercase hex string.
39 pub fn to_hex(&self) -> String {
40 hex::encode(self.0)
41 }
42}
43
44impl std::fmt::Display for CoseKeyThumbprint {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(f, "SHA256:{}", self.to_hex())
47 }
48}
49
50impl std::fmt::Debug for CoseKeyThumbprint {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "CoseKeyThumbprint({})", self.to_hex())
53 }
54}
55
56/// Computes the [RFC 9679](https://www.rfc-editor.org/rfc/rfc9679) COSE Key Thumbprint of a key.
57pub trait CoseKeyThumbprintExt {
58 /// Returns the SHA-256 COSE Key Thumbprint of this key.
59 fn thumbprint(&self) -> Result<CoseKeyThumbprint, crate::CryptoError>;
60}
61
62/// Builds the RFC 9679 thumbprint from a key type's required parameters.
63///
64/// `params` is the list of `(label, value)` pairs of the required parameters (order does not
65/// matter; this function sorts them into the RFC 8949 §4.2.1 deterministic order). The pairs are
66/// encoded as a canonical CBOR map and hashed with SHA-256.
67pub(crate) fn thumbprint_from_required_params(mut params: Vec<(i64, Value)>) -> CoseKeyThumbprint {
68 // RFC 8949 §4.2.1: map keys are sorted by the bytewise lexicographic order of their encoded
69 // representation. Encoding each label to CBOR and comparing the bytes is exactly this rule.
70 params.sort_by_key(|(label, _)| encoded_label(*label));
71
72 let map = Value::Map(
73 params
74 .into_iter()
75 .map(|(label, value)| (Value::Integer(Integer::from(label)), value))
76 .collect(),
77 );
78
79 let mut buf = Vec::new();
80 ciborium::into_writer(&map, &mut buf)
81 .expect("CBOR serialization of a COSE key parameter map cannot fail");
82 CoseKeyThumbprint(Sha256::digest(&buf).into())
83}
84
85/// Returns the CBOR encoding of an integer label, used as the sort key for canonical ordering.
86fn encoded_label(label: i64) -> Vec<u8> {
87 let mut buf = Vec::new();
88 ciborium::into_writer(&Value::Integer(Integer::from(label)), &mut buf)
89 .expect("CBOR serialization of an integer label cannot fail");
90 buf
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 /// Example from RFC 9679 §6, an EC2 / P-256 key, given in CBOR extended diagnostic notation:
98 ///
99 /// ```text
100 /// {
101 /// / kty set to EC2 = Elliptic Curve Keys /
102 /// 1:2,
103 /// / crv set to P-256 /
104 /// -1:1,
105 /// / public key: x-coordinate /
106 /// -2:h'65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d',
107 /// / public key: y-coordinate /
108 /// -3:h'1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c',
109 /// / kid is bstr, not used in COSE Key Thumbprint /
110 /// 2:h'496bd8afadf307e5b08c64b0421bf9dc01528a344a43bda88fadd1669da253ec'
111 /// }
112 /// ```
113 ///
114 /// EC2 is not a key type we otherwise support, but validating against it exercises the shared
115 /// canonicalization + CBOR + SHA-256 pipeline against the RFC's own test vector. Note the
116 /// `kid` above happens to equal the thumbprint (the RFC reuses it as a worked example of a
117 /// `kid` set from a key's own thumbprint), but it is not itself hashed — like any other
118 /// non-required parameter, it's excluded from the input.
119 #[test]
120 fn test_rfc9679_ec2_example() {
121 let x = hex::decode("65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d")
122 .unwrap();
123 let y = hex::decode("1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c")
124 .unwrap();
125 let expected_thumbprint =
126 "496bd8afadf307e5b08c64b0421bf9dc01528a344a43bda88fadd1669da253ec";
127
128 // Intentionally provide the params out of canonical order to exercise the sort.
129 let params = vec![
130 (-3i64, Value::Bytes(y)),
131 (1i64, Value::Integer(Integer::from(2))),
132 (-2i64, Value::Bytes(x)),
133 (-1i64, Value::Integer(Integer::from(1))),
134 ];
135
136 let thumbprint = thumbprint_from_required_params(params);
137 assert_eq!(thumbprint.to_hex(), expected_thumbprint);
138 }
139
140 #[test]
141 fn test_accessors() {
142 let thumbprint =
143 thumbprint_from_required_params(vec![(1i64, Value::Integer(Integer::from(4)))]);
144 assert_eq!(thumbprint.as_bytes().len(), 32);
145 assert_eq!(thumbprint.to_hex().len(), 64);
146 }
147}