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
426/// A cryptographic challenge issued by the proxy server for authentication.
427///
428/// The server sends a random challenge to newly connected clients. Clients must
429/// sign this challenge with their private key to prove their identity without
430/// revealing the private key itself.
431///
432/// # Protocol Flow
433///
434/// 1. Client connects via WebSocket
435/// 2. Server generates and sends [`Challenge`]
436/// 3. Client signs challenge using [`IdentityKeyPair`]
437/// 4. Client sends [`ChallengeResponse`] with signature
438/// 5. Server verifies signature to authenticate client
439///
440/// # Examples
441///
442/// Server-side challenge generation:
443///
444/// ```
445/// use ap_proxy_protocol::Challenge;
446///
447/// let challenge = Challenge::new();
448/// // Send to client for signing
449/// ```
450///
451/// Client-side challenge signing:
452///
453/// ```
454/// use ap_proxy_protocol::{Challenge, IdentityKeyPair};
455///
456/// let keypair = IdentityKeyPair::generate();
457/// # let challenge = Challenge::new();
458/// let response = challenge.sign(&keypair);
459/// // Send response back to server
460/// ```
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct Challenge([u8; 32]);
463
464impl Default for Challenge {
465    fn default() -> Self {
466        Self::new()
467    }
468}
469
470impl Challenge {
471    /// Generate a new random challenge using cryptographically secure randomness.
472    ///
473    /// Each challenge is 32 bytes of random data, providing sufficient entropy to
474    /// prevent replay attacks and ensure uniqueness.
475    ///
476    /// # Examples
477    ///
478    /// ```
479    /// use ap_proxy_protocol::Challenge;
480    ///
481    /// let challenge = Challenge::new();
482    /// // Each call produces a different random challenge
483    /// assert_ne!(format!("{:?}", challenge), format!("{:?}", Challenge::new()));
484    /// ```
485    pub fn new() -> Self {
486        let mut rng = rand::thread_rng();
487        let mut bytes = [0u8; 32];
488        rng.fill_bytes(&mut bytes);
489        Challenge(bytes)
490    }
491
492    /// Sign this challenge using the provided identity key-pair.
493    ///
494    /// # Examples
495    ///
496    /// ```
497    /// use ap_proxy_protocol::{Challenge, IdentityKeyPair};
498    ///
499    /// let keypair = IdentityKeyPair::generate();
500    /// let challenge = Challenge::new();
501    /// let response = challenge.sign(&keypair);
502    ///
503    /// // Verify the signature
504    /// let identity = keypair.identity();
505    /// assert!(response.verify(&challenge, &identity));
506    /// ```
507    pub fn sign(&self, identity: &IdentityKeyPair) -> ChallengeResponse {
508        match identity {
509            IdentityKeyPair::Ed25519 { private_key, .. } => {
510                let signature = private_key.sign(&self.0);
511
512                let cose_sign1 = CoseSign1 {
513                    protected: coset::ProtectedHeader {
514                        original_data: None,
515                        header: HeaderBuilder::new()
516                            .algorithm(iana::Algorithm::EdDSA)
517                            .build(),
518                    },
519                    unprotected: coset::Header::default(),
520                    payload: Some(self.0.to_vec()),
521                    signature: signature.to_bytes().to_vec(),
522                };
523
524                ChallengeResponse {
525                    cose_sign1_bytes: cose_sign1
526                        .to_vec()
527                        .expect("COSE_Sign1 serialization should succeed"),
528                }
529            }
530            #[cfg(feature = "experimental-post-quantum-crypto")]
531            IdentityKeyPair::MlDsa65 { private_key, .. } => {
532                let signature = private_key
533                    .sign_deterministic(&self.0, &[])
534                    .expect("ML-DSA signing should succeed");
535
536                let header = coset::Header {
537                    alg: Some(coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65)),
538                    ..Default::default()
539                };
540
541                let cose_sign1 = CoseSign1 {
542                    protected: coset::ProtectedHeader {
543                        original_data: None,
544                        header,
545                    },
546                    unprotected: coset::Header::default(),
547                    payload: Some(self.0.to_vec()),
548                    signature: signature.encode().to_vec(),
549                };
550
551                ChallengeResponse {
552                    cose_sign1_bytes: cose_sign1
553                        .to_vec()
554                        .expect("COSE_Sign1 serialization should succeed"),
555                }
556            }
557        }
558    }
559}
560
561/// A signed response to an authentication challenge.
562///
563/// Contains a COSE_Sign1 structure with the signature over the challenge bytes,
564/// proving possession of the private key corresponding to the claimed identity.
565///
566/// # Examples
567///
568/// Create and verify a challenge response:
569///
570/// ```
571/// use ap_proxy_protocol::{Challenge, IdentityKeyPair};
572///
573/// // Client signs challenge
574/// let keypair = IdentityKeyPair::generate();
575/// let challenge = Challenge::new();
576/// let response = challenge.sign(&keypair);
577///
578/// // Server verifies response
579/// let identity = keypair.identity();
580/// assert!(response.verify(&challenge, &identity));
581/// ```
582#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct ChallengeResponse {
584    cose_sign1_bytes: Vec<u8>,
585}
586
587impl ChallengeResponse {
588    /// Verify this response against the original challenge and claimed identity.
589    ///
590    /// Returns `true` if the signature is valid and was created by the private key
591    /// corresponding to the provided identity. Returns `false` if:
592    /// - The signature is malformed
593    /// - The signature verification fails
594    /// - The identity public key is invalid
595    /// - The algorithm in the signature doesn't match the identity
596    ///
597    /// # Authentication Process
598    ///
599    /// The server uses this method to authenticate clients:
600    /// 1. Receive [`Identity`] and [`ChallengeResponse`] from client
601    /// 2. Call `response.verify(&original_challenge, &claimed_identity)`
602    /// 3. If `true`, the client possesses the private key (authenticated)
603    /// 4. If `false`, reject the authentication attempt
604    ///
605    /// # Examples
606    ///
607    /// ```
608    /// use ap_proxy_protocol::{Challenge, IdentityKeyPair};
609    ///
610    /// let keypair = IdentityKeyPair::generate();
611    /// let challenge = Challenge::new();
612    /// let response = challenge.sign(&keypair);
613    ///
614    /// // Valid signature
615    /// assert!(response.verify(&challenge, &keypair.identity()));
616    ///
617    /// // Invalid signature (different challenge)
618    /// let other_challenge = Challenge::new();
619    /// assert!(!response.verify(&other_challenge, &keypair.identity()));
620    ///
621    /// // Invalid signature (different identity)
622    /// let other_keypair = IdentityKeyPair::generate();
623    /// assert!(!response.verify(&challenge, &other_keypair.identity()));
624    /// ```
625    pub fn verify(&self, challenge: &Challenge, identity: &Identity) -> bool {
626        let cose_sign1 = match CoseSign1::from_slice(&self.cose_sign1_bytes) {
627            Ok(s) => s,
628            Err(_) => return false,
629        };
630
631        // Extract algorithm from protected header
632        let sig_alg = match &cose_sign1.protected.header.alg {
633            Some(coset::Algorithm::Assigned(iana::Algorithm::EdDSA)) => SignatureAlgorithm::Ed25519,
634            #[cfg(feature = "experimental-post-quantum-crypto")]
635            Some(coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65)) => {
636                SignatureAlgorithm::MlDsa65
637            }
638            _ => return false,
639        };
640
641        // Algorithm must be valid and supported
642        let identity_alg = match identity.algorithm() {
643            Some(alg) => alg,
644            None => return false,
645        };
646
647        // Algorithms must match
648        if sig_alg != identity_alg {
649            return false;
650        }
651
652        // Payload must match challenge
653        let payload = match &cose_sign1.payload {
654            Some(p) => p,
655            None => return false,
656        };
657        if payload.as_slice() != challenge.0.as_slice() {
658            return false;
659        }
660
661        // Extract public key bytes
662        let pk_bytes = match identity.public_key_bytes() {
663            Some(bytes) => bytes,
664            None => return false,
665        };
666
667        // Dispatch to appropriate verification function
668        match sig_alg {
669            SignatureAlgorithm::Ed25519 => {
670                verify_ed25519(&cose_sign1.signature, &challenge.0, &pk_bytes)
671            }
672            #[cfg(feature = "experimental-post-quantum-crypto")]
673            SignatureAlgorithm::MlDsa65 => {
674                verify_ml_dsa_65(&cose_sign1.signature, &challenge.0, &pk_bytes)
675            }
676        }
677    }
678}
679
680fn verify_ed25519(sig: &[u8], msg: &[u8], pk: &[u8]) -> bool {
681    let signature: ed25519_dalek::Signature = match sig.try_into() {
682        Ok(sig_bytes) => ed25519_dalek::Signature::from_bytes(sig_bytes),
683        Err(_) => return false,
684    };
685
686    let public_key: VerifyingKey = match pk.try_into() {
687        Ok(pk_bytes) => match VerifyingKey::from_bytes(pk_bytes) {
688            Ok(pk) => pk,
689            Err(_) => return false,
690        },
691        Err(_) => return false,
692    };
693
694    public_key.verify(msg, &signature).is_ok()
695}
696
697#[cfg(feature = "experimental-post-quantum-crypto")]
698fn verify_ml_dsa_65(sig: &[u8], msg: &[u8], pk: &[u8]) -> bool {
699    use ml_dsa::signature::Verifier;
700
701    let signature = match sig.try_into() {
702        Ok(sig_bytes) => match ml_dsa::Signature::<MlDsa65>::decode(&sig_bytes) {
703            Some(sig) => sig,
704            None => return false,
705        },
706        Err(_) => return false,
707    };
708
709    let public_key = match pk.try_into() {
710        Ok(pk_bytes) => ml_dsa::VerifyingKey::<MlDsa65>::decode(&pk_bytes),
711        Err(_) => return false,
712    };
713
714    public_key.verify(msg, &signature).is_ok()
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720
721    #[test]
722    fn test_identity_keypair_generation() {
723        let identity_keypair = IdentityKeyPair::generate();
724        let challenge = Challenge::new();
725        let response = challenge.sign(&identity_keypair);
726        assert!(response.verify(&challenge, &identity_keypair.identity()));
727    }
728
729    #[test]
730    fn test_encoding_roundtrip() {
731        let identity_keypair = IdentityKeyPair::generate();
732        let cose_bytes = identity_keypair.to_cose();
733        let decoded_keypair =
734            IdentityKeyPair::from_cose(&cose_bytes).expect("Decoding should succeed");
735
736        // Sign and verify to ensure keys match
737        let challenge = Challenge::new();
738        let response = challenge.sign(&decoded_keypair);
739        assert!(response.verify(&challenge, &decoded_keypair.identity()));
740    }
741
742    #[test]
743    fn test_challenge_response() {
744        let identity_keypair = IdentityKeyPair::generate();
745        let public_identity = identity_keypair.identity();
746        let challenge = Challenge::new();
747        let response = challenge.sign(&identity_keypair);
748        assert!(response.verify(&challenge, &public_identity));
749    }
750
751    #[test]
752    fn test_challenge_response_wrong_challenge() {
753        let identity_keypair = IdentityKeyPair::generate();
754        let public_identity = identity_keypair.identity();
755        let challenge1 = Challenge::new();
756        let challenge2 = Challenge::new();
757        let response = challenge1.sign(&identity_keypair);
758        assert!(!response.verify(&challenge2, &public_identity));
759    }
760
761    #[test]
762    fn test_challenge_response_wrong_identity() {
763        let identity_keypair1 = IdentityKeyPair::generate();
764        let identity_keypair2 = IdentityKeyPair::generate();
765        let challenge = Challenge::new();
766        let response = challenge.sign(&identity_keypair1);
767        assert!(!response.verify(&challenge, &identity_keypair2.identity()));
768    }
769
770    #[test]
771    fn test_ed25519_round_trip() {
772        let keypair = IdentityKeyPair::generate_with_algorithm(SignatureAlgorithm::Ed25519);
773        let challenge = Challenge::new();
774        let response = challenge.sign(&keypair);
775        assert!(response.verify(&challenge, &keypair.identity()));
776    }
777
778    #[cfg(feature = "experimental-post-quantum-crypto")]
779    #[test]
780    fn test_ml_dsa_round_trip() {
781        let keypair = IdentityKeyPair::generate_with_algorithm(SignatureAlgorithm::MlDsa65);
782        let challenge = Challenge::new();
783        let response = challenge.sign(&keypair);
784        assert!(response.verify(&challenge, &keypair.identity()));
785    }
786
787    #[test]
788    fn test_cose_algorithm_detection() {
789        let ed25519_keypair = IdentityKeyPair::generate_with_algorithm(SignatureAlgorithm::Ed25519);
790        #[cfg(feature = "experimental-post-quantum-crypto")]
791        let ml_dsa_keypair = IdentityKeyPair::generate_with_algorithm(SignatureAlgorithm::MlDsa65);
792
793        assert_eq!(
794            ed25519_keypair.identity().algorithm(),
795            Some(SignatureAlgorithm::Ed25519)
796        );
797        #[cfg(feature = "experimental-post-quantum-crypto")]
798        assert_eq!(
799            ml_dsa_keypair.identity().algorithm(),
800            Some(SignatureAlgorithm::MlDsa65)
801        );
802    }
803}