1use crate::constants::*;
15use crate::error::{Error, Result};
16use crate::keys::PublicKeyBundle;
17use alloc::string::String;
18use alloc::vec::Vec;
19
20use ed25519_dalek::pkcs8::{DecodePublicKey as _, EncodePublicKey as _};
21use ml_dsa::{KeyExport as _, MlDsa87};
22use ml_kem::pkcs8::{DecodePublicKey as _, EncodePublicKey as _};
23use ml_kem::EncapsulationKey1024;
24
25const X25519_PEM_LABEL: &str = "X25519 PUBLIC KEY";
26
27impl PublicKeyBundle {
28 #[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
35 pub fn to_pem(&self) -> Result<String> {
36 let x25519 = pem_rfc7468::encode_string(
37 X25519_PEM_LABEL,
38 pem_rfc7468::LineEnding::LF,
39 self.x25519.as_bytes(),
40 )
41 .map_err(|_| Error::InvalidKey)?;
42
43 let mlkem = self
44 .mlkem
45 .to_public_key_pem(Default::default())
46 .map_err(|_| Error::InvalidKey)?;
47 let mldsa = self
48 .mldsa
49 .to_public_key_pem(Default::default())
50 .map_err(|_| Error::InvalidKey)?;
51 let ed25519 = self
52 .ed25519
53 .to_public_key_pem(Default::default())
54 .map_err(|_| Error::InvalidKey)?;
55
56 let mut out =
57 String::with_capacity(x25519.len() + mlkem.len() + mldsa.len() + ed25519.len());
58 out.push_str(&x25519);
59 out.push_str(&mlkem);
60 out.push_str(&mldsa);
61 out.push_str(&ed25519);
62 Ok(out)
63 }
64
65 #[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
76 pub fn from_pem(pem: &str) -> Result<Self> {
77 let blocks = split_pem_blocks(pem);
78 if blocks.len() != 4 {
79 return Err(Error::InvalidKey);
80 }
81
82 let (label, x25519_der) =
84 pem_rfc7468::decode_vec(blocks[0].as_bytes()).map_err(|_| Error::InvalidKey)?;
85 if label != X25519_PEM_LABEL || x25519_der.len() != X25519_PK_LEN {
86 return Err(Error::InvalidKey);
87 }
88
89 let mlkem =
91 EncapsulationKey1024::from_public_key_pem(&blocks[1]).map_err(|_| Error::InvalidKey)?;
92 let mldsa = ml_dsa::VerifyingKey::<MlDsa87>::from_public_key_pem(&blocks[2])
93 .map_err(|_| Error::InvalidKey)?;
94 let ed25519 = ed25519_dalek::VerifyingKey::from_public_key_pem(&blocks[3])
95 .map_err(|_| Error::InvalidKey)?;
96
97 let mut bytes = Vec::with_capacity(PUBLIC_BUNDLE_LEN);
100 bytes.extend_from_slice(&MAGIC_PUBLIC_BUNDLE);
101 bytes.push(WIRE_VERSION);
102 bytes.push(SUITE_ID);
103 bytes.extend_from_slice(&x25519_der);
104 bytes.extend_from_slice(&mlkem.to_bytes());
105 bytes.extend_from_slice(&ed25519.to_bytes());
106 bytes.extend_from_slice(&mldsa.encode());
107 Self::from_bytes(&bytes)
108 }
109}
110
111fn split_pem_blocks(pem: &str) -> Vec<String> {
114 let mut blocks = Vec::new();
115 let mut current = String::new();
116 for line in pem.lines() {
117 let trimmed = line.trim();
118 if trimmed.is_empty() && current.is_empty() {
119 continue;
120 }
121 current.push_str(trimmed);
122 current.push('\n');
123 if trimmed.starts_with("-----END") {
124 blocks.push(core::mem::take(&mut current));
125 }
126 }
127 blocks
128}
129
130#[cfg(test)]
131mod tests {
132 use crate::HybridCrypto;
133 use crate::PublicKeyBundle;
134
135 #[test]
136 fn pem_roundtrip() {
137 let kp = HybridCrypto::generate().unwrap();
138 let pem = kp.public_keys().to_pem().unwrap();
139 assert!(pem.contains("BEGIN X25519 PUBLIC KEY"));
140 assert!(pem.contains("BEGIN PUBLIC KEY"));
141 let parsed = PublicKeyBundle::from_pem(&pem).unwrap();
142 assert_eq!(&parsed, kp.public_keys());
143 }
144
145 #[test]
146 fn rejects_wrong_block_count() {
147 let kp = HybridCrypto::generate().unwrap();
148 let pem = kp.public_keys().to_pem().unwrap();
149 let cut = pem.rfind("-----BEGIN PUBLIC KEY").unwrap();
151 assert!(PublicKeyBundle::from_pem(&pem[..cut]).is_err());
152 }
153
154 #[test]
155 fn rejects_reordered_blocks() {
156 let kp = HybridCrypto::generate().unwrap();
157 let pem = kp.public_keys().to_pem().unwrap();
158 let blocks = super::split_pem_blocks(&pem);
159 let reordered = alloc::format!("{}{}{}{}", blocks[0], blocks[3], blocks[2], blocks[1]);
161 assert!(PublicKeyBundle::from_pem(&reordered).is_err());
162 }
163
164 #[test]
165 fn tampered_x25519_yields_different_key() {
166 use alloc::string::String;
167 let kp = HybridCrypto::generate().unwrap();
168 let pem = kp.public_keys().to_pem().unwrap();
169 let body = pem.find('\n').unwrap() + 1;
173 let tampered: String = pem
174 .char_indices()
175 .map(|(i, c)| if i == body { flip(c) } else { c })
176 .collect();
177 if let Ok(parsed) = PublicKeyBundle::from_pem(&tampered) {
180 assert_ne!(&parsed, kp.public_keys());
181 }
182 }
183
184 fn flip(c: char) -> char {
185 match c {
186 'A'..='Y' | 'a'..='y' | '0'..='8' => ((c as u8) + 1) as char,
187 _ => 'A',
188 }
189 }
190}