1use 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
14pub struct MinisignKey {
16 pub key_id: [u8; 8],
17 vk: VerifyingKey,
18}
19
20pub 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 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
40pub fn minisign_verify(data: &[u8], sig_file: &str, key: &MinisignKey) -> VtaResult<()> {
42 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 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 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 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}