//! DSSE envelopes, the signature wrapper in-toto, SLSA and cosign read, and //! the PEM public key cosign's `--key` takes: how Oxygen's attestations //! reach tools outside it (spec ยง14.6, oxygenhub#239). Only the Ed25519 half //! of a composite key signs here, since DSSE verifiers know Ed25519 and not //! ML-DSA-65; DSSE's own pre-authentication encoding separates the domain //! from Oxygen's signatures (which begin `oxy1 sig`). use crate::identity::{Identity, SecretKey, VerifyError}; /// DSSE's payload type for in-toto statements. pub const IN_TOTO: &str = "application/vnd.in-toto+json"; /// DSSE's pre-authentication encoding: what the signature covers. pub fn pae(payload_type: &str, payload: &[u8]) -> Vec { let mut out = format!( "DSSEv1 {} {} {} ", payload_type.len(), payload_type, payload.len() ) .into_bytes(); out.extend_from_slice(payload); out } /// A DSSE envelope over `payload`, signed by `sk`, as JSON. `keyid` names /// the key to a reader; DSSE gives it no meaning. pub fn envelope(payload_type: &str, payload: &[u8], keyid: &str, sk: &SecretKey) -> String { let sig = sk.sign_ed25519(&pae(payload_type, payload)); format!( "{{\"payloadType\":{},\"payload\":\"{}\",\"signatures\":[{{\"keyid\":{},\"sig\":\"{}\"}}]}}", crate::repo::json_str(payload_type), base64(payload), crate::repo::json_str(keyid), base64(&sig) ) } /// Check one envelope signature, its parts already decoded. pub fn verify( identity: &Identity, payload_type: &str, payload: &[u8], sig: &[u8], ) -> Result<(), VerifyError> { identity.verify_ed25519(&pae(payload_type, payload), sig) } /// The identity's Ed25519 public key as a PEM SubjectPublicKeyInfo. pub fn public_pem(identity: &Identity) -> String { // SEQUENCE { SEQUENCE { OID 1.3.101.112 }, BIT STRING { key } } let mut der = vec![ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, ]; der.extend_from_slice(&identity.ed25519_pk); format!( "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n", base64(&der) ) } const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /// Standard base64 with padding, as DSSE and PEM use. pub fn base64(bytes: &[u8]) -> String { let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); for chunk in bytes.chunks(3) { let n = chunk.iter().fold(0u32, |acc, b| (acc << 8) | *b as u32) << (8 * (3 - chunk.len())); for i in 0..4 { if i <= chunk.len() { out.push(ALPHABET[((n >> (18 - 6 * i)) & 63) as usize] as char); } else { out.push('='); } } } out } /// [`base64`] back to bytes; `None` for anything that is not. pub fn base64_decode(text: &str) -> Option> { let text = text.trim_end_matches('='); let mut out = Vec::with_capacity(text.len() * 3 / 4); let (mut acc, mut bits) = (0u32, 0); for c in text.bytes() { let v = ALPHABET.iter().position(|a| *a == c)? as u32; acc = (acc << 6) | v; bits += 6; if bits >= 8 { bits -= 8; out.push((acc >> bits) as u8); acc &= (1 << bits) - 1; } } Some(out) } #[cfg(test)] mod tests { use super::*; #[test] fn pae_and_base64_are_the_specs() { // DSSE's own example: PAE("http://example.com/HelloWorld", "hello world"). assert_eq!( pae("http://example.com/HelloWorld", b"hello world"), b"DSSEv1 29 http://example.com/HelloWorld 11 hello world".to_vec() ); for (plain, coded) in [ ("", ""), ("f", "Zg=="), ("fo", "Zm8="), ("foo", "Zm9v"), ("foobar", "Zm9vYmFy"), ] { assert_eq!(base64(plain.as_bytes()), coded); assert_eq!(base64_decode(coded).unwrap(), plain.as_bytes()); } assert!(base64_decode("Zm9v!").is_none()); } #[test] fn an_envelope_verifies_with_its_key_and_nothing_else() { let sk = SecretKey::generate(); let id = sk.identity("Ada", "ada@example.org", 1); let other = SecretKey::generate().identity("Eve", "eve@example.org", 1); let payload = br#"{"_type":"https://in-toto.io/Statement/v1"}"#; let env = envelope(IN_TOTO, payload, "k", &sk); let sig_b64 = env .rsplit("\"sig\":\"") .next() .unwrap() .trim_end_matches("\"}]}"); let sig = base64_decode(sig_b64).unwrap(); assert!(env.contains(&format!("\"payload\":\"{}\"", base64(payload)))); assert!(verify(&id, IN_TOTO, payload, &sig).is_ok()); assert!(verify(&other, IN_TOTO, payload, &sig).is_err()); assert!(verify(&id, IN_TOTO, b"{}", &sig).is_err()); assert!(verify(&id, "text/plain", payload, &sig).is_err()); } #[test] fn the_pem_holds_the_ed25519_key() { let id = SecretKey::generate().identity("Ada", "ada@example.org", 1); let pem = public_pem(&id); let body: String = pem.lines().filter(|l| !l.starts_with("-----")).collect(); let der = base64_decode(&body).unwrap(); assert_eq!(der.len(), 44); assert_eq!(&der[12..], &id.ed25519_pk); } }