#![forbid(unsafe_code)]
use std::path::Path;
use anyhow::{Result, anyhow};
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ed25519_dalek::Signer as _;
use ed25519_dalek::{SigningKey, pkcs8::DecodePrivateKey};
use sha2::{Digest, Sha256};
use time::OffsetDateTime;
use crate::manifest::PackSignature;
use super::canon::{CanonicalizedPack, canonicalize_pack_dir};
pub struct SigningOutcome {
pub signature: PackSignature,
pub canonical: CanonicalizedPack,
}
pub fn sign_pack(
pack_dir: &Path,
private_key_pem: &str,
key_id_override: Option<&str>,
) -> Result<SigningOutcome> {
let canonical = canonicalize_pack_dir(pack_dir)?;
let signing_key = load_signing_key(private_key_pem)?;
let verifying_key = signing_key.verifying_key();
let key_id = key_id_override
.map(|value| value.to_string())
.unwrap_or_else(|| derive_key_id(verifying_key.as_bytes()));
let signature = signing_key.sign(&canonical.bytes);
let encoded_sig = URL_SAFE_NO_PAD.encode(signature.to_bytes());
let pack_signature = PackSignature {
alg: "ed25519".to_string(),
key_id,
created_at: OffsetDateTime::now_utc(),
digest: format!("sha256:{}", canonical.digest_hex),
sig: encoded_sig,
};
Ok(SigningOutcome {
signature: pack_signature,
canonical,
})
}
fn load_signing_key(pem: &str) -> Result<SigningKey> {
match SigningKey::from_pkcs8_pem(pem) {
Ok(key) => Ok(key),
Err(primary_err) => {
let (label, doc) = pkcs8::SecretDocument::from_pem(pem)
.map_err(|err| anyhow!("failed to parse private key PEM: {err}"))?;
if label != "ED25519 PRIVATE KEY" {
return Err(anyhow!("unsupported private key format: {primary_err}"));
}
SigningKey::from_pkcs8_der(doc.as_bytes()).map_err(|err| {
anyhow!("failed to load ED25519 private key from PKCS#8 data: {err}")
})
}
}
}
fn derive_key_id(public_key_bytes: &[u8]) -> String {
let digest = Sha256::digest(public_key_bytes);
hex::encode(&digest[..16])
}
mod pkcs8 {
pub use pkcs8::SecretDocument;
}