Skip to main content

crypto/
pem_loader.rs

1// SPDX-License-Identifier: Apache-2.0
2//! PEM-header classification shared across signer backends.
3//!
4//! Every `Signer` impl in this crate had its own ad-hoc header sniff
5//! (Ed25519 and P-256 each parsed the same `-----BEGIN ...-----`
6//! lines). Centralizing the classification keeps the "which formats
7//! Heddle accepts" question answerable from a single source.
8//!
9//! The dispatch helper [`load_signer_from_pem`] mirrors what
10//! `lib.rs::load_signer` used to do inline, but now reads as a
11//! straight match instead of a chain of `contains()` predicates.
12
13use crate::{Ed25519Signer, P256Signer, Signer, SignerError};
14
15/// The wire format inferred from a PEM blob's BEGIN line, or `Raw*`
16/// when the input is just hex/base64 seed bytes with no PEM wrapper.
17/// Each variant maps to exactly one `Signer` constructor.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum PemKind {
20    /// `-----BEGIN PRIVATE KEY-----` — RFC 5208 PKCS#8.
21    Pkcs8,
22    /// `-----BEGIN EC PRIVATE KEY-----` — SEC1.
23    Sec1Ec,
24    /// `-----BEGIN OPENSSH PRIVATE KEY-----` — not yet supported.
25    OpenSsh,
26    /// Bare 32 hex bytes (Ed25519 seed).
27    Ed25519HexSeed,
28    /// Bare base64 — 32 bytes (seed) or 64 bytes (signing-key + public-key pair).
29    Ed25519Base64Seed,
30    Unknown,
31}
32
33/// Classify a PEM/raw-key blob by its header (or shape, for unwrapped
34/// seed material). Pure function — no I/O, no allocation beyond what
35/// the input trim implies.
36pub fn classify_pem(pem: &str) -> PemKind {
37    let trimmed = pem.trim();
38    if trimmed.contains("-----BEGIN PRIVATE KEY-----") {
39        return PemKind::Pkcs8;
40    }
41    if trimmed.contains("-----BEGIN EC PRIVATE KEY-----") {
42        return PemKind::Sec1Ec;
43    }
44    if trimmed.contains("-----BEGIN OPENSSH PRIVATE KEY-----") {
45        return PemKind::OpenSsh;
46    }
47    if hex::decode(trimmed).is_ok_and(|b| b.len() == 32) {
48        return PemKind::Ed25519HexSeed;
49    }
50    use base64::Engine;
51    if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(trimmed)
52        && (bytes.len() == 32 || bytes.len() == 64)
53    {
54        return PemKind::Ed25519Base64Seed;
55    }
56    PemKind::Unknown
57}
58
59/// Dispatch a PEM blob to the right `Signer` backend.
60///
61/// Replaces the chain of `if pem_content.contains(...)` blocks that
62/// `load_signer` used to inline. PKCS#8 is ambiguous (the same BEGIN
63/// line wraps multiple private-key algorithms), so the PKCS#8 case
64/// probes the supported backends in order and returns the first one
65/// that accepts the key.
66pub fn load_signer_from_pem(pem: &str) -> Result<Box<dyn Signer>, SignerError> {
67    match classify_pem(pem) {
68        PemKind::Pkcs8 => {
69            // PKCS#8 doesn't expose the algorithm in the BEGIN line, so try
70            // each backend. Ed25519 keys also encode the marker `MC4CAQ` near
71            // the start of the base64 body; checking that first avoids
72            // needlessly probing P-256 for common Ed25519 input.
73            if pem.contains("MC4CAQ")
74                && let Ok(s) = Ed25519Signer::from_pem(pem)
75            {
76                return Ok(Box::new(s) as Box<dyn Signer>);
77            }
78            if let Ok(s) = P256Signer::from_pem(pem) {
79                return Ok(Box::new(s) as Box<dyn Signer>);
80            }
81            if let Ok(s) = Ed25519Signer::from_pem(pem) {
82                return Ok(Box::new(s) as Box<dyn Signer>);
83            }
84            Err(SignerError::UnknownKeyFormat)
85        }
86        PemKind::Sec1Ec => P256Signer::from_pem(pem).map(|s| Box::new(s) as Box<dyn Signer>),
87        PemKind::Ed25519HexSeed | PemKind::Ed25519Base64Seed => {
88            Ed25519Signer::from_pem(pem).map(|s| Box::new(s) as Box<dyn Signer>)
89        }
90        PemKind::OpenSsh => Err(SignerError::Pem(
91            "OpenSSH private keys are not yet supported".to_string(),
92        )),
93        PemKind::Unknown => Err(SignerError::UnknownKeyFormat),
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn classifies_pkcs8_header() {
103        let pem = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBC...\n-----END PRIVATE KEY-----";
104        assert_eq!(classify_pem(pem), PemKind::Pkcs8);
105    }
106
107    #[test]
108    fn rejects_pkcs1_rsa_header() {
109        let pem = "-----BEGIN RSA PRIVATE KEY-----\nMIIBOg...\n-----END RSA PRIVATE KEY-----";
110        assert_eq!(classify_pem(pem), PemKind::Unknown);
111    }
112
113    #[test]
114    fn classifies_sec1_ec_header() {
115        let pem = "-----BEGIN EC PRIVATE KEY-----\nMHc...\n-----END EC PRIVATE KEY-----";
116        assert_eq!(classify_pem(pem), PemKind::Sec1Ec);
117    }
118
119    #[test]
120    fn classifies_openssh_header() {
121        let pem = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3Bl...\n-----END OPENSSH PRIVATE KEY-----";
122        assert_eq!(classify_pem(pem), PemKind::OpenSsh);
123    }
124
125    #[test]
126    fn classifies_hex_seed() {
127        let pem = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
128        assert_eq!(classify_pem(pem), PemKind::Ed25519HexSeed);
129    }
130
131    #[test]
132    fn unknown_input_classified_as_such() {
133        assert_eq!(classify_pem(""), PemKind::Unknown);
134        assert_eq!(classify_pem("not a key"), PemKind::Unknown);
135    }
136}