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::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 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::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 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
426#[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct ChallengeResponse {
584 cose_sign1_bytes: Vec<u8>,
585}
586
587impl ChallengeResponse {
588 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 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 let identity_alg = match identity.algorithm() {
643 Some(alg) => alg,
644 None => return false,
645 };
646
647 if sig_alg != identity_alg {
649 return false;
650 }
651
652 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 let pk_bytes = match identity.public_key_bytes() {
663 Some(bytes) => bytes,
664 None => return false,
665 };
666
667 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 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}