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}