use std::collections::HashMap;
#[cfg(feature = "algo-ed25519")]
use ed25519_dalek::{Signature as DalekSig, VerifyingKey};
use serde::Deserialize;
use crate::Error;
use crate::algorithm::Algorithm;
use crate::envelope::{self, Envelope};
use mkit_core::Hash;
use mkit_core::hash::{HASH_LEN, HEX_LEN, from_hex};
#[derive(Debug, Clone)]
pub enum TrustRoot {
Ed25519PubKey([u8; 32]),
#[cfg(feature = "algo-p256")]
P256PubKeySec1(Vec<u8>),
#[cfg(feature = "algo-secp256k1")]
Secp256k1PubKeySec1(Vec<u8>),
#[cfg(feature = "bls-threshold")]
Bls12381ThresholdPubKey(Vec<u8>),
SigstoreCa,
}
#[derive(Debug, Default)]
pub struct Registry {
entries: HashMap<String, TrustRoot>,
}
impl Registry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, keyid: impl Into<String>, root: TrustRoot) {
self.entries.insert(keyid.into(), root);
}
#[must_use]
pub fn lookup(&self, keyid: &str) -> Option<&TrustRoot> {
self.entries.get(keyid)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reason {
Ok,
UnknownKeyid,
SignatureMismatch,
UnsupportedTrustRoot,
AlgorithmNotEnabled,
UnknownKeyidPrefix,
}
#[derive(Debug, Clone)]
pub struct SignatureResult {
pub keyid: String,
pub verified: bool,
pub reason: Reason,
}
#[derive(Debug, Clone)]
pub struct VerifyResult {
pub any_verified: bool,
pub signatures: Vec<SignatureResult>,
}
pub fn verify_envelope(envelope_bytes: &[u8], registry: &Registry) -> Result<VerifyResult, Error> {
let env = envelope::decode(envelope_bytes)?;
verify(&env, registry)
}
pub fn verify(env: &Envelope, registry: &Registry) -> Result<VerifyResult, Error> {
if env.payload_type != envelope::PAYLOAD_TYPE_IN_TOTO {
return Err(Error::UnsupportedPayloadType);
}
if env.signatures.is_empty() {
return Err(Error::EmptySignatures);
}
let pae = env.pae();
let mut sigs = Vec::with_capacity(env.signatures.len());
let mut any_verified = false;
for s in &env.signatures {
let alg_hint = Algorithm::from_keyid(&s.keyid);
let mut row = SignatureResult {
keyid: s.keyid.clone(),
verified: false,
reason: Reason::UnknownKeyid,
};
match registry.lookup(&s.keyid) {
None => {
row.reason = if alg_hint.is_none() {
Reason::UnknownKeyidPrefix
} else {
Reason::UnknownKeyid
};
}
Some(TrustRoot::Ed25519PubKey(pk)) => {
row.reason = dispatch_ed25519(*pk, &s.sig, &pae);
if row.reason == Reason::Ok {
row.verified = true;
any_verified = true;
}
}
#[cfg(feature = "algo-p256")]
Some(TrustRoot::P256PubKeySec1(pk)) => {
row.reason = match crate::signer_p256::verify_p256(pk, &pae, &s.sig) {
Ok(()) => Reason::Ok,
Err(_) => Reason::SignatureMismatch,
};
if row.reason == Reason::Ok {
row.verified = true;
any_verified = true;
}
}
#[cfg(feature = "algo-secp256k1")]
Some(TrustRoot::Secp256k1PubKeySec1(pk)) => {
row.reason = match crate::signer_k256::verify_secp256k1(pk, &pae, &s.sig) {
Ok(()) => Reason::Ok,
Err(_) => Reason::SignatureMismatch,
};
if row.reason == Reason::Ok {
row.verified = true;
any_verified = true;
}
}
#[cfg(feature = "bls-threshold")]
Some(TrustRoot::Bls12381ThresholdPubKey(pk)) => {
row.reason = match crate::signer_bls_threshold::verify(pk, &pae, &s.sig) {
Ok(()) => Reason::Ok,
Err(_) => Reason::SignatureMismatch,
};
if row.reason == Reason::Ok {
row.verified = true;
any_verified = true;
}
}
Some(TrustRoot::SigstoreCa) => {
row.reason = Reason::UnsupportedTrustRoot;
}
}
sigs.push(row);
}
Ok(VerifyResult {
any_verified,
signatures: sigs,
})
}
pub fn verify_signature(
algorithm: Algorithm,
pubkey: &[u8],
msg: &[u8],
sig: &[u8],
) -> Result<(), Error> {
match algorithm {
Algorithm::Ed25519 => {
#[cfg(feature = "algo-ed25519")]
{
let pk: [u8; 32] = pubkey
.try_into()
.map_err(|_| Error::AlgorithmNotEnabled(Algorithm::Ed25519))?;
match verify_ed25519(pk, sig, msg) {
Reason::Ok => Ok(()),
_ => Err(Error::AlgorithmNotEnabled(Algorithm::Ed25519)),
}
}
#[cfg(not(feature = "algo-ed25519"))]
{
let _ = (pubkey, msg, sig);
Err(Error::AlgorithmNotEnabled(Algorithm::Ed25519))
}
}
Algorithm::Secp256k1 => {
#[cfg(feature = "algo-secp256k1")]
{
crate::signer_k256::verify_secp256k1(pubkey, msg, sig)
}
#[cfg(not(feature = "algo-secp256k1"))]
{
let _ = (pubkey, msg, sig);
Err(Error::AlgorithmNotEnabled(Algorithm::Secp256k1))
}
}
Algorithm::P256 => {
#[cfg(feature = "algo-p256")]
{
crate::signer_p256::verify_p256(pubkey, msg, sig)
}
#[cfg(not(feature = "algo-p256"))]
{
let _ = (pubkey, msg, sig);
Err(Error::AlgorithmNotEnabled(Algorithm::P256))
}
}
#[cfg(feature = "bls-threshold")]
Algorithm::Bls12381Threshold => {
match crate::signer_bls_threshold::verify(pubkey, msg, sig) {
Ok(()) => Ok(()),
Err(_) => Err(Error::AlgorithmNotEnabled(Algorithm::Bls12381Threshold)),
}
}
}
}
#[cfg(feature = "algo-ed25519")]
#[must_use]
pub fn verify_ed25519(pk: [u8; 32], sig_bytes: &[u8], pae: &[u8]) -> Reason {
if sig_bytes.len() != ed25519_dalek::SIGNATURE_LENGTH {
return Reason::SignatureMismatch;
}
let mut arr = [0u8; ed25519_dalek::SIGNATURE_LENGTH];
arr.copy_from_slice(sig_bytes);
let Ok(vk) = VerifyingKey::from_bytes(&pk) else {
return Reason::SignatureMismatch;
};
let sig = DalekSig::from_bytes(&arr);
if vk.verify_strict(pae, &sig).is_ok() {
Reason::Ok
} else {
Reason::SignatureMismatch
}
}
fn dispatch_ed25519(pk: [u8; 32], sig_bytes: &[u8], pae: &[u8]) -> Reason {
#[cfg(feature = "algo-ed25519")]
{
verify_ed25519(pk, sig_bytes, pae)
}
#[cfg(not(feature = "algo-ed25519"))]
{
let _ = (pk, sig_bytes, pae);
Reason::AlgorithmNotEnabled
}
}
pub fn extract_primary_commit_hash(statement_json: &[u8]) -> Result<Hash, Error> {
#[derive(Deserialize)]
struct Stmt {
#[serde(default)]
subject: Vec<SubjEntry>,
}
#[derive(Deserialize)]
struct SubjEntry {
digest: HashMap<String, String>,
}
let stmt: Stmt =
serde_json::from_slice(statement_json).map_err(|_| Error::MalformedStatement)?;
let first = stmt.subject.first().ok_or(Error::SubjectMissing)?;
let hex = first
.digest
.get("blake3")
.ok_or(Error::SubjectDigestMissing)?;
if hex.len() != HEX_LEN {
return Err(Error::InvalidDigestLength);
}
let h = from_hex(hex).map_err(|_| Error::InvalidDigestHex)?;
debug_assert_eq!(h.len(), HASH_LEN);
Ok(h)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::envelope::{self as env_mod, Envelope, Sig};
use crate::statement::{self, Statement, Subject};
use ed25519_dalek::{Signer, SigningKey};
use mkit_core::hash::to_hex;
fn build_signed_envelope(
seed: [u8; 32],
keyid: &str,
commit_hex: &str,
predicate_jcs: &[u8],
) -> (Vec<u8>, [u8; 32]) {
let signing = SigningKey::from_bytes(&seed);
let pk_bytes = signing.verifying_key().to_bytes();
let stmt = statement::encode(&Statement {
subjects: vec![Subject {
name: Some("commit".into()),
digest_blake3_hex: commit_hex.into(),
digest_sha256_hex: "00".repeat(32),
}],
predicate_type: "https://example.com/predicate/v1".into(),
predicate_jcs,
})
.unwrap();
let pae = env_mod::pae_of(env_mod::PAYLOAD_TYPE_IN_TOTO, stmt.as_bytes());
let sig = signing.sign(&pae);
let env = Envelope {
payload_type: env_mod::PAYLOAD_TYPE_IN_TOTO.into(),
payload: stmt.into_bytes(),
signatures: vec![Sig {
keyid: keyid.into(),
sig: sig.to_bytes().to_vec(),
}],
};
(env.encode().unwrap().into_bytes(), pk_bytes)
}
#[cfg(feature = "algo-ed25519")]
#[test]
fn deterministic_repo_key_roundtrip() {
let seed = [0xAB; 32];
let keyid = "blake3:deadbeef";
let commit_hex = "0011223344556677889900112233445566778899001122334455667788990011";
let (bytes, pk) = build_signed_envelope(seed, keyid, commit_hex, b"{}");
let mut reg = Registry::new();
reg.add(keyid, TrustRoot::Ed25519PubKey(pk));
let r = verify_envelope(&bytes, ®).unwrap();
assert!(r.any_verified);
assert_eq!(r.signatures.len(), 1);
assert_eq!(r.signatures[0].reason, Reason::Ok);
assert!(r.signatures[0].verified);
assert_eq!(r.signatures[0].keyid, keyid);
}
#[test]
fn rejects_empty_signatures() {
let bytes = b"{\"payload\":\"e30=\",\
\"payloadType\":\"application/vnd.in-toto+json\",\
\"signatures\":[]}";
let reg = Registry::new();
assert!(matches!(
verify_envelope(bytes, ®),
Err(Error::EmptySignatures)
));
}
#[test]
fn rejects_bad_payload_type() {
let bytes = b"{\"payload\":\"e30=\",\
\"payloadType\":\"application/x-foo\",\
\"signatures\":[{\"keyid\":\"k\",\"sig\":\"AQID\"}]}";
let reg = Registry::new();
assert!(matches!(
verify_envelope(bytes, ®),
Err(Error::UnsupportedPayloadType)
));
}
#[test]
fn unknown_keyid_does_not_verify() {
let seed = [0x11; 32];
let keyid = "blake3:unknown";
let commit_hex = "a".repeat(64);
let (bytes, _pk) = build_signed_envelope(seed, keyid, &commit_hex, b"{}");
let reg = Registry::new();
let r = verify_envelope(&bytes, ®).unwrap();
assert!(!r.any_verified);
assert_eq!(r.signatures[0].reason, Reason::UnknownKeyid);
assert!(!r.signatures[0].verified);
}
#[cfg(feature = "algo-ed25519")]
#[test]
fn tampered_payload_fails_signature() {
let seed = [0x42; 32];
let keyid = "blake3:tampered";
let commit_hex = "b".repeat(64);
let (bytes, pk) = build_signed_envelope(seed, keyid, &commit_hex, b"{}");
let mut env = env_mod::decode(&bytes).unwrap();
let mid = env.payload.len() / 2;
env.payload[mid] ^= 0x01;
let tampered = env.encode().unwrap();
let mut reg = Registry::new();
reg.add(keyid, TrustRoot::Ed25519PubKey(pk));
let r = verify_envelope(tampered.as_bytes(), ®).unwrap();
assert!(!r.any_verified);
assert_eq!(r.signatures[0].reason, Reason::SignatureMismatch);
}
#[test]
fn extract_primary_commit_hash_happy_path() {
let commit: Hash = [0xCC; 32];
let hex = to_hex(&commit);
let stmt = statement::encode(&Statement {
subjects: vec![Subject {
name: Some("commit".into()),
digest_blake3_hex: hex,
digest_sha256_hex: "00".repeat(32),
}],
predicate_type: "https://example.com/p".into(),
predicate_jcs: b"{}",
})
.unwrap();
let parsed = extract_primary_commit_hash(stmt.as_bytes()).unwrap();
assert_eq!(parsed, commit);
}
#[test]
fn extract_primary_commit_hash_rejects_missing_subject() {
let empty_subject = b"{\"_type\":\"https://in-toto.io/Statement/v1\",\
\"predicate\":{},\
\"predicateType\":\"https://example.com/p\",\
\"subject\":[]}";
assert!(matches!(
extract_primary_commit_hash(empty_subject),
Err(Error::SubjectMissing)
));
let no_subject = b"{\"_type\":\"https://in-toto.io/Statement/v1\",\
\"predicate\":{},\
\"predicateType\":\"https://example.com/p\"}";
assert!(matches!(
extract_primary_commit_hash(no_subject),
Err(Error::SubjectMissing)
));
}
#[test]
fn sigstore_trust_root_is_scaffold() {
let seed = [0x33; 32];
let keyid = "sigstore:https://example.com/workflow";
let commit_hex = "c".repeat(64);
let (bytes, _pk) = build_signed_envelope(seed, keyid, &commit_hex, b"{}");
let mut reg = Registry::new();
reg.add(keyid, TrustRoot::SigstoreCa);
let r = verify_envelope(&bytes, ®).unwrap();
assert!(!r.any_verified);
assert_eq!(r.signatures[0].reason, Reason::UnsupportedTrustRoot);
}
#[test]
fn registry_add_replaces_existing() {
let mut reg = Registry::new();
reg.add("k", TrustRoot::Ed25519PubKey([0; 32]));
reg.add("k", TrustRoot::Ed25519PubKey([1; 32]));
match reg.lookup("k") {
Some(TrustRoot::Ed25519PubKey(pk)) => assert_eq!(pk[0], 1),
_ => panic!(),
}
}
#[test]
fn secp256k1_keyid_unknown_to_registry_reports_unknown_keyid() {
let seed = [0x55; 32];
let keyid = "secp256k1:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
let commit_hex = "d".repeat(64);
let (bytes, _pk) = build_signed_envelope(seed, keyid, &commit_hex, b"{}");
let reg = Registry::new();
let r = verify_envelope(&bytes, ®).unwrap();
assert!(!r.any_verified);
assert_eq!(r.signatures.len(), 1);
assert_eq!(r.signatures[0].reason, Reason::UnknownKeyid);
}
#[test]
fn unknown_prefix_keyid_reports_unknown_keyid_prefix() {
let seed = [0x66; 32];
let keyid = "quantum-foo:whatever";
let commit_hex = "e".repeat(64);
let (bytes, _pk) = build_signed_envelope(seed, keyid, &commit_hex, b"{}");
let reg = Registry::new();
let r = verify_envelope(&bytes, ®).unwrap();
assert!(!r.any_verified);
assert_eq!(r.signatures[0].reason, Reason::UnknownKeyidPrefix);
}
#[cfg(feature = "algo-ed25519")]
#[test]
fn legacy_blake3_keyid_still_verifies_as_ed25519() {
let seed = [0x77; 32];
let keyid = "blake3:abc123";
let commit_hex = "f".repeat(64);
let (bytes, pk) = build_signed_envelope(seed, keyid, &commit_hex, b"{}");
assert_eq!(Algorithm::from_keyid(keyid), Some(Algorithm::Ed25519));
let mut reg = Registry::new();
reg.add(keyid, TrustRoot::Ed25519PubKey(pk));
let r = verify_envelope(&bytes, ®).unwrap();
assert!(r.any_verified);
assert_eq!(r.signatures[0].reason, Reason::Ok);
}
#[cfg(not(feature = "algo-ed25519"))]
#[test]
fn ed25519_backend_disabled_reports_algorithm_not_enabled() {
let bytes = b"{\"payload\":\"e30=\",\
\"payloadType\":\"application/vnd.in-toto+json\",\
\"signatures\":[{\"keyid\":\"blake3:abc\",\"sig\":\"AAAA\"}]}";
let mut reg = Registry::new();
reg.add("blake3:abc", TrustRoot::Ed25519PubKey([0u8; 32]));
let r = verify_envelope(bytes, ®).unwrap();
assert!(!r.any_verified);
assert_eq!(r.signatures[0].reason, Reason::AlgorithmNotEnabled);
}
#[cfg(feature = "algo-ed25519")]
#[test]
fn verify_signature_ed25519_happy_path() {
use ed25519_dalek::{Signer, SigningKey};
let sk = SigningKey::from_bytes(&[0x99; 32]);
let pk = sk.verifying_key().to_bytes();
let msg = b"DSSEv1 test message";
let sig = sk.sign(msg).to_bytes();
assert!(verify_signature(Algorithm::Ed25519, &pk, msg, &sig).is_ok());
}
#[cfg(not(any(feature = "algo-secp256k1", feature = "algo-p256")))]
#[test]
fn verify_signature_disabled_algorithms_return_not_enabled() {
let pk = [0u8; 33];
let msg = b"anything";
let sig = [0u8; 64];
#[cfg(not(feature = "algo-secp256k1"))]
match verify_signature(Algorithm::Secp256k1, &pk, msg, &sig) {
Err(Error::AlgorithmNotEnabled(Algorithm::Secp256k1)) => {}
other => panic!("expected AlgorithmNotEnabled(Secp256k1), got {other:?}"),
}
#[cfg(not(feature = "algo-p256"))]
match verify_signature(Algorithm::P256, &pk, msg, &sig) {
Err(Error::AlgorithmNotEnabled(Algorithm::P256)) => {}
other => panic!("expected AlgorithmNotEnabled(P256), got {other:?}"),
}
let _ = (pk, msg, sig);
}
#[cfg(feature = "algo-secp256k1")]
#[test]
fn verify_signature_secp256k1_enabled_surfaces_backend_error() {
let pk = [0u8; 33];
let msg = b"anything";
let sig = [0u8; 64];
assert!(matches!(
verify_signature(Algorithm::Secp256k1, &pk, msg, &sig),
Err(Error::Secp256k1KeyInvalid)
));
}
#[cfg(feature = "algo-p256")]
#[test]
fn verify_signature_p256_enabled_surfaces_backend_error() {
let pk = [0u8; 33];
let msg = b"anything";
let sig = [0u8; 64];
assert!(verify_signature(Algorithm::P256, &pk, msg, &sig).is_err());
}
#[cfg(feature = "algo-ed25519")]
#[test]
fn verify_signature_ed25519_mismatch_returns_err() {
let pk = [0u8; 32];
let msg = b"hello";
let sig = [0u8; 64];
assert!(verify_signature(Algorithm::Ed25519, &pk, msg, &sig).is_err());
}
#[cfg(feature = "bls-threshold")]
#[test]
fn registry_dispatches_bls_threshold_keyid_to_verify() {
use crate::signer::Signer as _;
use crate::signer_bls_threshold::{
KEYID_PREFIX, ThresholdSigner, aggregate, trusted_dealer,
};
use commonware_codec::Encode as _;
use commonware_utils::{NZU32, TestRng};
let mut rng = TestRng::new(0xBEEF);
let (sharing, shares) = trusted_dealer(&mut rng, NZU32!(4));
let agg_pubkey = sharing.public().encode().to_vec();
let keyid = format!("{KEYID_PREFIX}{}", mkit_core::to_hex_bytes(&agg_pubkey));
let stmt = crate::statement::encode(&crate::statement::Statement {
subjects: vec![crate::statement::Subject {
name: Some("commit".into()),
digest_blake3_hex: "a".repeat(64),
digest_sha256_hex: "00".repeat(32),
}],
predicate_type: "https://example.com/p/v1".into(),
predicate_jcs: b"{}",
})
.unwrap();
let pae = envelope::pae_of(envelope::PAYLOAD_TYPE_IN_TOTO, stmt.as_bytes());
let mut partials: Vec<Vec<u8>> = Vec::with_capacity(3);
for s in shares.iter().take(3) {
let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
partials.push(signer.sign(&pae).expect("partial sign"));
}
let agg_sig = aggregate(&sharing, &partials).expect("aggregate");
let env = Envelope {
payload_type: envelope::PAYLOAD_TYPE_IN_TOTO.into(),
payload: stmt.into_bytes(),
signatures: vec![envelope::Sig {
keyid: keyid.clone(),
sig: agg_sig,
}],
};
let bytes = env.encode().unwrap().into_bytes();
let mut reg = Registry::new();
reg.add(
keyid.clone(),
TrustRoot::Bls12381ThresholdPubKey(agg_pubkey),
);
let r = verify_envelope(&bytes, ®).unwrap();
assert!(r.any_verified);
assert_eq!(r.signatures.len(), 1);
assert_eq!(r.signatures[0].reason, Reason::Ok);
assert!(r.signatures[0].verified);
assert_eq!(r.signatures[0].keyid, keyid);
}
#[cfg(feature = "bls-threshold")]
#[test]
fn registry_rejects_tampered_bls_aggregate() {
use crate::signer::Signer as _;
use crate::signer_bls_threshold::{
KEYID_PREFIX, ThresholdSigner, aggregate, trusted_dealer,
};
use commonware_codec::Encode as _;
use commonware_utils::{NZU32, TestRng};
let mut rng = TestRng::new(0xCAFE);
let (sharing, shares) = trusted_dealer(&mut rng, NZU32!(4));
let agg_pubkey = sharing.public().encode().to_vec();
let keyid = format!("{KEYID_PREFIX}{}", mkit_core::to_hex_bytes(&agg_pubkey));
let stmt = crate::statement::encode(&crate::statement::Statement {
subjects: vec![crate::statement::Subject {
name: Some("commit".into()),
digest_blake3_hex: "b".repeat(64),
digest_sha256_hex: "00".repeat(32),
}],
predicate_type: "https://example.com/p/v1".into(),
predicate_jcs: b"{}",
})
.unwrap();
let pae = envelope::pae_of(envelope::PAYLOAD_TYPE_IN_TOTO, stmt.as_bytes());
let mut partials: Vec<Vec<u8>> = Vec::with_capacity(3);
for s in shares.iter().take(3) {
let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
partials.push(signer.sign(&pae).expect("partial sign"));
}
let mut agg_sig = aggregate(&sharing, &partials).expect("aggregate");
let last = agg_sig.len() - 1;
agg_sig[last] ^= 0x01;
let env = Envelope {
payload_type: envelope::PAYLOAD_TYPE_IN_TOTO.into(),
payload: stmt.into_bytes(),
signatures: vec![envelope::Sig {
keyid: keyid.clone(),
sig: agg_sig,
}],
};
let bytes = env.encode().unwrap().into_bytes();
let mut reg = Registry::new();
reg.add(keyid, TrustRoot::Bls12381ThresholdPubKey(agg_pubkey));
let r = verify_envelope(&bytes, ®).unwrap();
assert!(!r.any_verified);
assert_eq!(r.signatures[0].reason, Reason::SignatureMismatch);
}
}