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    /// Constructs a thumbprint from a raw 32-byte SHA-256 digest.
39    pub fn from_bytes(bytes: [u8; 32]) -> Self {
40        CoseKeyThumbprint(bytes)
41    }
42
43    /// The thumbprint as a lowercase hex string.
44    pub fn to_hex(&self) -> String {
45        hex::encode(self.0)
46    }
47}
48
49impl std::fmt::Display for CoseKeyThumbprint {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "SHA256:{}", self.to_hex())
52    }
53}
54
55impl std::fmt::Debug for CoseKeyThumbprint {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "CoseKeyThumbprint({})", self.to_hex())
58    }
59}
60
61/// Computes the [RFC 9679](https://www.rfc-editor.org/rfc/rfc9679) COSE Key Thumbprint of a key.
62pub trait CoseKeyThumbprintExt {
63    /// Returns the SHA-256 COSE Key Thumbprint of this key.
64    fn thumbprint(&self) -> Result<CoseKeyThumbprint, crate::CryptoError>;
65}
66
67/// Builds the RFC 9679 thumbprint from a key type's required parameters.
68///
69/// `params` is the list of `(label, value)` pairs of the required parameters (order does not
70/// matter; this function sorts them into the RFC 8949 §4.2.1 deterministic order). The pairs are
71/// encoded as a canonical CBOR map and hashed with SHA-256.
72pub(crate) fn thumbprint_from_required_params(mut params: Vec<(i64, Value)>) -> CoseKeyThumbprint {
73    // RFC 8949 §4.2.1: map keys are sorted by the bytewise lexicographic order of their encoded
74    // representation. Encoding each label to CBOR and comparing the bytes is exactly this rule.
75    params.sort_by_key(|(label, _)| encoded_label(*label));
76
77    let map = Value::Map(
78        params
79            .into_iter()
80            .map(|(label, value)| (Value::Integer(Integer::from(label)), value))
81            .collect(),
82    );
83
84    let mut buf = Vec::new();
85    ciborium::into_writer(&map, &mut buf)
86        .expect("CBOR serialization of a COSE key parameter map cannot fail");
87    CoseKeyThumbprint(Sha256::digest(&buf).into())
88}
89
90/// Returns the CBOR encoding of an integer label, used as the sort key for canonical ordering.
91fn encoded_label(label: i64) -> Vec<u8> {
92    let mut buf = Vec::new();
93    ciborium::into_writer(&Value::Integer(Integer::from(label)), &mut buf)
94        .expect("CBOR serialization of an integer label cannot fail");
95    buf
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    /// Example from RFC 9679 §6, an EC2 / P-256 key, given in CBOR extended diagnostic notation:
103    ///
104    /// ```text
105    /// {
106    ///   / kty set to EC2 = Elliptic Curve Keys /
107    ///   1:2,
108    ///   / crv set to P-256 /
109    ///   -1:1,
110    ///   / public key: x-coordinate /
111    ///   -2:h'65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d',
112    ///   / public key: y-coordinate /
113    ///   -3:h'1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c',
114    ///   / kid is bstr, not used in COSE Key Thumbprint /
115    ///   2:h'496bd8afadf307e5b08c64b0421bf9dc01528a344a43bda88fadd1669da253ec'
116    /// }
117    /// ```
118    ///
119    /// EC2 is not a key type we otherwise support, but validating against it exercises the shared
120    /// canonicalization + CBOR + SHA-256 pipeline against the RFC's own test vector. Note the
121    /// `kid` above happens to equal the thumbprint (the RFC reuses it as a worked example of a
122    /// `kid` set from a key's own thumbprint), but it is not itself hashed — like any other
123    /// non-required parameter, it's excluded from the input.
124    #[test]
125    fn test_rfc9679_ec2_example() {
126        let x = hex::decode("65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d")
127            .unwrap();
128        let y = hex::decode("1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c")
129            .unwrap();
130        let expected_thumbprint =
131            "496bd8afadf307e5b08c64b0421bf9dc01528a344a43bda88fadd1669da253ec";
132
133        // Intentionally provide the params out of canonical order to exercise the sort.
134        let params = vec![
135            (-3i64, Value::Bytes(y)),
136            (1i64, Value::Integer(Integer::from(2))),
137            (-2i64, Value::Bytes(x)),
138            (-1i64, Value::Integer(Integer::from(1))),
139        ];
140
141        let thumbprint = thumbprint_from_required_params(params);
142        assert_eq!(thumbprint.to_hex(), expected_thumbprint);
143    }
144
145    #[test]
146    fn test_accessors() {
147        let thumbprint =
148            thumbprint_from_required_params(vec![(1i64, Value::Integer(Integer::from(4)))]);
149        assert_eq!(thumbprint.as_bytes().len(), 32);
150        assert_eq!(thumbprint.to_hex().len(), 64);
151    }
152}