Skip to main content

vanta_security/
sign.rs

1//! Minisign (Ed25519) signature verification (`docs/15-security.md`).
2//!
3//! Supports both legacy (`Ed`, signs the file) and prehashed (`ED`, signs the
4//! BLAKE2b-512 of the file) minisign signatures. Verification is the
5//! security-relevant operation; key management/distribution is wired with the
6//! registry (`docs/26-registry-and-metadata-reference.md`).
7
8use base64::engine::general_purpose::STANDARD;
9use base64::Engine;
10use blake2::{Blake2b512, Digest};
11use ed25519_dalek::{Signature, Verifier, VerifyingKey};
12use vanta_core::{Area, VtaError, VtaResult};
13
14/// A parsed minisign public key.
15pub struct MinisignKey {
16    pub key_id: [u8; 8],
17    vk: VerifyingKey,
18}
19
20/// Parse a minisign public key (the file is a comment line + a base64 line).
21pub fn parse_minisign_pubkey(text: &str) -> VtaResult<MinisignKey> {
22    let b64 = text
23        .lines()
24        .map(str::trim)
25        .rfind(|l| !l.is_empty() && !l.starts_with("untrusted comment:"))
26        .ok_or_else(|| err("empty public key"))?;
27    let raw = decode(b64)?;
28    if raw.len() != 42 {
29        return Err(err("public key has wrong length"));
30    }
31    // raw = algo[2] ("Ed") + key_id[8] + ed25519_pk[32]
32    let mut key_id = [0u8; 8];
33    key_id.copy_from_slice(&raw[2..10]);
34    let mut pk = [0u8; 32];
35    pk.copy_from_slice(&raw[10..42]);
36    let vk = VerifyingKey::from_bytes(&pk).map_err(|e| err(&format!("bad public key: {e}")))?;
37    Ok(MinisignKey { key_id, vk })
38}
39
40/// Verify a `.minisig` signature over `data` against `key`. `VTA-VRF-0002` on failure.
41pub fn minisign_verify(data: &[u8], sig_file: &str, key: &MinisignKey) -> VtaResult<()> {
42    // The first base64 line is the signature; a later one is the global signature.
43    let sig_b64 = sig_file
44        .lines()
45        .map(str::trim)
46        .find(|l| {
47            !l.is_empty()
48                && !l.starts_with("untrusted comment:")
49                && !l.starts_with("trusted comment:")
50        })
51        .ok_or_else(|| err("no signature line"))?;
52    let raw = decode(sig_b64)?;
53    if raw.len() != 74 {
54        return Err(err("signature has wrong length"));
55    }
56    // raw = algo[2] + key_id[8] + sig[64]
57    let algo = &raw[0..2];
58    if raw[2..10] != key.key_id {
59        return Err(err("signature key id does not match the trusted key"));
60    }
61    let mut sig_bytes = [0u8; 64];
62    sig_bytes.copy_from_slice(&raw[10..74]);
63    let signature = Signature::from_bytes(&sig_bytes);
64
65    let result = if algo == b"ED" {
66        // Prehashed: the signature covers BLAKE2b-512(data).
67        let mut hasher = Blake2b512::new();
68        hasher.update(data);
69        let digest = hasher.finalize();
70        key.vk.verify(digest.as_slice(), &signature)
71    } else {
72        key.vk.verify(data, &signature)
73    };
74    result.map_err(|_| err("signature verification failed"))
75}
76
77fn decode(s: &str) -> VtaResult<Vec<u8>> {
78    STANDARD
79        .decode(s.trim())
80        .map_err(|e| err(&format!("base64 decode: {e}")))
81}
82
83fn err(msg: &str) -> VtaError {
84    VtaError::new(Area::Vrf, 2, msg.to_string())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use ed25519_dalek::{Signer, SigningKey};
91
92    /// Build a minisign pubkey string and a `.minisig` for `data` (legacy `Ed`).
93    fn make(seed: [u8; 32], key_id: [u8; 8], data: &[u8]) -> (String, String) {
94        let sk = SigningKey::from_bytes(&seed);
95        let pk = sk.verifying_key().to_bytes();
96        let sig = sk.sign(data).to_bytes();
97
98        let mut pk_raw = Vec::new();
99        pk_raw.extend_from_slice(b"Ed");
100        pk_raw.extend_from_slice(&key_id);
101        pk_raw.extend_from_slice(&pk);
102        let pubkey = format!("untrusted comment: test\n{}", STANDARD.encode(&pk_raw));
103
104        let mut sig_raw = Vec::new();
105        sig_raw.extend_from_slice(b"Ed");
106        sig_raw.extend_from_slice(&key_id);
107        sig_raw.extend_from_slice(&sig);
108        let sig_file = format!(
109            "untrusted comment: sig\n{}\ntrusted comment: t\n{}",
110            STANDARD.encode(&sig_raw),
111            STANDARD.encode([0u8; 64])
112        );
113        (pubkey, sig_file)
114    }
115
116    #[test]
117    fn verifies_valid_signature() {
118        let (pubkey, sig) = make([7u8; 32], [1, 2, 3, 4, 5, 6, 7, 8], b"hello world");
119        let key = parse_minisign_pubkey(&pubkey).unwrap();
120        assert!(minisign_verify(b"hello world", &sig, &key).is_ok());
121    }
122
123    #[test]
124    fn rejects_tampered_data() {
125        let (pubkey, sig) = make([7u8; 32], [1, 2, 3, 4, 5, 6, 7, 8], b"hello world");
126        let key = parse_minisign_pubkey(&pubkey).unwrap();
127        let err = minisign_verify(b"HELLO WORLD", &sig, &key).unwrap_err();
128        assert_eq!(err.area, Area::Vrf);
129    }
130
131    #[test]
132    fn rejects_wrong_key_id() {
133        let (pubkey, _) = make([7u8; 32], [9, 9, 9, 9, 9, 9, 9, 9], b"data");
134        let (_, sig) = make([7u8; 32], [1, 1, 1, 1, 1, 1, 1, 1], b"data");
135        let key = parse_minisign_pubkey(&pubkey).unwrap();
136        assert!(minisign_verify(b"data", &sig, &key).is_err());
137    }
138}