1#![forbid(unsafe_code)]
10
11pub const MESH_IDENTITY_INFO: &str = "wm/mesh-identity/v1";
13
14pub const RECORD_ATTESTATION_INFO: &str = "wm/record-attestation/v1";
16
17pub const GALAXY_DEK_INFO_PREFIX: &str = "wm/galaxy-dek/v1";
24
25#[must_use]
28pub fn galaxy_dek_info(galaxy_db_name: &str) -> String {
29 format!("{GALAXY_DEK_INFO_PREFIX}/{galaxy_db_name}")
30}
31
32pub const RELEASE_SIGNING_INFO: &str = "wm/release-signing/v1";
38
39#[must_use]
44pub fn hkdf32(root: &[u8], info: &str) -> [u8; 32] {
45 let mut out = [0u8; 32];
46 expand(root, info, &mut out);
47 out
48}
49
50pub fn expand(root: &[u8], info: &str, okm: &mut [u8]) {
56 let hk = hkdf::Hkdf::<sha2::Sha256>::new(None, root);
57 hk.expand(info.as_bytes(), okm)
58 .expect("HKDF-SHA256 output length must be <= 255*32 bytes");
59}
60
61#[must_use]
70pub fn root_bytes(material: &str) -> Vec<u8> {
71 let trimmed = material.trim();
72 if trimmed.len() == 64 {
73 if let Some(decoded) = decode_hex32(trimmed) {
74 return decoded.to_vec();
75 }
76 }
77 material.as_bytes().to_vec()
78}
79
80fn decode_hex32(hex: &str) -> Option<[u8; 32]> {
81 if hex.len() != 64 {
82 return None;
83 }
84 let mut out = [0u8; 32];
85 for (i, chunk) in hex.as_bytes().chunks_exact(2).enumerate() {
86 let hi = hex_val(chunk[0])?;
87 let lo = hex_val(chunk[1])?;
88 out[i] = (hi << 4) | lo;
89 }
90 Some(out)
91}
92
93const fn hex_val(b: u8) -> Option<u8> {
94 match b {
95 b'0'..=b'9' => Some(b - b'0'),
96 b'a'..=b'f' => Some(b - b'a' + 10),
97 b'A'..=b'F' => Some(b - b'A' + 10),
98 _ => None,
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn hex(bytes: &[u8]) -> String {
107 const HEX: &[u8; 16] = b"0123456789abcdef";
108 let mut out = String::with_capacity(bytes.len() * 2);
109 for b in bytes {
110 out.push(HEX[(b >> 4) as usize] as char);
111 out.push(HEX[(b & 0x0f) as usize] as char);
112 }
113 out
114 }
115
116 #[test]
117 fn rfc5869_test_case_1_is_reproduced() {
118 let ikm = [0x0b; 22];
120 let salt: [u8; 13] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
121 let info: [u8; 10] = [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9];
122 let hk = hkdf::Hkdf::<sha2::Sha256>::new(Some(&salt), &ikm);
123 let mut okm = [0u8; 42];
124 hk.expand(&info, &mut okm).unwrap();
125 assert_eq!(
126 hex(&okm),
127 "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf\
128 34007208d5b887185865"
129 );
130 }
131
132 #[test]
133 fn derivation_is_deterministic_and_domain_separated() {
134 let root = b"node-root-material-0123456789";
135 let mesh_a = hkdf32(root, MESH_IDENTITY_INFO);
136 let mesh_b = hkdf32(root, MESH_IDENTITY_INFO);
137 assert_eq!(
138 mesh_a, mesh_b,
139 "same root + info must derive the same subkey"
140 );
141
142 let attestation = hkdf32(root, RECORD_ATTESTATION_INFO);
143 assert_ne!(
144 mesh_a, attestation,
145 "mesh identity and attestation subkeys must never collide"
146 );
147 assert_ne!(mesh_a, hkdf32(root, "wm/beacon/v1"));
148 assert_ne!(mesh_a, hkdf32(b"another-root", MESH_IDENTITY_INFO));
149 }
150
151 #[test]
152 fn galaxy_dek_info_is_versioned_and_per_galaxy() {
153 assert_eq!(galaxy_dek_info("codex"), "wm/galaxy-dek/v1/codex");
154 assert_eq!(
155 galaxy_dek_info("codex"),
156 format!("{GALAXY_DEK_INFO_PREFIX}/codex")
157 );
158 assert_ne!(galaxy_dek_info("codex"), galaxy_dek_info("sessions"));
159 assert_ne!(
160 galaxy_dek_info("codex"),
161 RECORD_ATTESTATION_INFO,
162 "DEK wrapping must never share purpose space with signers"
163 );
164 }
165
166 #[test]
167 fn expand_writes_only_the_requested_length() {
168 let root = b"short-root";
169 let mut out = [0xAAu8; 64];
170 expand(root, MESH_IDENTITY_INFO, &mut out[..16]);
171 assert_eq!(
172 &out[16..],
173 [0xAA; 48],
174 "bytes past the output length untouched"
175 );
176 }
177
178 #[test]
179 fn root_material_is_canonical_hex_decode_with_raw_fallback() {
180 let hex_key = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
181 let decoded = root_bytes(hex_key);
182 assert_eq!(decoded.len(), 32);
183 assert_eq!(decoded[0], 0x00);
184 assert_eq!(decoded[3], 0x33);
185 assert_eq!(
186 hkdf32(&decoded, MESH_IDENTITY_INFO),
187 hkdf32(&root_bytes(hex_key), MESH_IDENTITY_INFO),
188 "same env material must always derive the same subkey"
189 );
190 assert_ne!(
191 decoded.as_slice(),
192 hex_key.as_bytes(),
193 "64-hex is decoded, not used as ASCII"
194 );
195
196 assert_eq!(root_bytes("legacy-or-test-key"), b"legacy-or-test-key");
197 let invalid_64 = "z".repeat(64);
198 assert_eq!(
199 root_bytes(&invalid_64),
200 invalid_64.as_bytes(),
201 "64 chars that are not hex fall back to raw bytes"
202 );
203 }
204}