use std::path::Path;
use anyhow::{Context, Result};
use ed25519_dalek::SigningKey;
use ed25519_dalek::pkcs8::DecodePrivateKey;
pub const DEFAULT_KEY_ENV: &str = "GREENTIC_EXT_SIGNING_KEY_PEM";
pub fn signing_key_from_pem(pem: &str) -> Result<SigningKey> {
SigningKey::from_pkcs8_pem(pem).map_err(|e| anyhow::anyhow!("parse PKCS8 PEM private key: {e}"))
}
pub fn load_signing_key(path: Option<&Path>, env_var: &str) -> Result<SigningKey> {
let pem = match path {
Some(p) => std::fs::read_to_string(p)
.with_context(|| format!("read signing key {}", p.display()))?,
None => std::env::var(env_var).with_context(|| {
format!("env var ${env_var} not set (use --key <path> or export the env var)")
})?,
};
signing_key_from_pem(&pem)
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::pkcs8::{EncodePrivateKey, spki::der::pem::LineEnding};
fn sample_pem() -> String {
SigningKey::from_bytes(&[7u8; 32])
.to_pkcs8_pem(LineEnding::LF)
.unwrap()
.to_string()
}
#[test]
fn parses_keygen_style_pkcs8_pem() {
let key = signing_key_from_pem(&sample_pem()).unwrap();
assert_eq!(key.to_bytes(), [7u8; 32]);
}
#[test]
fn rejects_base64_raw_seed() {
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
let raw_seed_b64 = B64.encode([7u8; 32]);
let err = signing_key_from_pem(&raw_seed_b64).unwrap_err();
assert!(
err.to_string().contains("PKCS8 PEM"),
"raw seed must be rejected as not-PEM, got: {err}"
);
}
#[test]
fn loads_from_path() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("k.key");
std::fs::write(&path, sample_pem()).unwrap();
let key = load_signing_key(Some(&path), DEFAULT_KEY_ENV).unwrap();
assert_eq!(key.to_bytes(), [7u8; 32]);
}
#[test]
fn missing_path_and_unset_env_errors() {
let err = load_signing_key(None, "GREENTIC_EXT_TEST_KEY_DEFINITELY_UNSET").unwrap_err();
assert!(err.to_string().contains("not set"), "got: {err}");
}
}