stem_rs/descriptor/certificate.rs
1//! Ed25519 certificate parsing for Tor descriptors.
2//!
3//! This module provides parsing for [Ed25519 certificates] used throughout the Tor
4//! network for cryptographic identity and signing key validation. These certificates
5//! are a fundamental building block of Tor's identity system, enabling:
6//!
7//! - Validating signing keys of server descriptors
8//! - Validating signing keys of hidden service v3 descriptors
9//! - Signing and encrypting hidden service v3 introduction points
10//! - Cross-certifying relay identity keys
11//!
12//! # Certificate Structure
13//!
14//! Ed25519 certificates follow the format specified in [cert-spec.txt]. Each
15//! certificate contains:
16//!
17//! - A version number (currently only version 1 is supported)
18//! - A certificate type indicating its purpose
19//! - An expiration time (in hours since Unix epoch)
20//! - A certified key (32 bytes)
21//! - Optional extensions (e.g., the signing key)
22//! - A signature over the certificate body
23//!
24//! # Security Considerations
25//!
26//! - Always check [`Ed25519Certificate::is_expired`] before trusting a certificate
27//! - Certificate validation requires the `cryptography` feature for signature verification
28//! - The signing key may be embedded in an extension or provided externally
29//! - Certificates with unknown types are rejected to prevent security issues
30//!
31//! # Example
32//!
33//! ```rust
34//! use stem_rs::descriptor::certificate::Ed25519Certificate;
35//!
36//! let cert_pem = r#"-----BEGIN ED25519 CERT-----
37//! AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR
38//! ptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8sGG8lTjx1
39//! g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98Ljhdp2w4=
40//! -----END ED25519 CERT-----"#;
41//!
42//! let cert = Ed25519Certificate::from_base64(cert_pem).unwrap();
43//! println!("Certificate type: {:?}", cert.cert_type);
44//! println!("Expires: {}", cert.expiration);
45//! println!("Is expired: {}", cert.is_expired());
46//!
47//! // Extract signing key if present
48//! if let Some(signing_key) = cert.signing_key() {
49//! println!("Signing key: {} bytes", signing_key.len());
50//! }
51//! ```
52//!
53//! # See Also
54//!
55//! - [`crate::descriptor::server`] - Server descriptors that contain Ed25519 certificates
56//! - [`crate::descriptor::hidden`] - Hidden service descriptors using Ed25519 certificates
57//! - [`crate::client::datatype`] - Low-level certificate types for ORPort communication
58//!
59//! [Ed25519 certificates]: https://gitweb.torproject.org/torspec.git/tree/cert-spec.txt
60//! [cert-spec.txt]: https://gitweb.torproject.org/torspec.git/tree/cert-spec.txt
61
62use chrono::{DateTime, TimeZone, Utc};
63use std::fmt;
64
65use crate::client::datatype::{CertType, Size};
66use crate::Error;
67
68/// Length of an Ed25519 public key in bytes.
69///
70/// Ed25519 keys are always exactly 32 bytes (256 bits).
71pub const ED25519_KEY_LENGTH: usize = 32;
72
73/// Length of the Ed25519 certificate header in bytes.
74///
75/// The header contains: version (1) + type (1) + expiration (4) + key_type (1) +
76/// key (32) + extension_count (1) = 40 bytes.
77pub const ED25519_HEADER_LENGTH: usize = 40;
78
79/// Length of an Ed25519 signature in bytes.
80///
81/// Ed25519 signatures are always exactly 64 bytes (512 bits).
82pub const ED25519_SIGNATURE_LENGTH: usize = 64;
83
84/// Types of extensions that can appear in an Ed25519 certificate.
85///
86/// Extensions provide additional data within a certificate, such as the
87/// signing key used to create the certificate.
88///
89/// # Stability
90///
91/// This enum is non-exhaustive. New extension types may be added in future
92/// Tor protocol versions.
93///
94/// # Example
95///
96/// ```rust
97/// use stem_rs::descriptor::certificate::ExtensionType;
98///
99/// let ext_type = ExtensionType::from_int(4);
100/// assert_eq!(ext_type, ExtensionType::HasSigningKey);
101///
102/// let unknown = ExtensionType::from_int(99);
103/// assert_eq!(unknown, ExtensionType::Unknown);
104/// ```
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub enum ExtensionType {
107 /// Extension contains the 32-byte Ed25519 public key used to sign this certificate.
108 ///
109 /// When present, this extension's data field contains the signing key that
110 /// can be used to verify the certificate's signature.
111 HasSigningKey = 4,
112
113 /// An unrecognized extension type.
114 ///
115 /// Extensions with unknown types are preserved but their semantics are not
116 /// interpreted. If the extension has the `AffectsValidation` flag set,
117 /// the certificate should be considered invalid.
118 Unknown,
119}
120
121impl ExtensionType {
122 /// Converts an integer value to an [`ExtensionType`].
123 ///
124 /// # Arguments
125 ///
126 /// * `val` - The integer extension type value from the certificate
127 ///
128 /// # Returns
129 ///
130 /// The corresponding [`ExtensionType`], or [`ExtensionType::Unknown`] if
131 /// the value is not recognized.
132 ///
133 /// # Example
134 ///
135 /// ```rust
136 /// use stem_rs::descriptor::certificate::ExtensionType;
137 ///
138 /// assert_eq!(ExtensionType::from_int(4), ExtensionType::HasSigningKey);
139 /// assert_eq!(ExtensionType::from_int(0), ExtensionType::Unknown);
140 /// ```
141 pub fn from_int(val: u8) -> Self {
142 match val {
143 4 => ExtensionType::HasSigningKey,
144 _ => ExtensionType::Unknown,
145 }
146 }
147
148 /// Returns the integer value of this extension type.
149 ///
150 /// # Returns
151 ///
152 /// The integer representation of this extension type, or 0 for unknown types.
153 ///
154 /// # Example
155 ///
156 /// ```rust
157 /// use stem_rs::descriptor::certificate::ExtensionType;
158 ///
159 /// assert_eq!(ExtensionType::HasSigningKey.value(), 4);
160 /// ```
161 pub fn value(&self) -> u8 {
162 match self {
163 ExtensionType::HasSigningKey => 4,
164 ExtensionType::Unknown => 0,
165 }
166 }
167}
168
169/// Flags that can be assigned to Ed25519 certificate extensions.
170///
171/// These flags modify how an extension should be interpreted during
172/// certificate validation.
173///
174/// # Example
175///
176/// ```rust
177/// use stem_rs::descriptor::certificate::ExtensionFlag;
178///
179/// // Check if an extension affects validation
180/// let flags = vec![ExtensionFlag::AffectsValidation];
181/// if flags.contains(&ExtensionFlag::AffectsValidation) {
182/// println!("This extension must be understood for validation");
183/// }
184/// ```
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186pub enum ExtensionFlag {
187 /// Indicates that this extension affects whether the certificate is valid.
188 ///
189 /// If an extension has this flag set and the extension type is not
190 /// understood, the certificate MUST be considered invalid. This ensures
191 /// forward compatibility - new critical extensions won't be silently ignored.
192 AffectsValidation,
193
194 /// Indicates that the extension contains flags not recognized by this parser.
195 ///
196 /// This flag is set when the extension's flag byte contains bits that
197 /// are not part of the known flag set.
198 Unknown,
199}
200
201/// An extension within an Ed25519 certificate.
202///
203/// Extensions provide additional data within a certificate. The most common
204/// extension type is [`ExtensionType::HasSigningKey`], which embeds the
205/// public key used to sign the certificate.
206///
207/// # Structure
208///
209/// Each extension consists of:
210/// - A 2-byte length field (big-endian)
211/// - A 1-byte extension type
212/// - A 1-byte flags field
213/// - Variable-length data
214///
215/// # Flags
216///
217/// The flags field is a bitmask:
218/// - Bit 0 (0x01): [`ExtensionFlag::AffectsValidation`] - Extension is critical
219/// - Other bits: Reserved, set [`ExtensionFlag::Unknown`] if present
220///
221/// # Example
222///
223/// ```rust
224/// use stem_rs::descriptor::certificate::{Ed25519Extension, ExtensionType, ExtensionFlag};
225///
226/// // Create a signing key extension
227/// let signing_key = vec![0u8; 32]; // 32-byte Ed25519 public key
228/// let ext = Ed25519Extension::new(4, 0, signing_key).unwrap();
229///
230/// assert_eq!(ext.ext_type, ExtensionType::HasSigningKey);
231/// assert!(ext.flags.is_empty()); // No flags set
232/// ```
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct Ed25519Extension {
235 /// The parsed extension type.
236 ///
237 /// This is the semantic interpretation of [`type_int`](Self::type_int).
238 pub ext_type: ExtensionType,
239
240 /// The raw integer value of the extension type.
241 ///
242 /// Preserved for round-trip encoding of unknown extension types.
243 pub type_int: u8,
244
245 /// Flags associated with this extension.
246 ///
247 /// See [`ExtensionFlag`] for the meaning of each flag.
248 pub flags: Vec<ExtensionFlag>,
249
250 /// The raw integer value of the flags byte.
251 ///
252 /// Preserved for round-trip encoding.
253 pub flag_int: u8,
254
255 /// The extension's data payload.
256 ///
257 /// For [`ExtensionType::HasSigningKey`], this is a 32-byte Ed25519 public key.
258 pub data: Vec<u8>,
259}
260
261impl Ed25519Extension {
262 /// Creates a new Ed25519 certificate extension.
263 ///
264 /// # Arguments
265 ///
266 /// * `ext_type` - The extension type as an integer
267 /// * `flag_val` - The flags byte
268 /// * `data` - The extension's data payload
269 ///
270 /// # Returns
271 ///
272 /// A new [`Ed25519Extension`] on success.
273 ///
274 /// # Errors
275 ///
276 /// Returns [`Error::Parse`] if:
277 /// - The extension type is [`ExtensionType::HasSigningKey`] but the data
278 /// is not exactly 32 bytes
279 ///
280 /// # Example
281 ///
282 /// ```rust
283 /// use stem_rs::descriptor::certificate::Ed25519Extension;
284 ///
285 /// // Create a signing key extension (type 4)
286 /// let key_data = vec![0u8; 32];
287 /// let ext = Ed25519Extension::new(4, 0, key_data).unwrap();
288 ///
289 /// // Invalid: signing key must be 32 bytes
290 /// let result = Ed25519Extension::new(4, 0, vec![0u8; 16]);
291 /// assert!(result.is_err());
292 /// ```
293 pub fn new(ext_type: u8, flag_val: u8, data: Vec<u8>) -> Result<Self, Error> {
294 let extension_type = ExtensionType::from_int(ext_type);
295 let mut flags = Vec::new();
296 let mut remaining_flags = flag_val;
297
298 if remaining_flags % 2 == 1 {
299 flags.push(ExtensionFlag::AffectsValidation);
300 remaining_flags -= 1;
301 }
302
303 if remaining_flags != 0 {
304 flags.push(ExtensionFlag::Unknown);
305 }
306
307 if extension_type == ExtensionType::HasSigningKey && data.len() != 32 {
308 return Err(Error::Parse {
309 location: "Ed25519Extension".to_string(),
310 reason: format!(
311 "Ed25519 HAS_SIGNING_KEY extension must be 32 bytes, but was {}",
312 data.len()
313 ),
314 });
315 }
316
317 Ok(Ed25519Extension {
318 ext_type: extension_type,
319 type_int: ext_type,
320 flags,
321 flag_int: flag_val,
322 data,
323 })
324 }
325
326 /// Encodes this extension to its binary representation.
327 ///
328 /// The encoded format is:
329 /// - 2 bytes: data length (big-endian)
330 /// - 1 byte: extension type
331 /// - 1 byte: flags
332 /// - N bytes: data
333 ///
334 /// # Returns
335 ///
336 /// A byte vector containing the encoded extension.
337 ///
338 /// # Example
339 ///
340 /// ```rust
341 /// use stem_rs::descriptor::certificate::Ed25519Extension;
342 ///
343 /// let ext = Ed25519Extension::new(4, 0, vec![0u8; 32]).unwrap();
344 /// let packed = ext.pack();
345 ///
346 /// // 2 (length) + 1 (type) + 1 (flags) + 32 (data) = 36 bytes
347 /// assert_eq!(packed.len(), 36);
348 /// ```
349 pub fn pack(&self) -> Vec<u8> {
350 let mut encoded = Vec::new();
351 encoded.extend_from_slice(&Size::Short.pack(self.data.len() as u64));
352 encoded.push(self.type_int);
353 encoded.push(self.flag_int);
354 encoded.extend_from_slice(&self.data);
355 encoded
356 }
357
358 /// Parses an extension from the beginning of a byte slice.
359 ///
360 /// This method reads one extension from the input and returns both the
361 /// parsed extension and the remaining unparsed bytes.
362 ///
363 /// # Arguments
364 ///
365 /// * `content` - The byte slice to parse from
366 ///
367 /// # Returns
368 ///
369 /// A tuple of (parsed extension, remaining bytes) on success.
370 ///
371 /// # Errors
372 ///
373 /// Returns [`Error::Parse`] if:
374 /// - The input is too short to contain the extension header (< 4 bytes)
375 /// - The input is truncated (data length exceeds available bytes)
376 /// - The extension data is invalid (e.g., wrong size for signing key)
377 ///
378 /// # Example
379 ///
380 /// ```rust
381 /// use stem_rs::descriptor::certificate::Ed25519Extension;
382 ///
383 /// // Extension: length=2, type=5, flags=0, data=[0x11, 0x22]
384 /// let data = [0x00, 0x02, 0x05, 0x00, 0x11, 0x22, 0xFF];
385 /// let (ext, remaining) = Ed25519Extension::pop(&data).unwrap();
386 ///
387 /// assert_eq!(ext.type_int, 5);
388 /// assert_eq!(ext.data, vec![0x11, 0x22]);
389 /// assert_eq!(remaining, &[0xFF]); // Remaining byte
390 /// ```
391 pub fn pop(content: &[u8]) -> Result<(Self, &[u8]), Error> {
392 if content.len() < 4 {
393 return Err(Error::Parse {
394 location: "Ed25519Extension".to_string(),
395 reason: "Ed25519 extension is missing header fields".to_string(),
396 });
397 }
398
399 let (data_size, content) = Size::Short.pop(content)?;
400 let data_size = data_size as usize;
401 let (ext_type, content) = (content[0], &content[1..]);
402 let (flags, content) = (content[0], &content[1..]);
403
404 if content.len() < data_size {
405 return Err(Error::Parse {
406 location: "Ed25519Extension".to_string(),
407 reason: format!(
408 "Ed25519 extension is truncated. It should have {} bytes of data but there's only {}",
409 data_size,
410 content.len()
411 ),
412 });
413 }
414
415 let (data, content) = content.split_at(data_size);
416 let extension = Ed25519Extension::new(ext_type, flags, data.to_vec())?;
417
418 Ok((extension, content))
419 }
420}
421
422/// A version 1 Ed25519 certificate used in Tor descriptors.
423///
424/// Ed25519 certificates are used throughout Tor to bind Ed25519 keys to
425/// identities and validate signatures on descriptors. They are found in:
426///
427/// - Server descriptors (signing key certificates)
428/// - Hidden service v3 descriptors (blinded key certificates)
429/// - Introduction point authentication
430///
431/// # Certificate Types
432///
433/// The certificate type indicates its purpose. Common types include:
434///
435/// | Type | Name | Purpose |
436/// |------|------|---------|
437/// | 4 | Ed25519 Signing | Signs server descriptors |
438/// | 5 | Link Auth | TLS link authentication |
439/// | 6 | Ed25519 Auth | Ed25519 authentication |
440/// | 8 | Short-term Signing | Short-term descriptor signing |
441/// | 9 | Intro Point Auth | HS introduction point auth |
442/// | 11 | Ntor Onion Key | Ntor key cross-certification |
443///
444/// # Invariants
445///
446/// - Version is always 1 (only supported version)
447/// - Key is always exactly 32 bytes
448/// - Signature is always exactly 64 bytes
449/// - Certificate types 1, 2, 3, and 7 are reserved and rejected
450///
451/// # Security Considerations
452///
453/// - Always verify [`is_expired`](Self::is_expired) before trusting a certificate
454/// - The signature should be verified against the signing key
455/// - Unknown certificate types are rejected for security
456/// - Extensions with [`ExtensionFlag::AffectsValidation`] must be understood
457///
458/// # Example
459///
460/// ```rust
461/// use stem_rs::descriptor::certificate::Ed25519Certificate;
462///
463/// let cert_b64 = "AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABn\
464/// prVRptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8s\
465/// GG8lTjx1g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98L\
466/// jhdp2w4=";
467///
468/// let cert = Ed25519Certificate::from_base64(cert_b64).unwrap();
469///
470/// // Check certificate properties
471/// assert_eq!(cert.version, 1);
472/// println!("Type: {:?}", cert.cert_type);
473/// println!("Expires: {}", cert.expiration);
474///
475/// // Check if expired
476/// if cert.is_expired() {
477/// println!("Certificate has expired!");
478/// }
479///
480/// // Get signing key if present
481/// if let Some(key) = cert.signing_key() {
482/// println!("Signing key: {} bytes", key.len());
483/// }
484/// ```
485///
486/// # See Also
487///
488/// - [`Ed25519Extension`] - Extensions within certificates
489/// - [`CertType`](crate::client::datatype::CertType) - Certificate type enumeration
490/// - [cert-spec.txt](https://gitweb.torproject.org/torspec.git/tree/cert-spec.txt) - Tor specification
491#[derive(Debug, Clone, PartialEq, Eq)]
492pub struct Ed25519Certificate {
493 /// Certificate format version.
494 ///
495 /// Currently only version 1 is supported. Future versions may have
496 /// different structures.
497 pub version: u8,
498
499 /// The parsed certificate type.
500 ///
501 /// Indicates the purpose of this certificate. See [`CertType`](crate::client::datatype::CertType)
502 /// for the full enumeration.
503 pub cert_type: CertType,
504
505 /// The raw integer value of the certificate type.
506 ///
507 /// Preserved for round-trip encoding and debugging.
508 pub type_int: u8,
509
510 /// When this certificate expires.
511 ///
512 /// Certificates should not be trusted after this time. Use [`is_expired`](Self::is_expired)
513 /// to check validity.
514 ///
515 /// # Note
516 ///
517 /// The expiration is stored in the certificate as hours since Unix epoch,
518 /// so the precision is limited to one hour.
519 pub expiration: DateTime<Utc>,
520
521 /// The key type (always 1 for Ed25519).
522 ///
523 /// This field indicates the type of key in the [`key`](Self::key) field.
524 /// Currently only type 1 (Ed25519) is defined.
525 pub key_type: u8,
526
527 /// The certified Ed25519 public key.
528 ///
529 /// This is the key being certified by this certificate. Its meaning
530 /// depends on the certificate type.
531 pub key: [u8; ED25519_KEY_LENGTH],
532
533 /// Extensions included in this certificate.
534 ///
535 /// Extensions provide additional data such as the signing key.
536 /// See [`Ed25519Extension`] for details.
537 pub extensions: Vec<Ed25519Extension>,
538
539 /// The Ed25519 signature over the certificate body.
540 ///
541 /// This signature covers all certificate data except the signature itself.
542 /// It should be verified using the signing key (from an extension or
543 /// provided externally).
544 pub signature: [u8; ED25519_SIGNATURE_LENGTH],
545}
546
547impl Ed25519Certificate {
548 /// Parses an Ed25519 certificate from its binary representation.
549 ///
550 /// This method decodes a certificate from raw bytes as they appear in
551 /// descriptors after base64 decoding.
552 ///
553 /// # Arguments
554 ///
555 /// * `content` - The raw certificate bytes
556 ///
557 /// # Returns
558 ///
559 /// The parsed [`Ed25519Certificate`] on success.
560 ///
561 /// # Errors
562 ///
563 /// Returns [`Error::Parse`] if:
564 /// - The input is too short (minimum 104 bytes: 40 header + 64 signature)
565 /// - The version is not 1
566 /// - The certificate type is reserved (1, 2, 3, 7) or unknown (0)
567 /// - The expiration timestamp is invalid
568 /// - Extension parsing fails
569 /// - There is unused data after parsing extensions
570 ///
571 /// # Example
572 ///
573 /// ```rust
574 /// use stem_rs::descriptor::certificate::Ed25519Certificate;
575 ///
576 /// // Typically you'd get these bytes from base64 decoding
577 /// // let cert = Ed25519Certificate::unpack(&decoded_bytes)?;
578 /// ```
579 pub fn unpack(content: &[u8]) -> Result<Self, Error> {
580 if content.len() < ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH {
581 return Err(Error::Parse {
582 location: "Ed25519Certificate".to_string(),
583 reason: format!(
584 "Ed25519 certificate was {} bytes, but should be at least {}",
585 content.len(),
586 ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH
587 ),
588 });
589 }
590
591 let (header, signature_bytes) = content.split_at(content.len() - ED25519_SIGNATURE_LENGTH);
592
593 let (version, header) = Size::Char.pop(header)?;
594 let version = version as u8;
595
596 if version != 1 {
597 return Err(Error::Parse {
598 location: "Ed25519Certificate".to_string(),
599 reason: format!(
600 "Ed25519 certificate is version {}. Parser presently only supports version 1",
601 version
602 ),
603 });
604 }
605
606 let (cert_type_int, header) = Size::Char.pop(header)?;
607 let cert_type_int = cert_type_int as u8;
608 let (cert_type, _) = CertType::get(cert_type_int);
609
610 Self::validate_cert_type(cert_type, cert_type_int)?;
611
612 let (expiration_hours, header) = Size::Long.pop(header)?;
613 let expiration = Utc
614 .timestamp_opt((expiration_hours * 3600) as i64, 0)
615 .single()
616 .ok_or_else(|| Error::Parse {
617 location: "Ed25519Certificate".to_string(),
618 reason: "Invalid expiration timestamp".to_string(),
619 })?;
620
621 let (key_type, header) = Size::Char.pop(header)?;
622 let key_type = key_type as u8;
623
624 let (key_bytes, header) = header.split_at(ED25519_KEY_LENGTH);
625 let mut key = [0u8; ED25519_KEY_LENGTH];
626 key.copy_from_slice(key_bytes);
627
628 let (extension_count, mut extension_data) = Size::Char.pop(header)?;
629 let extension_count = extension_count as usize;
630
631 let mut extensions = Vec::new();
632 for _ in 0..extension_count {
633 let (extension, remainder) = Ed25519Extension::pop(extension_data)?;
634 extensions.push(extension);
635 extension_data = remainder;
636 }
637
638 if !extension_data.is_empty() {
639 return Err(Error::Parse {
640 location: "Ed25519Certificate".to_string(),
641 reason: format!(
642 "Ed25519 certificate had {} bytes of unused extension data",
643 extension_data.len()
644 ),
645 });
646 }
647
648 let mut signature = [0u8; ED25519_SIGNATURE_LENGTH];
649 signature.copy_from_slice(signature_bytes);
650
651 Ok(Ed25519Certificate {
652 version,
653 cert_type,
654 type_int: cert_type_int,
655 expiration,
656 key_type,
657 key,
658 extensions,
659 signature,
660 })
661 }
662
663 /// Validates that the certificate type is allowed for Ed25519 certificates.
664 ///
665 /// Certain certificate types are reserved for other purposes (CERTS cells,
666 /// RSA cross-certification) and cannot be used in Ed25519 certificates.
667 fn validate_cert_type(cert_type: CertType, cert_type_int: u8) -> Result<(), Error> {
668 match cert_type {
669 CertType::Link | CertType::Identity | CertType::Authenticate => {
670 Err(Error::Parse {
671 location: "Ed25519Certificate".to_string(),
672 reason: format!(
673 "Ed25519 certificate cannot have a type of {}. This is reserved for CERTS cells",
674 cert_type_int
675 ),
676 })
677 }
678 CertType::Ed25519Identity => {
679 Err(Error::Parse {
680 location: "Ed25519Certificate".to_string(),
681 reason: "Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification".to_string(),
682 })
683 }
684 CertType::Unknown => {
685 Err(Error::Parse {
686 location: "Ed25519Certificate".to_string(),
687 reason: format!("Ed25519 certificate type {} is unrecognized", cert_type_int),
688 })
689 }
690 _ => Ok(()),
691 }
692 }
693
694 /// Parses an Ed25519 certificate from a base64-encoded string.
695 ///
696 /// This method handles both raw base64 and PEM-formatted certificates
697 /// (with `-----BEGIN ED25519 CERT-----` headers).
698 ///
699 /// # Arguments
700 ///
701 /// * `content` - The base64-encoded certificate string
702 ///
703 /// # Returns
704 ///
705 /// The parsed [`Ed25519Certificate`] on success.
706 ///
707 /// # Errors
708 ///
709 /// Returns [`Error::Parse`] if:
710 /// - The input is empty
711 /// - The base64 encoding is invalid
712 /// - The decoded certificate is malformed (see [`unpack`](Self::unpack))
713 ///
714 /// # Example
715 ///
716 /// ```rust
717 /// use stem_rs::descriptor::certificate::Ed25519Certificate;
718 ///
719 /// // Raw base64
720 /// let cert_b64 = "AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABn\
721 /// prVRptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8s\
722 /// GG8lTjx1g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98L\
723 /// jhdp2w4=";
724 /// let cert = Ed25519Certificate::from_base64(cert_b64).unwrap();
725 ///
726 /// // PEM format also works
727 /// let pem = format!(
728 /// "-----BEGIN ED25519 CERT-----\n{}\n-----END ED25519 CERT-----",
729 /// cert_b64
730 /// );
731 /// let cert2 = Ed25519Certificate::from_base64(&pem).unwrap();
732 /// ```
733 pub fn from_base64(content: &str) -> Result<Self, Error> {
734 let content = content.trim();
735
736 let content = if content.starts_with("-----BEGIN ED25519 CERT-----") {
737 content
738 .strip_prefix("-----BEGIN ED25519 CERT-----")
739 .and_then(|s| s.strip_suffix("-----END ED25519 CERT-----"))
740 .map(|s| s.trim())
741 .unwrap_or(content)
742 } else {
743 content
744 };
745
746 let content: String = content.chars().filter(|c| !c.is_whitespace()).collect();
747
748 if content.is_empty() {
749 return Err(Error::Parse {
750 location: "Ed25519Certificate".to_string(),
751 reason: "Ed25519 certificate wasn't properly base64 encoded (empty):".to_string(),
752 });
753 }
754
755 let decoded = base64_decode(&content).ok_or_else(|| Error::Parse {
756 location: "Ed25519Certificate".to_string(),
757 reason: format!(
758 "Ed25519 certificate wasn't properly base64 encoded (Incorrect padding):\n{}",
759 content
760 ),
761 })?;
762
763 Self::unpack(&decoded)
764 }
765
766 /// Encodes this certificate to its binary representation.
767 ///
768 /// The encoded format matches the Tor specification and can be decoded
769 /// with [`unpack`](Self::unpack).
770 ///
771 /// # Returns
772 ///
773 /// A byte vector containing the encoded certificate.
774 ///
775 /// # Example
776 ///
777 /// ```rust
778 /// use stem_rs::descriptor::certificate::Ed25519Certificate;
779 ///
780 /// let cert_b64 = "AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABn\
781 /// prVRptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8s\
782 /// GG8lTjx1g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98L\
783 /// jhdp2w4=";
784 /// let cert = Ed25519Certificate::from_base64(cert_b64).unwrap();
785 ///
786 /// let packed = cert.pack();
787 /// let reparsed = Ed25519Certificate::unpack(&packed).unwrap();
788 /// assert_eq!(cert, reparsed);
789 /// ```
790 pub fn pack(&self) -> Vec<u8> {
791 let mut encoded = Vec::new();
792 encoded.push(self.version);
793 encoded.push(self.type_int);
794 encoded.extend_from_slice(&Size::Long.pack((self.expiration.timestamp() / 3600) as u64));
795 encoded.push(self.key_type);
796 encoded.extend_from_slice(&self.key);
797 encoded.push(self.extensions.len() as u8);
798
799 for extension in &self.extensions {
800 encoded.extend_from_slice(&extension.pack());
801 }
802
803 encoded.extend_from_slice(&self.signature);
804 encoded
805 }
806
807 /// Encodes this certificate to a base64 string.
808 ///
809 /// The output is formatted with line breaks every 64 characters,
810 /// suitable for embedding in descriptors.
811 ///
812 /// # Returns
813 ///
814 /// A base64-encoded string representation of the certificate.
815 ///
816 /// # Example
817 ///
818 /// ```rust
819 /// use stem_rs::descriptor::certificate::Ed25519Certificate;
820 ///
821 /// let cert_b64 = "AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABn\
822 /// prVRptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8s\
823 /// GG8lTjx1g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98L\
824 /// jhdp2w4=";
825 /// let cert = Ed25519Certificate::from_base64(cert_b64).unwrap();
826 ///
827 /// let encoded = cert.to_base64();
828 /// // Can be decoded back
829 /// let decoded = Ed25519Certificate::from_base64(&encoded).unwrap();
830 /// ```
831 pub fn to_base64(&self) -> String {
832 let packed = self.pack();
833 let encoded = base64_encode(&packed);
834
835 encoded
836 .as_bytes()
837 .chunks(64)
838 .map(|chunk| std::str::from_utf8(chunk).unwrap_or(""))
839 .collect::<Vec<_>>()
840 .join("\n")
841 }
842
843 /// Encodes this certificate to a PEM-formatted string.
844 ///
845 /// The output includes `-----BEGIN ED25519 CERT-----` and
846 /// `-----END ED25519 CERT-----` headers.
847 ///
848 /// # Returns
849 ///
850 /// A PEM-formatted string representation of the certificate.
851 ///
852 /// # Example
853 ///
854 /// ```rust
855 /// use stem_rs::descriptor::certificate::Ed25519Certificate;
856 ///
857 /// let cert_b64 = "AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABn\
858 /// prVRptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8s\
859 /// GG8lTjx1g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98L\
860 /// jhdp2w4=";
861 /// let cert = Ed25519Certificate::from_base64(cert_b64).unwrap();
862 ///
863 /// let pem = cert.to_base64_pem();
864 /// assert!(pem.starts_with("-----BEGIN ED25519 CERT-----"));
865 /// assert!(pem.ends_with("-----END ED25519 CERT-----"));
866 /// ```
867 pub fn to_base64_pem(&self) -> String {
868 format!(
869 "-----BEGIN ED25519 CERT-----\n{}\n-----END ED25519 CERT-----",
870 self.to_base64()
871 )
872 }
873
874 /// Checks if this certificate has expired.
875 ///
876 /// A certificate is considered expired if the current time is past
877 /// the certificate's expiration time.
878 ///
879 /// # Returns
880 ///
881 /// `true` if the certificate has expired, `false` otherwise.
882 ///
883 /// # Security
884 ///
885 /// Always check expiration before trusting a certificate. Expired
886 /// certificates should not be used for validation, even if their
887 /// signatures are valid.
888 ///
889 /// # Example
890 ///
891 /// ```rust
892 /// use stem_rs::descriptor::certificate::Ed25519Certificate;
893 ///
894 /// let cert_b64 = "AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABn\
895 /// prVRptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8s\
896 /// GG8lTjx1g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98L\
897 /// jhdp2w4=";
898 /// let cert = Ed25519Certificate::from_base64(cert_b64).unwrap();
899 ///
900 /// if cert.is_expired() {
901 /// println!("Certificate expired on {}", cert.expiration);
902 /// }
903 /// ```
904 pub fn is_expired(&self) -> bool {
905 Utc::now() > self.expiration
906 }
907
908 /// Extracts the signing key from this certificate's extensions.
909 ///
910 /// The signing key is the Ed25519 public key used to sign this certificate.
911 /// It is typically embedded in an extension of type [`ExtensionType::HasSigningKey`].
912 ///
913 /// # Returns
914 ///
915 /// - `Some(&[u8])` - A reference to the 32-byte signing key if present
916 /// - `None` - If no signing key extension exists
917 ///
918 /// # Security
919 ///
920 /// The signing key should be used to verify the certificate's signature.
921 /// If no signing key is embedded, it must be obtained from another source
922 /// (e.g., the descriptor's master key).
923 ///
924 /// # Example
925 ///
926 /// ```rust
927 /// use stem_rs::descriptor::certificate::Ed25519Certificate;
928 ///
929 /// let cert_b64 = "AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABn\
930 /// prVRptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8s\
931 /// GG8lTjx1g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98L\
932 /// jhdp2w4=";
933 /// let cert = Ed25519Certificate::from_base64(cert_b64).unwrap();
934 ///
935 /// match cert.signing_key() {
936 /// Some(key) => println!("Signing key: {} bytes", key.len()),
937 /// None => println!("No embedded signing key"),
938 /// }
939 /// ```
940 pub fn signing_key(&self) -> Option<&[u8]> {
941 for extension in &self.extensions {
942 if extension.ext_type == ExtensionType::HasSigningKey {
943 return Some(&extension.data);
944 }
945 }
946 None
947 }
948}
949
950impl fmt::Display for Ed25519Certificate {
951 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
952 write!(f, "{}", self.to_base64_pem())
953 }
954}
955
956/// Decodes a base64-encoded string to bytes.
957///
958/// This is a simple base64 decoder that handles standard base64 alphabet
959/// (A-Z, a-z, 0-9, +, /) with optional padding.
960///
961/// # Arguments
962///
963/// * `input` - The base64-encoded string (padding characters are optional)
964///
965/// # Returns
966///
967/// - `Some(Vec<u8>)` - The decoded bytes on success
968/// - `None` - If the input contains invalid base64 characters
969fn base64_decode(input: &str) -> Option<Vec<u8>> {
970 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
971
972 let input = input.trim_end_matches('=');
973 let mut result = Vec::new();
974 let mut buffer: u32 = 0;
975 let mut bits: u32 = 0;
976
977 for c in input.chars() {
978 let value = ALPHABET.iter().position(|&x| x == c as u8)? as u32;
979 buffer = (buffer << 6) | value;
980 bits += 6;
981
982 if bits >= 8 {
983 bits -= 8;
984 result.push((buffer >> bits) as u8);
985 buffer &= (1 << bits) - 1;
986 }
987 }
988
989 Some(result)
990}
991
992/// Encodes bytes to a base64 string.
993///
994/// This is a simple base64 encoder that produces standard base64 output
995/// with padding.
996///
997/// # Arguments
998///
999/// * `bytes` - The bytes to encode
1000///
1001/// # Returns
1002///
1003/// A base64-encoded string with appropriate padding.
1004fn base64_encode(bytes: &[u8]) -> String {
1005 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1006 let mut result = String::new();
1007 let mut i = 0;
1008
1009 while i < bytes.len() {
1010 let b0 = bytes[i] as u32;
1011 let b1 = bytes.get(i + 1).map(|&b| b as u32).unwrap_or(0);
1012 let b2 = bytes.get(i + 2).map(|&b| b as u32).unwrap_or(0);
1013 let triple = (b0 << 16) | (b1 << 8) | b2;
1014
1015 result.push(ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
1016 result.push(ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
1017
1018 if i + 1 < bytes.len() {
1019 result.push(ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
1020 } else {
1021 result.push('=');
1022 }
1023
1024 if i + 2 < bytes.len() {
1025 result.push(ALPHABET[(triple & 0x3F) as usize] as char);
1026 } else {
1027 result.push('=');
1028 }
1029
1030 i += 3;
1031 }
1032
1033 result
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use super::*;
1039
1040 const ED25519_CERT: &str = r#"
1041AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR
1042ptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8sGG8lTjx1
1043g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98Ljhdp2w4=
1044"#;
1045
1046 const EXPECTED_CERT_KEY: [u8; 32] = [
1047 0xa5, 0xb6, 0x1a, 0x80, 0x44, 0x0f, 0x52, 0x23, 0x63, 0x70, 0x3a, 0x7f, 0xa1, 0x8d, 0xa8,
1048 0x11, 0x25, 0xe4, 0x0f, 0x37, 0x7c, 0x3d, 0x99, 0x6b, 0xdb, 0xa9, 0x1a, 0x47, 0xb9, 0xd4,
1049 0x91, 0xaa,
1050 ];
1051
1052 const EXPECTED_EXTENSION_DATA: [u8; 32] = [
1053 0x67, 0xa6, 0xb5, 0x51, 0xa6, 0xd2, 0x2b, 0xe3, 0x76, 0xd6, 0x3e, 0x8d, 0x9f, 0x23, 0x3a,
1054 0x37, 0xb8, 0xec, 0xb0, 0x7e, 0x83, 0x2b, 0xaf, 0x2a, 0x6b, 0xa5, 0xb9, 0xb8, 0x1e, 0x10,
1055 0xa4, 0x64,
1056 ];
1057
1058 const EXPECTED_SIGNATURE: [u8; 64] = [
1059 0xc6, 0x8e, 0xd3, 0xae, 0x0b, 0x3f, 0xed, 0x4a, 0x36, 0xe2, 0xef, 0x95, 0xcf, 0x2c, 0x18,
1060 0x6f, 0x25, 0x4e, 0x3c, 0x75, 0x83, 0x89, 0x37, 0x10, 0xbb, 0x96, 0x62, 0x01, 0xd8, 0x59,
1061 0x4e, 0x6b, 0x02, 0x26, 0xbb, 0x9e, 0x5e, 0x20, 0x51, 0xf0, 0x59, 0x38, 0x47, 0xc7, 0x01,
1062 0xf2, 0x84, 0x4b, 0xb9, 0x77, 0x77, 0xad, 0xdd, 0x04, 0x48, 0xc4, 0x5f, 0xdf, 0x0b, 0x8e,
1063 0x17, 0x69, 0xdb, 0x0e,
1064 ];
1065
1066 fn create_test_certificate(
1067 version: u8,
1068 cert_type: u8,
1069 extension_data: Vec<Vec<u8>>,
1070 ) -> Vec<u8> {
1071 let mut cert = Vec::new();
1072 cert.push(version);
1073 cert.push(cert_type);
1074 cert.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
1075 cert.push(0x01);
1076 cert.extend_from_slice(&[0x03; 32]);
1077 cert.push(extension_data.len() as u8);
1078 for ext in extension_data {
1079 cert.extend_from_slice(&ext);
1080 }
1081 cert.extend_from_slice(&[0x01; ED25519_SIGNATURE_LENGTH]);
1082 cert
1083 }
1084
1085 fn encode_test_certificate(version: u8, cert_type: u8, extension_data: Vec<Vec<u8>>) -> String {
1086 let cert = create_test_certificate(version, cert_type, extension_data);
1087 base64_encode(&cert)
1088 }
1089
1090 #[test]
1091 fn test_basic_parsing() {
1092 let signing_key = vec![0x11u8; 32];
1093 let mut ext1 = vec![0x00, 0x20, 0x04, 0x07];
1094 ext1.extend_from_slice(&signing_key);
1095 let ext2 = vec![0x00, 0x00, 0x05, 0x04];
1096
1097 let cert_b64 = encode_test_certificate(1, 4, vec![ext1, ext2]);
1098 let cert = Ed25519Certificate::from_base64(&cert_b64).unwrap();
1099
1100 assert_eq!(1, cert.version);
1101 assert_eq!(CertType::Ed25519Signing, cert.cert_type);
1102 assert_eq!(Utc.timestamp_opt(0, 0).unwrap(), cert.expiration);
1103 assert_eq!(1, cert.key_type);
1104 assert_eq!([0x03u8; 32], cert.key);
1105 assert_eq!([0x01u8; 64], cert.signature);
1106 assert_eq!(2, cert.extensions.len());
1107
1108 assert_eq!(ExtensionType::HasSigningKey, cert.extensions[0].ext_type);
1109 assert_eq!(4, cert.extensions[0].type_int);
1110 assert_eq!(7, cert.extensions[0].flag_int);
1111 assert_eq!(signing_key, cert.extensions[0].data);
1112 assert!(cert.extensions[0]
1113 .flags
1114 .contains(&ExtensionFlag::AffectsValidation));
1115 assert!(cert.extensions[0].flags.contains(&ExtensionFlag::Unknown));
1116
1117 assert_eq!(ExtensionType::Unknown, cert.extensions[1].ext_type);
1118 assert_eq!(5, cert.extensions[1].type_int);
1119 assert!(cert.extensions[1].data.is_empty());
1120
1121 assert!(cert.is_expired());
1122 }
1123
1124 #[test]
1125 fn test_with_real_cert() {
1126 let cert = Ed25519Certificate::from_base64(ED25519_CERT).unwrap();
1127
1128 assert_eq!(1, cert.version);
1129 assert_eq!(CertType::Ed25519Signing, cert.cert_type);
1130 assert_eq!(
1131 Utc.with_ymd_and_hms(2015, 8, 28, 17, 0, 0).unwrap(),
1132 cert.expiration
1133 );
1134 assert_eq!(1, cert.key_type);
1135 assert_eq!(EXPECTED_CERT_KEY, cert.key);
1136 assert_eq!(1, cert.extensions.len());
1137 assert_eq!(ExtensionType::HasSigningKey, cert.extensions[0].ext_type);
1138 assert_eq!(EXPECTED_EXTENSION_DATA.to_vec(), cert.extensions[0].data);
1139 assert_eq!(EXPECTED_SIGNATURE, cert.signature);
1140 }
1141
1142 #[test]
1143 fn test_extension_encoding() {
1144 let cert = Ed25519Certificate::from_base64(ED25519_CERT).unwrap();
1145 let extension = &cert.extensions[0];
1146
1147 let mut expected = Vec::new();
1148 expected.extend_from_slice(&Size::Short.pack(EXPECTED_EXTENSION_DATA.len() as u64));
1149 expected.push(4);
1150 expected.push(0);
1151 expected.extend_from_slice(&EXPECTED_EXTENSION_DATA);
1152
1153 assert_eq!(4, extension.type_int);
1154 assert_eq!(0, extension.flag_int);
1155 assert_eq!(EXPECTED_EXTENSION_DATA.to_vec(), extension.data);
1156 assert_eq!(expected, extension.pack());
1157 }
1158
1159 #[test]
1160 fn test_certificate_encoding() {
1161 let cert = Ed25519Certificate::from_base64(ED25519_CERT).unwrap();
1162 let encoded = cert.to_base64();
1163 let expected: String = ED25519_CERT
1164 .trim()
1165 .chars()
1166 .filter(|c| !c.is_whitespace())
1167 .collect();
1168 let actual: String = encoded.chars().filter(|c| !c.is_whitespace()).collect();
1169 assert_eq!(expected, actual);
1170 }
1171
1172 #[test]
1173 fn test_non_base64() {
1174 let result = Ed25519Certificate::from_base64("\x02\x0323\x04");
1175 assert!(result.is_err());
1176 let err = result.unwrap_err();
1177 assert!(err.to_string().contains("base64"));
1178 }
1179
1180 #[test]
1181 fn test_too_short() {
1182 let result = Ed25519Certificate::from_base64("");
1183 assert!(result.is_err());
1184 assert!(result.unwrap_err().to_string().contains("empty"));
1185
1186 let result = Ed25519Certificate::from_base64("AQQABhtZAaW2GoBED1IjY3A6");
1187 assert!(result.is_err());
1188 let err = result.unwrap_err();
1189 assert!(err.to_string().contains("18 bytes"));
1190 assert!(err.to_string().contains("at least 104"));
1191 }
1192
1193 #[test]
1194 fn test_with_invalid_version() {
1195 let cert_b64 = encode_test_certificate(2, 4, vec![]);
1196 let result = Ed25519Certificate::from_base64(&cert_b64);
1197 assert!(result.is_err());
1198 let err = result.unwrap_err();
1199 assert!(err.to_string().contains("version 2"));
1200 assert!(err.to_string().contains("only supports version 1"));
1201 }
1202
1203 #[test]
1204 fn test_with_invalid_cert_type_zero() {
1205 let cert_b64 = encode_test_certificate(1, 0, vec![]);
1206 let result = Ed25519Certificate::from_base64(&cert_b64);
1207 assert!(result.is_err());
1208 let err = result.unwrap_err();
1209 assert!(err.to_string().contains("type 0"));
1210 assert!(err.to_string().contains("unrecognized"));
1211 }
1212
1213 #[test]
1214 fn test_with_invalid_cert_type_reserved() {
1215 let cert_b64 = encode_test_certificate(1, 1, vec![]);
1216 let result = Ed25519Certificate::from_base64(&cert_b64);
1217 assert!(result.is_err());
1218 let err = result.unwrap_err();
1219 assert!(err.to_string().contains("type of 1"));
1220 assert!(err.to_string().contains("CERTS cells"));
1221 }
1222
1223 #[test]
1224 fn test_with_invalid_cert_type_rsa_crosscert() {
1225 let cert_b64 = encode_test_certificate(1, 7, vec![]);
1226 let result = Ed25519Certificate::from_base64(&cert_b64);
1227 assert!(result.is_err());
1228 let err = result.unwrap_err();
1229 assert!(err.to_string().contains("type of 7"));
1230 assert!(err.to_string().contains("RSA identity"));
1231 }
1232
1233 #[test]
1234 fn test_truncated_extension() {
1235 let cert_b64 = encode_test_certificate(1, 4, vec![vec![]]);
1236 let result = Ed25519Certificate::from_base64(&cert_b64);
1237 assert!(result.is_err());
1238 assert!(result.unwrap_err().to_string().contains("missing header"));
1239
1240 let ext = vec![0x50, 0x00, 0x00, 0x00, 0x15, 0x12];
1241 let cert_b64 = encode_test_certificate(1, 4, vec![ext]);
1242 let result = Ed25519Certificate::from_base64(&cert_b64);
1243 assert!(result.is_err());
1244 let err = result.unwrap_err();
1245 assert!(err.to_string().contains("truncated"));
1246 }
1247
1248 #[test]
1249 fn test_extra_extension_data() {
1250 let ext = vec![0x00, 0x01, 0x00, 0x00, 0x15, 0x12];
1251 let cert_b64 = encode_test_certificate(1, 4, vec![ext]);
1252 let result = Ed25519Certificate::from_base64(&cert_b64);
1253 assert!(result.is_err());
1254 let err = result.unwrap_err();
1255 assert!(err.to_string().contains("unused extension data"));
1256 }
1257
1258 #[test]
1259 fn test_truncated_signing_key() {
1260 let ext = vec![0x00, 0x02, 0x04, 0x07, 0x11, 0x12];
1261 let cert_b64 = encode_test_certificate(1, 4, vec![ext]);
1262 let result = Ed25519Certificate::from_base64(&cert_b64);
1263 assert!(result.is_err());
1264 let err = result.unwrap_err();
1265 assert!(err.to_string().contains("HAS_SIGNING_KEY"));
1266 assert!(err.to_string().contains("32 bytes"));
1267 assert!(err.to_string().contains("was 2"));
1268 }
1269
1270 #[test]
1271 fn test_signing_key_extraction() {
1272 let signing_key = vec![0x11u8; 32];
1273 let mut ext = vec![0x00, 0x20, 0x04, 0x00];
1274 ext.extend_from_slice(&signing_key);
1275
1276 let cert_b64 = encode_test_certificate(1, 4, vec![ext]);
1277 let cert = Ed25519Certificate::from_base64(&cert_b64).unwrap();
1278
1279 assert_eq!(Some(signing_key.as_slice()), cert.signing_key());
1280 }
1281
1282 #[test]
1283 fn test_signing_key_not_present() {
1284 let cert_b64 = encode_test_certificate(1, 4, vec![]);
1285 let cert = Ed25519Certificate::from_base64(&cert_b64).unwrap();
1286
1287 assert_eq!(None, cert.signing_key());
1288 }
1289
1290 #[test]
1291 fn test_pem_format() {
1292 let pem_cert = format!(
1293 "-----BEGIN ED25519 CERT-----\n{}\n-----END ED25519 CERT-----",
1294 ED25519_CERT.trim()
1295 );
1296 let cert = Ed25519Certificate::from_base64(&pem_cert).unwrap();
1297 assert_eq!(1, cert.version);
1298 assert_eq!(CertType::Ed25519Signing, cert.cert_type);
1299 }
1300
1301 #[test]
1302 fn test_base64_roundtrip() {
1303 let original = b"Hello, World!";
1304 let encoded = base64_encode(original);
1305 let decoded = base64_decode(&encoded).unwrap();
1306 assert_eq!(original.to_vec(), decoded);
1307 }
1308
1309 #[test]
1310 fn test_base64_with_padding() {
1311 for len in 1..20 {
1312 let original: Vec<u8> = (0..len).map(|i| i as u8).collect();
1313 let encoded = base64_encode(&original);
1314 let decoded = base64_decode(&encoded).unwrap();
1315 assert_eq!(original, decoded);
1316 }
1317 }
1318}