1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum SignatureAlgorithm {
21 Ed25519,
23 #[cfg(feature = "experimental-post-quantum-crypto")]
24 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#[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 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 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::from_seed(&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 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 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 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 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::from_seed(&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 pub fn identity(&self) -> Identity {
250 Identity::from(self)
251 }
252}
253
254#[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 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 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 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 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 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#[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 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 pub fn to_hex(&self) -> String {
460 hex::encode(self.0)
461 }
462}
463
464#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct ChallengeResponse {
622 cose_sign1_bytes: Vec<u8>,
623}
624
625impl ChallengeResponse {
626 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 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 let identity_alg = match identity.algorithm() {
681 Some(alg) => alg,
682 None => return false,
683 };
684
685 if sig_alg != identity_alg {
687 return false;
688 }
689
690 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 let pk_bytes = match identity.public_key_bytes() {
701 Some(bytes) => bytes,
702 None => return false,
703 };
704
705 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 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}