Skip to main content

ap_proxy_protocol/
auth.rs

1//! Authentication module for the ap-proxy crate. Authentication works by creating a cryptographic
2//! identity - a signature key-pair. The identity is the public key. It is proven to the proxy, by
3//! signing a challenge provided by the proxy using the signature key.
4
5use coset::{
6    CborSerializable, CoseKey, CoseKeyBuilder, CoseSign1, HeaderBuilder, Label,
7    iana::{self},
8};
9use ed25519_dalek::{Signer, SigningKey, Verifier as Ed25519Verifier, VerifyingKey};
10#[cfg(feature = "experimental-post-quantum-crypto")]
11use ml_dsa::MlDsa65;
12use rand::RngCore;
13use serde::{Deserialize, Serialize};
14use sha2::Digest;
15
16use crate::error::ProxyError;
17
18/// Signature algorithm selection for key generation.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum SignatureAlgorithm {
21    /// Classical Ed25519 signatures (EdDSA)
22    Ed25519,
23    #[cfg(feature = "experimental-post-quantum-crypto")]
24    /// Post-quantum ML-DSA-65 signatures (Dilithium)
25    MlDsa65,
26}
27
28#[allow(clippy::derivable_impls)]
29impl Default for SignatureAlgorithm {
30    fn default() -> Self {
31        #[cfg(not(feature = "experimental-post-quantum-crypto"))]
32        {
33            SignatureAlgorithm::Ed25519
34        }
35        #[cfg(feature = "experimental-post-quantum-crypto")]
36        {
37            SignatureAlgorithm::MlDsa65
38        }
39    }
40}
41
42/// A cryptographic identity key-pair for signing challenges.
43#[derive(Clone)]
44#[allow(clippy::large_enum_variant)]
45pub enum IdentityKeyPair {
46    Ed25519 {
47        private_key_encoded: [u8; 32],
48        private_key: SigningKey,
49        public_key: VerifyingKey,
50    },
51    #[cfg(feature = "experimental-post-quantum-crypto")]
52    /// ML-DSA-65 keys are boxed because their in-memory representation is ~108KB,
53    /// which causes stack overflows on Windows when stored inline in the enum.
54    MlDsa65 {
55        private_key_encoded: [u8; 32],
56        private_key: Box<ml_dsa::SigningKey<MlDsa65>>,
57        public_key: Box<ml_dsa::VerifyingKey<MlDsa65>>,
58    },
59}
60
61impl IdentityKeyPair {
62    /// Generate a new identity key-pair using the default algorithm.
63    pub fn generate() -> Self {
64        Self::generate_with_algorithm(SignatureAlgorithm::default())
65    }
66
67    fn generate_with_algorithm(algorithm: SignatureAlgorithm) -> Self {
68        match algorithm {
69            SignatureAlgorithm::Ed25519 => {
70                let mut seed = [0u8; 32];
71                let mut rng = rand::thread_rng();
72                rng.fill_bytes(&mut seed);
73                let private_key = SigningKey::from_bytes(&seed);
74                let public_key = VerifyingKey::from(&private_key);
75                IdentityKeyPair::Ed25519 {
76                    private_key_encoded: seed,
77                    private_key,
78                    public_key,
79                }
80            }
81            #[cfg(feature = "experimental-post-quantum-crypto")]
82            SignatureAlgorithm::MlDsa65 => {
83                use ml_dsa::KeyGen;
84
85                let mut seed = [0u8; 32];
86                let mut rng = rand::thread_rng();
87                rng.fill_bytes(&mut seed);
88                let keypair = MlDsa65::key_gen_internal(&seed.into());
89                let private_key = keypair.signing_key();
90                let public_key = keypair.verifying_key();
91                IdentityKeyPair::MlDsa65 {
92                    private_key_encoded: seed,
93                    private_key: Box::new(private_key.clone()),
94                    public_key: Box::new(public_key.clone()),
95                }
96            }
97        }
98    }
99
100    /// Serialize this key pair to COSE key format.
101    pub fn to_cose(&self) -> Vec<u8> {
102        match self {
103            IdentityKeyPair::Ed25519 {
104                private_key_encoded,
105                public_key,
106                ..
107            } => {
108                let cose_key = CoseKeyBuilder::new_okp_key()
109                    .algorithm(iana::Algorithm::EdDSA)
110                    .param(
111                        iana::OkpKeyParameter::Crv as i64,
112                        coset::cbor::Value::Integer((iana::Algorithm::EdDSA as i64).into()),
113                    )
114                    .param(
115                        iana::OkpKeyParameter::X as i64,
116                        coset::cbor::Value::Bytes(public_key.to_bytes().to_vec()),
117                    )
118                    .param(
119                        iana::OkpKeyParameter::D as i64,
120                        coset::cbor::Value::Bytes(private_key_encoded.to_vec()),
121                    )
122                    .build();
123                cose_key
124                    .to_vec()
125                    .expect("COSE key serialization should succeed")
126            }
127            #[cfg(feature = "experimental-post-quantum-crypto")]
128            IdentityKeyPair::MlDsa65 {
129                private_key_encoded,
130                public_key,
131                ..
132            } => {
133                let cose_key = CoseKey {
134                    kty: coset::KeyType::Assigned(iana::KeyType::AKP),
135                    alg: Some(coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65)),
136                    params: vec![
137                        (
138                            Label::Int(iana::AkpKeyParameter::Pub as i64),
139                            coset::cbor::Value::Bytes(public_key.encode().to_vec()),
140                        ),
141                        (
142                            Label::Int(iana::AkpKeyParameter::Priv as i64),
143                            coset::cbor::Value::Bytes(private_key_encoded.to_vec()),
144                        ),
145                    ],
146                    ..Default::default()
147                };
148                cose_key
149                    .to_vec()
150                    .expect("COSE key serialization should succeed")
151            }
152        }
153    }
154
155    /// Deserialize a key pair from COSE key format.
156    pub fn from_cose(cose_bytes: &[u8]) -> Result<Self, ProxyError> {
157        let cose_key = CoseKey::from_slice(cose_bytes)
158            .map_err(|_| ProxyError::InvalidMessage("Invalid COSE key encoding".to_string()))?;
159
160        let alg = cose_key.alg.as_ref().ok_or_else(|| {
161            ProxyError::InvalidMessage("Missing algorithm in COSE key".to_string())
162        })?;
163
164        match alg {
165            coset::Algorithm::Assigned(iana::Algorithm::EdDSA) => {
166                // Extract private key seed (D parameter)
167                let mut seed: Option<[u8; 32]> = None;
168                for (label, value) in &cose_key.params {
169                    if *label == Label::Int(iana::OkpKeyParameter::D as i64) {
170                        if let coset::cbor::Value::Bytes(bytes) = value {
171                            if bytes.len() == 32 {
172                                let mut arr = [0u8; 32];
173                                arr.copy_from_slice(bytes);
174                                seed = Some(arr);
175                            }
176                        }
177                    }
178                }
179
180                let seed = seed.ok_or_else(|| {
181                    ProxyError::InvalidMessage(
182                        "Missing Ed25519 private key seed in COSE key".to_string(),
183                    )
184                })?;
185                let private_key = SigningKey::from_bytes(&seed);
186                let public_key = VerifyingKey::from(&private_key);
187
188                Ok(IdentityKeyPair::Ed25519 {
189                    private_key_encoded: seed,
190                    private_key,
191                    public_key,
192                })
193            }
194            #[cfg(feature = "experimental-post-quantum-crypto")]
195            coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65) => {
196                // Extract private key seed
197
198                use ml_dsa::KeyGen;
199                let mut seed: Option<[u8; 32]> = None;
200                for (label, value) in &cose_key.params {
201                    if *label == Label::Int(iana::AkpKeyParameter::Priv as i64) {
202                        if let coset::cbor::Value::Bytes(bytes) = value {
203                            if bytes.len() == 32 {
204                                let mut arr = [0u8; 32];
205                                arr.copy_from_slice(bytes);
206                                seed = Some(arr);
207                            }
208                        }
209                    }
210                }
211
212                let seed = seed.ok_or_else(|| {
213                    ProxyError::InvalidMessage(
214                        "Missing ML-DSA-65 private key seed in COSE key".to_string(),
215                    )
216                })?;
217                let keypair = MlDsa65::key_gen_internal(&seed.into());
218                let private_key = keypair.signing_key();
219                let public_key = keypair.verifying_key();
220
221                Ok(IdentityKeyPair::MlDsa65 {
222                    private_key_encoded: seed,
223                    private_key: Box::new(private_key.clone()),
224                    public_key: Box::new(public_key.clone()),
225                })
226            }
227            _ => Err(ProxyError::InvalidMessage(
228                "Unsupported algorithm in COSE key".to_string(),
229            )),
230        }
231    }
232
233    /// Get the public identity corresponding to this key pair.
234    ///
235    /// The public [`Identity`] contains only the public key and can be shared freely.
236    /// It is used to verify signatures and identify clients to the proxy.
237    ///
238    /// # Examples
239    ///
240    /// ```
241    /// use ap_proxy_protocol::IdentityKeyPair;
242    ///
243    /// let keypair = IdentityKeyPair::generate();
244    /// let public_identity = keypair.identity();
245    ///
246    /// // Share the public identity with others
247    /// println!("My fingerprint: {:?}", public_identity.fingerprint());
248    /// ```
249    pub fn identity(&self) -> Identity {
250        Identity::from(self)
251    }
252}
253
254/// A public cryptographic identity.
255///
256/// Contains the COSE-encoded public key that identifies a client. This can be shared freely
257/// and is used by the proxy to verify challenge-response signatures.
258///
259/// # Examples
260///
261/// ```
262/// use ap_proxy_protocol::IdentityKeyPair;
263///
264/// let keypair = IdentityKeyPair::generate();
265/// let identity = keypair.identity();
266///
267/// // Get a compact fingerprint for identification
268/// let fingerprint = identity.fingerprint();
269/// println!("Identity fingerprint: {:?}", fingerprint);
270/// ```
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct Identity {
273    cose_key_bytes: Vec<u8>,
274}
275
276impl From<&IdentityKeyPair> for Identity {
277    fn from(keypair: &IdentityKeyPair) -> Self {
278        match keypair {
279            IdentityKeyPair::Ed25519 { public_key, .. } => {
280                let cose_key = CoseKeyBuilder::new_okp_key()
281                    .algorithm(iana::Algorithm::EdDSA)
282                    .param(
283                        iana::OkpKeyParameter::Crv as i64,
284                        coset::cbor::Value::Integer((iana::Algorithm::EdDSA as i64).into()),
285                    )
286                    .param(
287                        iana::OkpKeyParameter::X as i64,
288                        coset::cbor::Value::Bytes(public_key.to_bytes().to_vec()),
289                    )
290                    .build();
291                Identity {
292                    cose_key_bytes: cose_key
293                        .to_vec()
294                        .expect("COSE key serialization should succeed"),
295                }
296            }
297            #[cfg(feature = "experimental-post-quantum-crypto")]
298            IdentityKeyPair::MlDsa65 { public_key, .. } => {
299                let cose_key = CoseKey {
300                    kty: coset::KeyType::Assigned(iana::KeyType::AKP),
301                    alg: Some(coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65)),
302                    params: vec![(
303                        Label::Int(iana::AkpKeyParameter::Pub as i64),
304                        coset::cbor::Value::Bytes(public_key.encode().to_vec()),
305                    )],
306                    ..Default::default()
307                };
308                Identity {
309                    cose_key_bytes: cose_key
310                        .to_vec()
311                        .expect("COSE key serialization should succeed"),
312                }
313            }
314        }
315    }
316}
317
318impl Identity {
319    /// Get the signature algorithm for this identity.
320    ///
321    /// Returns the algorithm detected from the COSE key header.
322    pub fn algorithm(&self) -> Option<SignatureAlgorithm> {
323        let cose_key = CoseKey::from_slice(&self.cose_key_bytes).ok()?;
324        match cose_key.alg? {
325            coset::Algorithm::Assigned(iana::Algorithm::EdDSA) => Some(SignatureAlgorithm::Ed25519),
326            #[cfg(feature = "experimental-post-quantum-crypto")]
327            coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65) => {
328                Some(SignatureAlgorithm::MlDsa65)
329            }
330            _ => None,
331        }
332    }
333
334    /// Extract the raw public key bytes from the COSE key.
335    pub fn public_key_bytes(&self) -> Option<Vec<u8>> {
336        let cose_key = CoseKey::from_slice(&self.cose_key_bytes).ok()?;
337        let alg = self.algorithm()?;
338
339        match alg {
340            SignatureAlgorithm::Ed25519 => {
341                // Ed25519: extract X parameter from OKP key
342                for (label, value) in &cose_key.params {
343                    if *label == Label::Int(iana::OkpKeyParameter::X as i64) {
344                        if let coset::cbor::Value::Bytes(bytes) = value {
345                            return Some(bytes.clone());
346                        }
347                    }
348                }
349                None
350            }
351            #[cfg(feature = "experimental-post-quantum-crypto")]
352            SignatureAlgorithm::MlDsa65 => {
353                // ML-DSA-65: extract K parameter (we store public key there)
354                for (label, value) in &cose_key.params {
355                    if *label == Label::Int(iana::SymmetricKeyParameter::K as i64) {
356                        if let coset::cbor::Value::Bytes(bytes) = value {
357                            return Some(bytes.clone());
358                        }
359                    }
360                }
361                None
362            }
363        }
364    }
365
366    /// Compute the SHA256 fingerprint of this identity.
367    ///
368    /// The fingerprint is a 32-byte hash of the public key, providing a compact
369    /// and uniform-length identifier. Fingerprints are used for:
370    /// - Identifying clients in message routing
371    /// - Displaying identities to users
372    /// - Indexing connections in the proxy server
373    ///
374    /// The fingerprint is deterministic - the same identity always produces
375    /// the same fingerprint.
376    ///
377    /// # Examples
378    ///
379    /// ```
380    /// use ap_proxy_protocol::IdentityKeyPair;
381    ///
382    /// let keypair = IdentityKeyPair::generate();
383    /// let identity = keypair.identity();
384    /// let fingerprint = identity.fingerprint();
385    ///
386    /// // Fingerprints can be compared for equality
387    /// assert_eq!(identity.fingerprint(), fingerprint);
388    /// ```
389    pub fn fingerprint(&self) -> IdentityFingerprint {
390        let hash = sha2::Sha256::digest(
391            self.public_key_bytes()
392                .expect("Public key bytes should be extractable for valid identity"),
393        );
394        IdentityFingerprint(hash.into())
395    }
396}
397
398/// A compact SHA256 fingerprint of an [`Identity`].
399///
400/// Fingerprints are 32-byte hashes of public keys, providing a uniform-length
401/// identifier that is easier to work with than full public keys. They are used
402/// throughout the proxy protocol for addressing clients.
403///
404/// # Examples
405///
406/// ```
407/// use ap_proxy_protocol::IdentityKeyPair;
408/// use std::collections::HashMap;
409///
410/// let keypair = IdentityKeyPair::generate();
411/// let fingerprint = keypair.identity().fingerprint();
412///
413/// // Use as a map key
414/// let mut clients = HashMap::new();
415/// clients.insert(fingerprint, "Alice");
416/// ```
417#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
418pub struct IdentityFingerprint(pub [u8; 32]);
419
420impl std::fmt::Debug for IdentityFingerprint {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(f, "IdentityFingerprint({})", hex::encode(self.0))
423    }
424}
425
426impl IdentityFingerprint {
427    /// Parse an `IdentityFingerprint` from a 64-character hex string.
428    ///
429    /// # Errors
430    ///
431    /// Returns [`ProxyError::InvalidMessage`] if the string is not exactly 64
432    /// hex characters or contains non-hex characters.
433    ///
434    /// # Examples
435    ///
436    /// ```
437    /// use ap_proxy_protocol::IdentityFingerprint;
438    ///
439    /// let hex_str = "a".repeat(64);
440    /// let fp = IdentityFingerprint::from_hex(&hex_str).unwrap();
441    /// assert_eq!(fp.to_hex(), hex_str);
442    /// ```
443    pub fn from_hex(s: &str) -> Result<Self, crate::error::ProxyError> {
444        if s.len() != 64 {
445            return Err(crate::error::ProxyError::InvalidMessage(format!(
446                "Fingerprint hex must be exactly 64 characters, got {}",
447                s.len()
448            )));
449        }
450        let bytes = hex::decode(s).map_err(|e| {
451            crate::error::ProxyError::InvalidMessage(format!("Invalid hex in fingerprint: {e}"))
452        })?;
453        let mut arr = [0u8; 32];
454        arr.copy_from_slice(&bytes);
455        Ok(Self(arr))
456    }
457
458    /// Encode this fingerprint as a 64-character lowercase hex string.
459    pub fn to_hex(&self) -> String {
460        hex::encode(self.0)
461    }
462}
463
464/// A cryptographic challenge issued by the proxy server for authentication.
465///
466/// The server sends a random challenge to newly connected clients. Clients must
467/// sign this challenge with their private key to prove their identity without
468/// revealing the private key itself.
469///
470/// # Protocol Flow
471///
472/// 1. Client connects via WebSocket
473/// 2. Server generates and sends [`Challenge`]
474/// 3. Client signs challenge using [`IdentityKeyPair`]
475/// 4. Client sends [`ChallengeResponse`] with signature
476/// 5. Server verifies signature to authenticate client
477///
478/// # Examples
479///
480/// Server-side challenge generation:
481///
482/// ```
483/// use ap_proxy_protocol::Challenge;
484///
485/// let challenge = Challenge::new();
486/// // Send to client for signing
487/// ```
488///
489/// Client-side challenge signing:
490///
491/// ```
492/// use ap_proxy_protocol::{Challenge, IdentityKeyPair};
493///
494/// let keypair = IdentityKeyPair::generate();
495/// # let challenge = Challenge::new();
496/// let response = challenge.sign(&keypair);
497/// // Send response back to server
498/// ```
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct Challenge([u8; 32]);
501
502impl Default for Challenge {
503    fn default() -> Self {
504        Self::new()
505    }
506}
507
508impl Challenge {
509    /// Generate a new random challenge using cryptographically secure randomness.
510    ///
511    /// Each challenge is 32 bytes of random data, providing sufficient entropy to
512    /// prevent replay attacks and ensure uniqueness.
513    ///
514    /// # Examples
515    ///
516    /// ```
517    /// use ap_proxy_protocol::Challenge;
518    ///
519    /// let challenge = Challenge::new();
520    /// // Each call produces a different random challenge
521    /// assert_ne!(format!("{:?}", challenge), format!("{:?}", Challenge::new()));
522    /// ```
523    pub fn new() -> Self {
524        let mut rng = rand::thread_rng();
525        let mut bytes = [0u8; 32];
526        rng.fill_bytes(&mut bytes);
527        Challenge(bytes)
528    }
529
530    /// Sign this challenge using the provided identity key-pair.
531    ///
532    /// # Examples
533    ///
534    /// ```
535    /// use ap_proxy_protocol::{Challenge, IdentityKeyPair};
536    ///
537    /// let keypair = IdentityKeyPair::generate();
538    /// let challenge = Challenge::new();
539    /// let response = challenge.sign(&keypair);
540    ///
541    /// // Verify the signature
542    /// let identity = keypair.identity();
543    /// assert!(response.verify(&challenge, &identity));
544    /// ```
545    pub fn sign(&self, identity: &IdentityKeyPair) -> ChallengeResponse {
546        match identity {
547            IdentityKeyPair::Ed25519 { private_key, .. } => {
548                let signature = private_key.sign(&self.0);
549
550                let cose_sign1 = CoseSign1 {
551                    protected: coset::ProtectedHeader {
552                        original_data: None,
553                        header: HeaderBuilder::new()
554                            .algorithm(iana::Algorithm::EdDSA)
555                            .build(),
556                    },
557                    unprotected: coset::Header::default(),
558                    payload: Some(self.0.to_vec()),
559                    signature: signature.to_bytes().to_vec(),
560                };
561
562                ChallengeResponse {
563                    cose_sign1_bytes: cose_sign1
564                        .to_vec()
565                        .expect("COSE_Sign1 serialization should succeed"),
566                }
567            }
568            #[cfg(feature = "experimental-post-quantum-crypto")]
569            IdentityKeyPair::MlDsa65 { private_key, .. } => {
570                let signature = private_key
571                    .sign_deterministic(&self.0, &[])
572                    .expect("ML-DSA signing should succeed");
573
574                let header = coset::Header {
575                    alg: Some(coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65)),
576                    ..Default::default()
577                };
578
579                let cose_sign1 = CoseSign1 {
580                    protected: coset::ProtectedHeader {
581                        original_data: None,
582                        header,
583                    },
584                    unprotected: coset::Header::default(),
585                    payload: Some(self.0.to_vec()),
586                    signature: signature.encode().to_vec(),
587                };
588
589                ChallengeResponse {
590                    cose_sign1_bytes: cose_sign1
591                        .to_vec()
592                        .expect("COSE_Sign1 serialization should succeed"),
593                }
594            }
595        }
596    }
597}
598
599/// A signed response to an authentication challenge.
600///
601/// Contains a COSE_Sign1 structure with the signature over the challenge bytes,
602/// proving possession of the private key corresponding to the claimed identity.
603///
604/// # Examples
605///
606/// Create and verify a challenge response:
607///
608/// ```
609/// use ap_proxy_protocol::{Challenge, IdentityKeyPair};
610///
611/// // Client signs challenge
612/// let keypair = IdentityKeyPair::generate();
613/// let challenge = Challenge::new();
614/// let response = challenge.sign(&keypair);
615///
616/// // Server verifies response
617/// let identity = keypair.identity();
618/// assert!(response.verify(&challenge, &identity));
619/// ```
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct ChallengeResponse {
622    cose_sign1_bytes: Vec<u8>,
623}
624
625impl ChallengeResponse {
626    /// Verify this response against the original challenge and claimed identity.
627    ///
628    /// Returns `true` if the signature is valid and was created by the private key
629    /// corresponding to the provided identity. Returns `false` if:
630    /// - The signature is malformed
631    /// - The signature verification fails
632    /// - The identity public key is invalid
633    /// - The algorithm in the signature doesn't match the identity
634    ///
635    /// # Authentication Process
636    ///
637    /// The server uses this method to authenticate clients:
638    /// 1. Receive [`Identity`] and [`ChallengeResponse`] from client
639    /// 2. Call `response.verify(&original_challenge, &claimed_identity)`
640    /// 3. If `true`, the client possesses the private key (authenticated)
641    /// 4. If `false`, reject the authentication attempt
642    ///
643    /// # Examples
644    ///
645    /// ```
646    /// use ap_proxy_protocol::{Challenge, IdentityKeyPair};
647    ///
648    /// let keypair = IdentityKeyPair::generate();
649    /// let challenge = Challenge::new();
650    /// let response = challenge.sign(&keypair);
651    ///
652    /// // Valid signature
653    /// assert!(response.verify(&challenge, &keypair.identity()));
654    ///
655    /// // Invalid signature (different challenge)
656    /// let other_challenge = Challenge::new();
657    /// assert!(!response.verify(&other_challenge, &keypair.identity()));
658    ///
659    /// // Invalid signature (different identity)
660    /// let other_keypair = IdentityKeyPair::generate();
661    /// assert!(!response.verify(&challenge, &other_keypair.identity()));
662    /// ```
663    pub fn verify(&self, challenge: &Challenge, identity: &Identity) -> bool {
664        let cose_sign1 = match CoseSign1::from_slice(&self.cose_sign1_bytes) {
665            Ok(s) => s,
666            Err(_) => return false,
667        };
668
669        // Extract algorithm from protected header
670        let sig_alg = match &cose_sign1.protected.header.alg {
671            Some(coset::Algorithm::Assigned(iana::Algorithm::EdDSA)) => SignatureAlgorithm::Ed25519,
672            #[cfg(feature = "experimental-post-quantum-crypto")]
673            Some(coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65)) => {
674                SignatureAlgorithm::MlDsa65
675            }
676            _ => return false,
677        };
678
679        // Algorithm must be valid and supported
680        let identity_alg = match identity.algorithm() {
681            Some(alg) => alg,
682            None => return false,
683        };
684
685        // Algorithms must match
686        if sig_alg != identity_alg {
687            return false;
688        }
689
690        // Payload must match challenge
691        let payload = match &cose_sign1.payload {
692            Some(p) => p,
693            None => return false,
694        };
695        if payload.as_slice() != challenge.0.as_slice() {
696            return false;
697        }
698
699        // Extract public key bytes
700        let pk_bytes = match identity.public_key_bytes() {
701            Some(bytes) => bytes,
702            None => return false,
703        };
704
705        // Dispatch to appropriate verification function
706        match sig_alg {
707            SignatureAlgorithm::Ed25519 => {
708                verify_ed25519(&cose_sign1.signature, &challenge.0, &pk_bytes)
709            }
710            #[cfg(feature = "experimental-post-quantum-crypto")]
711            SignatureAlgorithm::MlDsa65 => {
712                verify_ml_dsa_65(&cose_sign1.signature, &challenge.0, &pk_bytes)
713            }
714        }
715    }
716}
717
718fn verify_ed25519(sig: &[u8], msg: &[u8], pk: &[u8]) -> bool {
719    let signature: ed25519_dalek::Signature = match sig.try_into() {
720        Ok(sig_bytes) => ed25519_dalek::Signature::from_bytes(sig_bytes),
721        Err(_) => return false,
722    };
723
724    let public_key: VerifyingKey = match pk.try_into() {
725        Ok(pk_bytes) => match VerifyingKey::from_bytes(pk_bytes) {
726            Ok(pk) => pk,
727            Err(_) => return false,
728        },
729        Err(_) => return false,
730    };
731
732    public_key.verify(msg, &signature).is_ok()
733}
734
735#[cfg(feature = "experimental-post-quantum-crypto")]
736fn verify_ml_dsa_65(sig: &[u8], msg: &[u8], pk: &[u8]) -> bool {
737    use ml_dsa::signature::Verifier;
738
739    let signature = match sig.try_into() {
740        Ok(sig_bytes) => match ml_dsa::Signature::<MlDsa65>::decode(&sig_bytes) {
741            Some(sig) => sig,
742            None => return false,
743        },
744        Err(_) => return false,
745    };
746
747    let public_key = match pk.try_into() {
748        Ok(pk_bytes) => ml_dsa::VerifyingKey::<MlDsa65>::decode(&pk_bytes),
749        Err(_) => return false,
750    };
751
752    public_key.verify(msg, &signature).is_ok()
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    #[test]
760    fn test_fingerprint_hex_roundtrip() {
761        let fp = IdentityFingerprint([0xab; 32]);
762        let hex_str = fp.to_hex();
763        assert_eq!(hex_str.len(), 64);
764        let parsed = IdentityFingerprint::from_hex(&hex_str).expect("should parse");
765        assert_eq!(parsed, fp);
766    }
767
768    #[test]
769    fn test_fingerprint_from_hex_wrong_length() {
770        let err = IdentityFingerprint::from_hex("aabb").unwrap_err();
771        assert!(err.to_string().contains("64 characters"));
772    }
773
774    #[test]
775    fn test_fingerprint_from_hex_invalid_chars() {
776        let bad = format!("{}zz", "aa".repeat(31));
777        assert!(IdentityFingerprint::from_hex(&bad).is_err());
778    }
779
780    #[test]
781    fn test_identity_keypair_generation() {
782        let identity_keypair = IdentityKeyPair::generate();
783        let challenge = Challenge::new();
784        let response = challenge.sign(&identity_keypair);
785        assert!(response.verify(&challenge, &identity_keypair.identity()));
786    }
787
788    #[test]
789    fn test_encoding_roundtrip() {
790        let identity_keypair = IdentityKeyPair::generate();
791        let cose_bytes = identity_keypair.to_cose();
792        let decoded_keypair =
793            IdentityKeyPair::from_cose(&cose_bytes).expect("Decoding should succeed");
794
795        // Sign and verify to ensure keys match
796        let challenge = Challenge::new();
797        let response = challenge.sign(&decoded_keypair);
798        assert!(response.verify(&challenge, &decoded_keypair.identity()));
799    }
800
801    #[test]
802    fn test_challenge_response() {
803        let identity_keypair = IdentityKeyPair::generate();
804        let public_identity = identity_keypair.identity();
805        let challenge = Challenge::new();
806        let response = challenge.sign(&identity_keypair);
807        assert!(response.verify(&challenge, &public_identity));
808    }
809
810    #[test]
811    fn test_challenge_response_wrong_challenge() {
812        let identity_keypair = IdentityKeyPair::generate();
813        let public_identity = identity_keypair.identity();
814        let challenge1 = Challenge::new();
815        let challenge2 = Challenge::new();
816        let response = challenge1.sign(&identity_keypair);
817        assert!(!response.verify(&challenge2, &public_identity));
818    }
819
820    #[test]
821    fn test_challenge_response_wrong_identity() {
822        let identity_keypair1 = IdentityKeyPair::generate();
823        let identity_keypair2 = IdentityKeyPair::generate();
824        let challenge = Challenge::new();
825        let response = challenge.sign(&identity_keypair1);
826        assert!(!response.verify(&challenge, &identity_keypair2.identity()));
827    }
828
829    #[test]
830    fn test_ed25519_round_trip() {
831        let keypair = IdentityKeyPair::generate_with_algorithm(SignatureAlgorithm::Ed25519);
832        let challenge = Challenge::new();
833        let response = challenge.sign(&keypair);
834        assert!(response.verify(&challenge, &keypair.identity()));
835    }
836
837    #[cfg(feature = "experimental-post-quantum-crypto")]
838    #[test]
839    fn test_ml_dsa_round_trip() {
840        let keypair = IdentityKeyPair::generate_with_algorithm(SignatureAlgorithm::MlDsa65);
841        let challenge = Challenge::new();
842        let response = challenge.sign(&keypair);
843        assert!(response.verify(&challenge, &keypair.identity()));
844    }
845
846    #[test]
847    fn test_cose_algorithm_detection() {
848        let ed25519_keypair = IdentityKeyPair::generate_with_algorithm(SignatureAlgorithm::Ed25519);
849        #[cfg(feature = "experimental-post-quantum-crypto")]
850        let ml_dsa_keypair = IdentityKeyPair::generate_with_algorithm(SignatureAlgorithm::MlDsa65);
851
852        assert_eq!(
853            ed25519_keypair.identity().algorithm(),
854            Some(SignatureAlgorithm::Ed25519)
855        );
856        #[cfg(feature = "experimental-post-quantum-crypto")]
857        assert_eq!(
858            ml_dsa_keypair.identity().algorithm(),
859            Some(SignatureAlgorithm::MlDsa65)
860        );
861    }
862}