use ed25519_dalek::{Signer as _, SigningKey};
use zeroize::Zeroizing;
use crate::Error;
use crate::algorithm::Algorithm;
use crate::signer::Signer;
use mkit_core::sign::KeyPair;
pub const KEYID_PREFIX: &str = "blake3:";
#[derive(Debug)]
pub struct RepoKeySigner {
kp: KeyPair,
}
impl RepoKeySigner {
#[must_use]
pub fn new(kp: KeyPair) -> Self {
Self { kp }
}
#[must_use]
pub fn from_seed_zeroizing(seed: &Zeroizing<[u8; 32]>) -> Self {
Self {
kp: KeyPair::from_seed_zeroizing(seed),
}
}
#[must_use]
pub fn keyid_string(&self) -> String {
let h = mkit_core::hash::hash(&self.kp.public.0);
let hex = mkit_core::hash::to_hex(&h);
format!("{KEYID_PREFIX}{hex}")
}
}
impl Signer for RepoKeySigner {
fn algorithm(&self) -> Algorithm {
Algorithm::Ed25519
}
fn keyid(&self) -> Result<String, Error> {
Ok(self.keyid_string())
}
fn sign(&mut self, pae: &[u8]) -> Result<Vec<u8>, Error> {
let signing = SigningKey::from_bytes(&self.kp.secret.0);
let sig = signing.sign(pae);
Ok(sig.to_bytes().to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signature as DalekSig, VerifyingKey};
#[test]
fn signature_verifies_with_dalek() {
let kp = KeyPair::from_seed([0x42; 32]);
let pk = kp.public.0;
let mut s = RepoKeySigner::new(kp);
let pae = b"DSSEv1 28 application/vnd.in-toto+json 2 {}";
let sig_bytes = s.sign(pae).unwrap();
assert_eq!(sig_bytes.len(), 64);
let vk = VerifyingKey::from_bytes(&pk).unwrap();
let sig = DalekSig::from_bytes(sig_bytes.as_slice().try_into().unwrap());
vk.verify_strict(pae, &sig).expect("verify");
let tampered = b"DSSEv1 28 application/vnd.in-toto+json 2 {X";
assert!(vk.verify_strict(tampered, &sig).is_err());
}
#[test]
fn dsse_signature_never_verifies_as_a_commit_signature_and_vice_versa() {
use mkit_core::sign::{self, COMMIT_DOMAIN, Signature as CoreSignature};
let kp = KeyPair::from_seed([0x77; 32]);
let payload = b"shared bytes reused across both schemes";
let mut dsse_signer = RepoKeySigner::new(KeyPair::from_seed([0x77; 32]));
let dsse_sig_bytes = dsse_signer.sign(payload).unwrap();
let mut dsse_sig = [0u8; 64];
dsse_sig.copy_from_slice(&dsse_sig_bytes);
assert!(
sign::verify(&kp.public, COMMIT_DOMAIN, payload, &CoreSignature(dsse_sig)).is_err(),
"a DSSE-domain signature must not verify as a commit signature"
);
let commit_sig = kp.sign(COMMIT_DOMAIN, payload);
let vk = VerifyingKey::from_bytes(&kp.public.0).unwrap();
let dalek_commit_sig = DalekSig::from_bytes(&commit_sig.0);
assert!(
vk.verify_strict(payload, &dalek_commit_sig).is_err(),
"a commit-domain signature must not verify as a DSSE signature over the same bytes"
);
}
#[test]
fn keyid_shape_blake3_of_pubkey() {
let kp = KeyPair::from_seed([0x11; 32]);
let pk = kp.public.0;
let s = RepoKeySigner::new(kp);
let kid = s.keyid().unwrap();
assert_eq!(kid.len(), 71);
assert!(kid.starts_with(KEYID_PREFIX));
let hex = &kid[KEYID_PREFIX.len()..];
assert_eq!(hex.len(), 64);
assert!(
hex.bytes()
.all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c))
);
let expected = mkit_core::hash::to_hex(&mkit_core::hash::hash(&pk));
assert_eq!(hex, expected);
}
}