1use serde::{Deserialize, Serialize};
40
41use crate::types::Base64UrlEncoded;
42
43#[cfg(feature = "subkey-chain-v1")]
44use p256::ecdsa::signature::Verifier;
45
46#[cfg(feature = "subkey-chain-v1")]
49pub const SUBKEY_CHAIN_V1_DOMAIN: &[u8] = b"auths-device-subkey-v1";
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
56pub struct SubkeyChain {
57 pub bootstrap_pubkey: Base64UrlEncoded,
61 pub subkey_binding_signature: Base64UrlEncoded,
64}
65
66#[cfg(feature = "subkey-chain-v1")]
68#[derive(Debug, thiserror::Error)]
69pub enum SubkeyChainError {
70 #[error("bootstrap pubkey decode failed: {0}")]
72 BootstrapPubkeyDecode(String),
73 #[error("bootstrap pubkey wrong length: got {0}, expected 33")]
75 BootstrapPubkeyLength(usize),
76 #[error("bootstrap pubkey not a valid P-256 point: {0}")]
78 BootstrapPubkeyInvalid(String),
79 #[error("binding signature decode failed: {0}")]
81 SignatureDecode(String),
82 #[error("binding signature wrong length: got {0}, expected 64")]
84 SignatureLength(usize),
85 #[error("subkey pubkey wrong length: got {0}, expected 33")]
87 SubkeyPubkeyLength(usize),
88 #[error("binding signature does not verify under bootstrap pubkey")]
91 VerifyFailed,
92 #[error("chain is self-referential (bootstrap == subkey)")]
96 SelfReferential,
97}
98
99#[cfg(feature = "subkey-chain-v1")]
106pub fn build_binding_message_v1(session_id: &str, subkey_pubkey_compressed: &[u8]) -> Vec<u8> {
107 let mut out = Vec::with_capacity(
108 SUBKEY_CHAIN_V1_DOMAIN.len() + session_id.len() + subkey_pubkey_compressed.len(),
109 );
110 out.extend_from_slice(SUBKEY_CHAIN_V1_DOMAIN);
111 out.extend_from_slice(session_id.as_bytes());
112 out.extend_from_slice(subkey_pubkey_compressed);
113 out
114}
115
116#[cfg(feature = "subkey-chain-v1")]
134pub fn verify_subkey_chain(
135 chain: &SubkeyChain,
136 subkey_pubkey_compressed: &[u8],
137 session_id: &str,
138) -> Result<[u8; 33], SubkeyChainError> {
139 if subkey_pubkey_compressed.len() != 33 {
140 return Err(SubkeyChainError::SubkeyPubkeyLength(
141 subkey_pubkey_compressed.len(),
142 ));
143 }
144
145 let bootstrap_bytes = chain
146 .bootstrap_pubkey
147 .decode()
148 .map_err(|e| SubkeyChainError::BootstrapPubkeyDecode(e.to_string()))?;
149 if bootstrap_bytes.len() != 33 {
150 return Err(SubkeyChainError::BootstrapPubkeyLength(
151 bootstrap_bytes.len(),
152 ));
153 }
154 let mut bootstrap_arr = [0u8; 33];
155 bootstrap_arr.copy_from_slice(&bootstrap_bytes);
156
157 if bootstrap_arr == subkey_pubkey_compressed[..] {
158 return Err(SubkeyChainError::SelfReferential);
159 }
160
161 let bootstrap_vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(&bootstrap_arr)
162 .map_err(|e| SubkeyChainError::BootstrapPubkeyInvalid(e.to_string()))?;
163
164 let sig_bytes = chain
165 .subkey_binding_signature
166 .decode()
167 .map_err(|e| SubkeyChainError::SignatureDecode(e.to_string()))?;
168 if sig_bytes.len() != 64 {
169 return Err(SubkeyChainError::SignatureLength(sig_bytes.len()));
170 }
171 let sig = p256::ecdsa::Signature::from_slice(&sig_bytes)
172 .map_err(|e| SubkeyChainError::BootstrapPubkeyInvalid(e.to_string()))?;
173
174 let msg = build_binding_message_v1(session_id, subkey_pubkey_compressed);
175
176 bootstrap_vk
177 .verify(&msg, &sig)
178 .map_err(|_| SubkeyChainError::VerifyFailed)?;
179
180 Ok(bootstrap_arr)
181}
182
183#[cfg(all(test, feature = "subkey-chain-v1"))]
184mod tests {
185 use super::*;
186 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
187 use p256::ecdsa::signature::Signer;
188 use p256::ecdsa::{Signature, SigningKey};
189 use rand::rngs::OsRng;
190
191 fn random_keypair() -> (SigningKey, [u8; 33]) {
192 let sk = SigningKey::random(&mut OsRng);
193 let compressed = sk.verifying_key().to_encoded_point(true);
194 let mut arr = [0u8; 33];
195 arr.copy_from_slice(compressed.as_bytes());
196 (sk, arr)
197 }
198
199 fn sign_chain(bootstrap: &SigningKey, session_id: &str, subkey: &[u8; 33]) -> SubkeyChain {
200 let msg = build_binding_message_v1(session_id, subkey);
201 let sig: Signature = bootstrap.sign(&msg);
202 let raw: [u8; 64] = sig.to_bytes().into();
203 let bootstrap_compressed = bootstrap.verifying_key().to_encoded_point(true);
204 SubkeyChain {
205 bootstrap_pubkey: Base64UrlEncoded::from_raw(
206 URL_SAFE_NO_PAD.encode(bootstrap_compressed.as_bytes()),
207 ),
208 subkey_binding_signature: Base64UrlEncoded::from_raw(URL_SAFE_NO_PAD.encode(raw)),
209 }
210 }
211
212 #[test]
213 fn valid_chain_verifies_and_returns_bootstrap_bytes() {
214 let (bootstrap_sk, bootstrap_pk) = random_keypair();
215 let (_subkey_sk, subkey_pk) = random_keypair();
216 let chain = sign_chain(&bootstrap_sk, "sess-abc", &subkey_pk);
217
218 let out =
219 verify_subkey_chain(&chain, &subkey_pk, "sess-abc").expect("valid chain must verify");
220 assert_eq!(out, bootstrap_pk);
221 }
222
223 #[test]
224 fn binding_message_format_is_domain_plus_session_plus_subkey() {
225 let (_sk, pk) = random_keypair();
226 let msg = build_binding_message_v1("sess-XYZ", &pk);
227 assert!(msg.starts_with(SUBKEY_CHAIN_V1_DOMAIN));
228 assert_eq!(
229 &msg[SUBKEY_CHAIN_V1_DOMAIN.len()..SUBKEY_CHAIN_V1_DOMAIN.len() + 8],
230 b"sess-XYZ"
231 );
232 assert_eq!(&msg[SUBKEY_CHAIN_V1_DOMAIN.len() + 8..], &pk[..]);
233 }
234
235 #[test]
236 fn different_session_id_fails_verification() {
237 let (bootstrap_sk, _) = random_keypair();
238 let (_, subkey_pk) = random_keypair();
239 let chain = sign_chain(&bootstrap_sk, "sess-abc", &subkey_pk);
240
241 let err = verify_subkey_chain(&chain, &subkey_pk, "sess-different").unwrap_err();
242 assert!(matches!(err, SubkeyChainError::VerifyFailed));
243 }
244
245 #[test]
246 fn different_subkey_pubkey_fails_verification() {
247 let (bootstrap_sk, _) = random_keypair();
248 let (_, subkey_a) = random_keypair();
249 let (_, subkey_b) = random_keypair();
250 let chain = sign_chain(&bootstrap_sk, "sess-abc", &subkey_a);
251
252 let err = verify_subkey_chain(&chain, &subkey_b, "sess-abc").unwrap_err();
254 assert!(matches!(err, SubkeyChainError::VerifyFailed));
255 }
256
257 #[test]
258 fn self_referential_chain_rejected() {
259 let (sk, pk) = random_keypair();
260 let chain = sign_chain(&sk, "sess-abc", &pk);
262 let err = verify_subkey_chain(&chain, &pk, "sess-abc").unwrap_err();
263 assert!(matches!(err, SubkeyChainError::SelfReferential));
264 }
265
266 #[test]
267 fn wrong_subkey_length_errors() {
268 let (sk, _) = random_keypair();
269 let (_, subkey_pk) = random_keypair();
270 let chain = sign_chain(&sk, "sess-abc", &subkey_pk);
271 let err = verify_subkey_chain(&chain, &[0u8; 32], "sess-abc").unwrap_err();
272 assert!(matches!(err, SubkeyChainError::SubkeyPubkeyLength(32)));
273 }
274
275 #[test]
276 fn wrong_bootstrap_pubkey_length_errors() {
277 let (_, subkey_pk) = random_keypair();
278 let chain = SubkeyChain {
279 bootstrap_pubkey: Base64UrlEncoded::from_raw(URL_SAFE_NO_PAD.encode([0u8; 32])),
280 subkey_binding_signature: Base64UrlEncoded::from_raw(URL_SAFE_NO_PAD.encode([0u8; 64])),
281 };
282 let err = verify_subkey_chain(&chain, &subkey_pk, "sess-abc").unwrap_err();
283 assert!(matches!(err, SubkeyChainError::BootstrapPubkeyLength(32)));
284 }
285
286 #[test]
287 fn wrong_signature_length_errors() {
288 let (bootstrap_sk, _) = random_keypair();
289 let (_, subkey_pk) = random_keypair();
290 let bootstrap_compressed = bootstrap_sk.verifying_key().to_encoded_point(true);
291 let chain = SubkeyChain {
292 bootstrap_pubkey: Base64UrlEncoded::from_raw(
293 URL_SAFE_NO_PAD.encode(bootstrap_compressed.as_bytes()),
294 ),
295 subkey_binding_signature: Base64UrlEncoded::from_raw(URL_SAFE_NO_PAD.encode([0u8; 32])),
297 };
298 let err = verify_subkey_chain(&chain, &subkey_pk, "sess-abc").unwrap_err();
299 assert!(matches!(err, SubkeyChainError::SignatureLength(32)));
300 }
301}