Skip to main content

rc_crypto/keys/
key_id.rs

1// Copyright 2026-Present Datadog, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Display;
16
17use aws_lc_rs::digest::{SHA256, SHA256_OUTPUT_LEN};
18use thiserror::Error;
19
20use crate::{cached_string_repr::CachedStringRepr, hex::colon_string, keys::PublicKey};
21
22/// Constructing a [`KeyId`] from a byte slice failed due to incorrect length.
23#[derive(Debug, Error)]
24#[error("invalid key ID length {0}, expected {SHA256_OUTPUT_LEN}")]
25pub struct KeyIdParseError(usize);
26
27/// A [`KeyId`] uniquely identifies a [`PublicKey`].
28///
29/// This [`KeyId`] is suitable for use as an X509 Subject Key Identifier defined
30/// in [RFC 5280 § 4.2.1.2]. The SKI is generated by constructing the
31/// `SubjectPublicKeyInfo` ASN.1 message as defined in [RFC 5280 § 4.1] and
32/// hashing the serialised DER bytes with SHA-256.
33///
34/// [RFC 5280 § 4.1]: https://tools.ietf.org/html/rfc5280#section-4.1
35/// [RFC 5280 § 4.2.1.2]:
36///     https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2
37#[derive(Debug, PartialEq, Eq, Hash, Clone)]
38pub struct KeyId {
39    digest: [u8; SHA256_OUTPUT_LEN],
40
41    /// A lazily-rendered string representation of `digest`.
42    ///
43    /// See [`Self::as_hex_str()`] for initialisation.
44    rendered: CachedStringRepr,
45}
46
47impl KeyId {
48    /// Render the [`KeyId`] as a lowercase hex string delimited by colons in
49    /// the style of OpenSSL.
50    ///
51    /// Example: `cc:cb:0f:63:f1:63:5e:f1:0e:26:e8:82:f7:7a:6e:f9`
52    ///
53    /// This value is lazily rendered and cached for reuse.
54    pub fn as_hex_str(&self) -> &str {
55        self.rendered.get_or_init(|| colon_string(self.as_ref()))
56    }
57
58    /// Return this [`KeyId`] as a raw byte slice.
59    pub fn as_bytes(&self) -> &[u8] {
60        &self.digest
61    }
62}
63
64impl std::ops::Deref for KeyId {
65    type Target = [u8; 32];
66
67    fn deref(&self) -> &Self::Target {
68        &self.digest
69    }
70}
71
72impl From<&PublicKey<'_>> for KeyId {
73    fn from(key: &PublicKey) -> Self {
74        // Combine the algorithm identifier and raw key bytes into a DER-encoded
75        // SubjectPublicKeyInfo message.
76        let info = rcgen::PublicKeyData::subject_public_key_info(key);
77
78        Self {
79            digest: aws_lc_rs::digest::digest(&SHA256, &info)
80                .as_ref()
81                .try_into()
82                .expect("sha256 digest is 32 bytes"),
83            rendered: Default::default(),
84        }
85    }
86}
87
88impl TryFrom<&[u8]> for KeyId {
89    type Error = KeyIdParseError;
90
91    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
92        Ok(Self {
93            digest: value.try_into().map_err(|_| KeyIdParseError(value.len()))?,
94            rendered: Default::default(),
95        })
96    }
97}
98
99impl Display for KeyId {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.write_str(self.as_hex_str())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use std::hash::{DefaultHasher, Hash, Hasher};
108
109    use crate::keys::{PrivateKey, tests::fixture_key};
110
111    use super::*;
112
113    /// Ensure a fixed public key always returns the same Subject Key
114    /// Identifier.
115    #[test]
116    fn test_ski_fixture() {
117        const WANT: &[u8] = &[
118            242, 141, 210, 92, 111, 76, 250, 141, 48, 196, 108, 210, 4, 182, 182, 128, 17, 12, 24,
119            54, 159, 16, 208, 42, 122, 158, 205, 152, 190, 76, 82, 160,
120        ];
121
122        let key = fixture_key();
123        let ski = KeyId::from(&key.public_key());
124
125        assert_eq!(*ski, WANT);
126        assert_eq!(
127            ski.to_string(),
128            "f2:8d:d2:5c:6f:4c:fa:8d:30:c4:6c:d2:04:b6:b6:80:11:0c:18:36:9f:10:d0:2a:7a:9e:cd:98:be:4c:52:a0"
129        );
130    }
131
132    #[test]
133    fn test_deterministic_ski() {
134        let key = PrivateKey::new();
135        let public = key.public_key();
136
137        let ski = KeyId::from(&public);
138        assert_eq!(ski, KeyId::from(&public));
139    }
140
141    #[test]
142    fn test_eq() {
143        let key = fixture_key();
144        let a = KeyId::from(&key.public_key());
145        let b = KeyId::from(&key.public_key());
146
147        assert_eq!(a, b);
148
149        // Drive the population of the cached rendered repr.
150        let _ = b.to_string();
151        assert_eq!(a, b);
152    }
153
154    #[test]
155    fn test_hash() {
156        let key = fixture_key();
157        let a = KeyId::from(&key.public_key());
158        let b = KeyId::from(&key.public_key());
159
160        fn do_hash<T: Hash>(t: &T) -> u64 {
161            let mut s = DefaultHasher::new();
162            t.hash(&mut s);
163            s.finish()
164        }
165
166        assert_eq!(do_hash(&a), do_hash(&b));
167
168        // Drive the population of the cached rendered repr.
169        let _ = b.to_string();
170        assert_eq!(do_hash(&a), do_hash(&b));
171    }
172}