1use base64::{Engine, engine::general_purpose::STANDARD};
2
3use crate::error::TransparencyError;
4use crate::types::MerkleHash;
5
6const ALG_ED25519: u8 = 0x01;
8
9pub fn compute_key_id(key_name: &str, pubkey: &[u8; 32]) -> [u8; 4] {
22 let mut data = Vec::with_capacity(key_name.len() + 1 + 1 + 32);
23 data.extend_from_slice(key_name.as_bytes());
24 data.push(b'\n');
25 data.push(ALG_ED25519);
26 data.extend_from_slice(pubkey);
27
28 let hash = MerkleHash::sha256(&data);
29 let mut id = [0u8; 4];
30 id.copy_from_slice(&hash.as_bytes()[..4]);
31 id
32}
33
34pub fn compute_ecdsa_key_id(spki_der: &[u8]) -> [u8; 4] {
50 let hash = MerkleHash::sha256(spki_der);
51 let mut id = [0u8; 4];
52 id.copy_from_slice(&hash.as_bytes()[..4]);
53 id
54}
55
56pub fn build_signature_line(key_name: &str, key_id: &[u8; 4], signature: &[u8; 64]) -> String {
68 let mut sig_data = Vec::with_capacity(1 + 4 + 64);
69 sig_data.push(ALG_ED25519);
70 sig_data.extend_from_slice(key_id);
71 sig_data.extend_from_slice(signature);
72 let encoded = STANDARD.encode(&sig_data);
73 format!("\u{2014} {key_name} {encoded}\n")
74}
75
76pub fn parse_signed_note(note: &str) -> Result<(String, Vec<NoteSignature>), TransparencyError> {
93 let (body, sig_lines) = split_note(note)?;
94 let mut signatures = Vec::with_capacity(sig_lines.len());
95 for line in &sig_lines {
96 signatures.push(parse_signature_line(line)?);
97 }
98 Ok((body, signatures))
99}
100
101fn split_note(note: &str) -> Result<(String, Vec<String>), TransparencyError> {
107 let sig_marker = "\u{2014} ";
108 let lines: Vec<&str> = note.lines().collect();
109
110 let body_end_idx = lines
111 .iter()
112 .position(|l| l.starts_with(sig_marker))
113 .ok_or_else(|| TransparencyError::InvalidNote("no signature lines found".into()))?;
114
115 let mut body_lines = &lines[..body_end_idx];
118 if body_lines.last() == Some(&"") {
119 body_lines = &body_lines[..body_lines.len() - 1];
120 }
121 let body = body_lines.join("\n") + "\n";
122
123 let sig_lines = lines[body_end_idx..]
124 .iter()
125 .filter_map(|l| l.strip_prefix(sig_marker).map(str::to_string))
126 .collect();
127
128 Ok((body, sig_lines))
129}
130
131#[derive(Debug, Clone)]
133pub struct NoteSignature {
134 pub key_name: String,
136 pub algorithm: u8,
138 pub key_id: [u8; 4],
140 pub signature: Vec<u8>,
142}
143
144fn parse_signature_line(line: &str) -> Result<NoteSignature, TransparencyError> {
145 let space_idx = line
146 .find(' ')
147 .ok_or_else(|| TransparencyError::InvalidNote("malformed signature line".into()))?;
148
149 let key_name = &line[..space_idx];
150 let b64 = &line[space_idx + 1..];
151
152 let raw = STANDARD
153 .decode(b64.trim())
154 .map_err(|e| TransparencyError::InvalidNote(format!("base64 decode: {e}")))?;
155
156 if raw.len() < 5 {
157 return Err(TransparencyError::InvalidNote(
158 "signature data too short".into(),
159 ));
160 }
161
162 let algorithm = raw[0];
163 let mut key_id = [0u8; 4];
164 key_id.copy_from_slice(&raw[1..5]);
165 let signature = raw[5..].to_vec();
166
167 Ok(NoteSignature {
168 key_name: key_name.to_string(),
169 algorithm,
170 key_id,
171 signature,
172 })
173}
174
175#[derive(Debug, Clone)]
182pub struct C2spSignature {
183 pub key_name: String,
185 pub key_id: [u8; 4],
187 pub signature: Vec<u8>,
189}
190
191pub fn parse_signed_note_c2sp(
207 note: &str,
208) -> Result<(String, Vec<C2spSignature>), TransparencyError> {
209 let (body, sig_lines) = split_note(note)?;
210 let mut signatures = Vec::with_capacity(sig_lines.len());
211 for line in &sig_lines {
212 signatures.push(parse_c2sp_signature_line(line)?);
213 }
214 Ok((body, signatures))
215}
216
217fn parse_c2sp_signature_line(line: &str) -> Result<C2spSignature, TransparencyError> {
218 let space_idx = line
219 .find(' ')
220 .ok_or_else(|| TransparencyError::InvalidNote("malformed signature line".into()))?;
221
222 let key_name = &line[..space_idx];
223 let b64 = &line[space_idx + 1..];
224
225 let raw = STANDARD
226 .decode(b64.trim())
227 .map_err(|e| TransparencyError::InvalidNote(format!("base64 decode: {e}")))?;
228
229 if raw.len() < 5 {
232 return Err(TransparencyError::InvalidNote(
233 "signature data too short".into(),
234 ));
235 }
236
237 let mut key_id = [0u8; 4];
238 key_id.copy_from_slice(&raw[..4]);
239 let signature = raw[4..].to_vec();
240
241 Ok(C2spSignature {
242 key_name: key_name.to_string(),
243 key_id,
244 signature,
245 })
246}
247
248pub fn serialize_signed_note(body: &str, signatures: &[String]) -> String {
259 let mut out =
260 String::with_capacity(body.len() + signatures.iter().map(|s| s.len()).sum::<usize>() + 1);
261 out.push_str(body);
262 out.push('\n');
263 for sig in signatures {
264 out.push_str(sig);
265 }
266 out
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn key_id_computation() {
275 let pubkey = [0xab; 32];
276 let key_id = compute_key_id("auths.dev/log", &pubkey);
277 assert_eq!(key_id.len(), 4);
278
279 let key_id2 = compute_key_id("auths.dev/log", &pubkey);
281 assert_eq!(key_id, key_id2);
282
283 let key_id3 = compute_key_id("other.dev/log", &pubkey);
285 assert_ne!(key_id, key_id3);
286 }
287
288 #[test]
289 fn signature_line_format() {
290 let key_id = [0x01, 0x02, 0x03, 0x04];
291 let sig = [0xaa; 64];
292 let line = build_signature_line("auths.dev/log", &key_id, &sig);
293
294 assert!(line.starts_with("\u{2014} auths.dev/log "));
295 assert!(line.ends_with('\n'));
296
297 let parts: Vec<&str> = line.trim().splitn(3, ' ').collect();
299 let decoded = STANDARD.decode(parts[2]).unwrap();
300 assert_eq!(decoded[0], ALG_ED25519);
301 assert_eq!(&decoded[1..5], &key_id);
302 assert_eq!(&decoded[5..], &sig);
303 }
304
305 #[test]
306 fn signed_note_roundtrip() {
307 let body = "auths.dev/log\n42\nq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\n";
308 let key_id = [0x01, 0x02, 0x03, 0x04];
309 let sig = [0xcc; 64];
310 let sig_line = build_signature_line("auths.dev/log", &key_id, &sig);
311 let note = serialize_signed_note(body, &[sig_line]);
312
313 let (parsed_body, parsed_sigs) = parse_signed_note(¬e).unwrap();
314 assert_eq!(parsed_body, body);
315 assert_eq!(parsed_sigs.len(), 1);
316 assert_eq!(parsed_sigs[0].key_name, "auths.dev/log");
317 assert_eq!(parsed_sigs[0].algorithm, ALG_ED25519);
318 assert_eq!(parsed_sigs[0].key_id, key_id);
319 assert_eq!(parsed_sigs[0].signature, sig.to_vec());
320 }
321
322 #[test]
323 fn parse_note_rejects_no_signatures() {
324 let note = "just body\nno sigs\n";
325 assert!(parse_signed_note(note).is_err());
326 }
327
328 #[test]
329 fn ecdsa_key_id_matches_rekor_hint() {
330 const REKOR_PROD_PUBKEY_B64: &str = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==";
333 let der = STANDARD.decode(REKOR_PROD_PUBKEY_B64).unwrap();
334 assert_eq!(compute_ecdsa_key_id(&der), [0xc0, 0xd2, 0x3d, 0x6a]);
337 }
338
339 #[test]
340 fn ecdsa_and_ed25519_key_ids_use_distinct_formulas() {
341 let bytes = [0x11u8; 32];
345 let ed = compute_key_id("rekor.sigstore.dev", &bytes);
346 let ec = compute_ecdsa_key_id(&bytes);
347 assert_ne!(ed, ec);
348 }
349
350 #[test]
351 fn c2sp_signature_standard_layout_roundtrip() {
352 let key_id = [0xc0, 0xd2, 0x3d, 0x6a];
354 let sig = vec![0x30u8, 0x45, 0x02, 0x21, 0x00, 0xab, 0xcd];
355 let mut blob = Vec::new();
356 blob.extend_from_slice(&key_id);
357 blob.extend_from_slice(&sig);
358 let note = format!(
359 "rekor.sigstore.dev - 1\n42\nq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\n\n\u{2014} rekor.sigstore.dev {}\n",
360 STANDARD.encode(&blob)
361 );
362 let (body, sigs) = parse_signed_note_c2sp(¬e).unwrap();
363 assert_eq!(
364 body,
365 "rekor.sigstore.dev - 1\n42\nq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\n"
366 );
367 assert_eq!(sigs.len(), 1);
368 assert_eq!(sigs[0].key_name, "rekor.sigstore.dev");
369 assert_eq!(sigs[0].key_id, key_id);
370 assert_eq!(sigs[0].signature, sig);
371 }
372}