Skip to main content

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}