Skip to main content

alef_component_runtime/
keys.rs

1//! Ed25519 public key decoding shared by every layer that trusts a component signature:
2//! this crate's own artifact cache, `alef`'s config validation, and `alef`'s artifact
3//! signing/verification CLI path. All three used to decode independently and disagreed on
4//! accepted base64 padding, so a key config validation accepted could fail every load. This
5//! is the one decoder all of them call now.
6
7use base64::Engine as _;
8use ed25519_dalek::VerifyingKey;
9use ed25519_dalek::pkcs8::DecodePublicKey as _;
10
11use crate::ComponentError;
12
13/// Decode an Ed25519 public key from PEM, or from base64-encoded (standard or unpadded)
14/// raw 32-byte / DER `SubjectPublicKeyInfo` bytes.
15///
16/// Standard and unpadded base64 are both accepted because config authors, signing
17/// tooling, and hand-copied keys are not consistent about padding, and a key that
18/// validates under one variant but not the other is a config that silently breaks at
19/// load time.
20pub fn decode_public_key(value: &str) -> Result<VerifyingKey, ComponentError> {
21    if value.trim_start().starts_with("-----BEGIN") {
22        return VerifyingKey::from_public_key_pem(value).map_err(|_| ComponentError::InvalidPublicKey);
23    }
24    let trimmed = value.trim();
25    let bytes = base64::engine::general_purpose::STANDARD
26        .decode(trimmed)
27        .or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(trimmed))
28        .map_err(|_| ComponentError::InvalidPublicKey)?;
29    if let Ok(raw) = <[u8; 32]>::try_from(bytes.as_slice()) {
30        return VerifyingKey::from_bytes(&raw).map_err(|_| ComponentError::InvalidPublicKey);
31    }
32    VerifyingKey::from_public_key_der(&bytes).map_err(|_| ComponentError::InvalidPublicKey)
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use ed25519_dalek::SigningKey;
39
40    /// A fixture key independent of this module's own PEM/DER encoding path, generated with
41    /// `openssl genpkey -algorithm ED25519` / `openssl pkey -pubout`, so the PEM test does
42    /// not validate `decode_public_key` against bytes this same module produced.
43    const FIXTURE_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA68ACmVmPwMQ4mvLWXAuT7IezNeigy/lauMP08bFC6lU=\n-----END PUBLIC KEY-----\n";
44    const FIXTURE_RAW: [u8; 32] = [
45        0xeb, 0xc0, 0x02, 0x99, 0x59, 0x8f, 0xc0, 0xc4, 0x38, 0x9a, 0xf2, 0xd6, 0x5c, 0x0b, 0x93, 0xec, 0x87, 0xb3,
46        0x35, 0xe8, 0xa0, 0xcb, 0xf9, 0x5a, 0xb8, 0xc3, 0xf4, 0xf1, 0xb1, 0x42, 0xea, 0x55,
47    ];
48
49    fn key() -> VerifyingKey {
50        SigningKey::from_bytes(&[9; 32]).verifying_key()
51    }
52
53    #[test]
54    fn decodes_standard_padded_base64() {
55        let encoded = base64::engine::general_purpose::STANDARD.encode(key().to_bytes());
56        assert_eq!(decode_public_key(&encoded).unwrap(), key());
57    }
58
59    #[test]
60    fn decodes_unpadded_base64() {
61        let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(key().to_bytes());
62        assert_eq!(decode_public_key(&encoded).unwrap(), key());
63    }
64
65    #[test]
66    fn decodes_pem() {
67        let expected = VerifyingKey::from_bytes(&FIXTURE_RAW).unwrap();
68        assert_eq!(decode_public_key(FIXTURE_PEM).unwrap(), expected);
69    }
70
71    #[test]
72    fn rejects_garbage() {
73        assert!(matches!(
74            decode_public_key("not-base64"),
75            Err(ComponentError::InvalidPublicKey)
76        ));
77    }
78}