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}