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