| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
use crate::identity::{Identity, SecretKey, VerifyError}; |
| 9 |
|
| 10 |
|
| 11 |
pub const IN_TOTO: &str = "application/vnd.in-toto+json"; |
| 12 |
|
| 13 |
|
| 14 |
pub fn pae(payload_type: &str, payload: &[u8]) -> Vec<u8> { |
| 15 |
let mut out = format!( |
| 16 |
"DSSEv1 {} {} {} ", |
| 17 |
payload_type.len(), |
| 18 |
payload_type, |
| 19 |
payload.len() |
| 20 |
) |
| 21 |
.into_bytes(); |
| 22 |
out.extend_from_slice(payload); |
| 23 |
out |
| 24 |
} |
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
pub fn envelope(payload_type: &str, payload: &[u8], keyid: &str, sk: &SecretKey) -> String { |
| 29 |
let sig = sk.sign_ed25519(&pae(payload_type, payload)); |
| 30 |
format!( |
| 31 |
"{{\"payloadType\":{},\"payload\":\"{}\",\"signatures\":[{{\"keyid\":{},\"sig\":\"{}\"}}]}}", |
| 32 |
crate::repo::json_str(payload_type), |
| 33 |
base64(payload), |
| 34 |
crate::repo::json_str(keyid), |
| 35 |
base64(&sig) |
| 36 |
) |
| 37 |
} |
| 38 |
|
| 39 |
|
| 40 |
pub fn verify( |
| 41 |
identity: &Identity, |
| 42 |
payload_type: &str, |
| 43 |
payload: &[u8], |
| 44 |
sig: &[u8], |
| 45 |
) -> Result<(), VerifyError> { |
| 46 |
identity.verify_ed25519(&pae(payload_type, payload), sig) |
| 47 |
} |
| 48 |
|
| 49 |
|
| 50 |
pub fn public_pem(identity: &Identity) -> String { |
| 51 |
|
| 52 |
let mut der = vec![ |
| 53 |
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, |
| 54 |
]; |
| 55 |
der.extend_from_slice(&identity.ed25519_pk); |
| 56 |
format!( |
| 57 |
"-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n", |
| 58 |
base64(&der) |
| 59 |
) |
| 60 |
} |
| 61 |
|
| 62 |
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
| 63 |
|
| 64 |
|
| 65 |
pub fn base64(bytes: &[u8]) -> String { |
| 66 |
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); |
| 67 |
for chunk in bytes.chunks(3) { |
| 68 |
let n = chunk.iter().fold(0u32, |acc, b| (acc << 8) | *b as u32) << (8 * (3 - chunk.len())); |
| 69 |
for i in 0..4 { |
| 70 |
if i <= chunk.len() { |
| 71 |
out.push(ALPHABET[((n >> (18 - 6 * i)) & 63) as usize] as char); |
| 72 |
} else { |
| 73 |
out.push('='); |
| 74 |
} |
| 75 |
} |
| 76 |
} |
| 77 |
out |
| 78 |
} |
| 79 |
|
| 80 |
|
| 81 |
pub fn base64_decode(text: &str) -> Option<Vec<u8>> { |
| 82 |
let text = text.trim_end_matches('='); |
| 83 |
let mut out = Vec::with_capacity(text.len() * 3 / 4); |
| 84 |
let (mut acc, mut bits) = (0u32, 0); |
| 85 |
for c in text.bytes() { |
| 86 |
let v = ALPHABET.iter().position(|a| *a == c)? as u32; |
| 87 |
acc = (acc << 6) | v; |
| 88 |
bits += 6; |
| 89 |
if bits >= 8 { |
| 90 |
bits -= 8; |
| 91 |
out.push((acc >> bits) as u8); |
| 92 |
acc &= (1 << bits) - 1; |
| 93 |
} |
| 94 |
} |
| 95 |
Some(out) |
| 96 |
} |
| 97 |
|
| 98 |
#[cfg(test)] |
| 99 |
mod tests { |
| 100 |
use super::*; |
| 101 |
|
| 102 |
#[test] |
| 103 |
fn pae_and_base64_are_the_specs() { |
| 104 |
|
| 105 |
assert_eq!( |
| 106 |
pae("http://example.com/HelloWorld", b"hello world"), |
| 107 |
b"DSSEv1 29 http://example.com/HelloWorld 11 hello world".to_vec() |
| 108 |
); |
| 109 |
for (plain, coded) in [ |
| 110 |
("", ""), |
| 111 |
("f", "Zg=="), |
| 112 |
("fo", "Zm8="), |
| 113 |
("foo", "Zm9v"), |
| 114 |
("foobar", "Zm9vYmFy"), |
| 115 |
] { |
| 116 |
assert_eq!(base64(plain.as_bytes()), coded); |
| 117 |
assert_eq!(base64_decode(coded).unwrap(), plain.as_bytes()); |
| 118 |
} |
| 119 |
assert!(base64_decode("Zm9v!").is_none()); |
| 120 |
} |
| 121 |
|
| 122 |
#[test] |
| 123 |
fn an_envelope_verifies_with_its_key_and_nothing_else() { |
| 124 |
let sk = SecretKey::generate(); |
| 125 |
let id = sk.identity("Ada", "[email protected]", 1); |
| 126 |
let other = SecretKey::generate().identity("Eve", "[email protected]", 1); |
| 127 |
let payload = br#"{"_type":"https://in-toto.io/Statement/v1"}"#; |
| 128 |
let env = envelope(IN_TOTO, payload, "k", &sk); |
| 129 |
let sig_b64 = env |
| 130 |
.rsplit("\"sig\":\"") |
| 131 |
.next() |
| 132 |
.unwrap() |
| 133 |
.trim_end_matches("\"}]}"); |
| 134 |
let sig = base64_decode(sig_b64).unwrap(); |
| 135 |
assert!(env.contains(&format!("\"payload\":\"{}\"", base64(payload)))); |
| 136 |
assert!(verify(&id, IN_TOTO, payload, &sig).is_ok()); |
| 137 |
assert!(verify(&other, IN_TOTO, payload, &sig).is_err()); |
| 138 |
assert!(verify(&id, IN_TOTO, b"{}", &sig).is_err()); |
| 139 |
assert!(verify(&id, "text/plain", payload, &sig).is_err()); |
| 140 |
} |
| 141 |
|
| 142 |
#[test] |
| 143 |
fn the_pem_holds_the_ed25519_key() { |
| 144 |
let id = SecretKey::generate().identity("Ada", "[email protected]", 1); |
| 145 |
let pem = public_pem(&id); |
| 146 |
let body: String = pem.lines().filter(|l| !l.starts_with("-----")).collect(); |
| 147 |
let der = base64_decode(&body).unwrap(); |
| 148 |
assert_eq!(der.len(), 44); |
| 149 |
assert_eq!(&der[12..], &id.ed25519_pk); |
| 150 |
} |
| 151 |
} |