OXYGENHUB

Coverage / crates/oxy-core/src/dsse.rs 100.0%

101 of 101 lines run, 20 of 20 functions · run #347 on proposal/125 · run missed

1
//! DSSE envelopes, the signature wrapper in-toto, SLSA and cosign read, and
2
//! the PEM public key cosign's `--key` takes: how Oxygen's attestations
3
//! reach tools outside it (spec §14.6, oxygenhub#239). Only the Ed25519 half
4
//! of a composite key signs here, since DSSE verifiers know Ed25519 and not
5
//! ML-DSA-65; DSSE's own pre-authentication encoding separates the domain
6
//! from Oxygen's signatures (which begin `oxy1 sig`).
7
8
use crate::identity::{Identity, SecretKey, VerifyError};
9
10
/// DSSE's payload type for in-toto statements.
11
pub const IN_TOTO: &str = "application/vnd.in-toto+json";
12
13
/// DSSE's pre-authentication encoding: what the signature covers.
149×
pub fn pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
159×
    let mut out = format!(
16
        "DSSEv1 {} {} {} ",
179×
        payload_type.len(),
18
        payload_type,
199×
        payload.len()
20
    )
219×
    .into_bytes();
229×
    out.extend_from_slice(payload);
239×
    out
249×
}
25
26
/// A DSSE envelope over `payload`, signed by `sk`, as JSON. `keyid` names
27
/// the key to a reader; DSSE gives it no meaning.
282×
pub fn envelope(payload_type: &str, payload: &[u8], keyid: &str, sk: &SecretKey) -> String {
292×
    let sig = sk.sign_ed25519(&pae(payload_type, payload));
302×
    format!(
31
        "{{\"payloadType\":{},\"payload\":\"{}\",\"signatures\":[{{\"keyid\":{},\"sig\":\"{}\"}}]}}",
322×
        crate::repo::json_str(payload_type),
332×
        base64(payload),
342×
        crate::repo::json_str(keyid),
352×
        base64(&sig)
36
    )
372×
}
38
39
/// Check one envelope signature, its parts already decoded.
406×
pub fn verify(
416×
    identity: &Identity,
426×
    payload_type: &str,
436×
    payload: &[u8],
446×
    sig: &[u8],
456×
) -> Result<(), VerifyError> {
466×
    identity.verify_ed25519(&pae(payload_type, payload), sig)
476×
}
48
49
/// The identity's Ed25519 public key as a PEM SubjectPublicKeyInfo.
503×
pub fn public_pem(identity: &Identity) -> String {
51
    // SEQUENCE { SEQUENCE { OID 1.3.101.112 }, BIT STRING { key } }
523×
    let mut der = vec![
53
        0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
54
    ];
553×
    der.extend_from_slice(&identity.ed25519_pk);
563×
    format!(
57
        "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n",
583×
        base64(&der)
59
    )
603×
}
61
62
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
63
64
/// Standard base64 with padding, as DSSE and PEM use.
6576×
pub fn base64(bytes: &[u8]) -> String {
6676×
    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
6722784×
    for chunk in bytes.chunks(3) {
6868268×
        let n = chunk.iter().fold(0u32, |acc, b| (acc << 8) | *b as u32) << (8 * (3 - chunk.len()));
6991136×
        for i in 0..4 {
7091136×
            if i <= chunk.len() {
7191052×
                out.push(ALPHABET[((n >> (18 - 6 * i)) & 63) as usize] as char);
7291052×
            } else {
7384×
                out.push('=');
7484×
            }
75
        }
76
    }
7776×
    out
7876×
}
79
80
/// [`base64`] back to bytes; `None` for anything that is not.
8177×
pub fn base64_decode(text: &str) -> Option<Vec<u8>> {
8277×
    let text = text.trim_end_matches('=');
8377×
    let mut out = Vec::with_capacity(text.len() * 3 / 4);
8477×
    let (mut acc, mut bits) = (0u32, 0);
85100109×
    for c in text.bytes() {
863180104×
        let v = ALPHABET.iter().position(|a| *a == c)? as u32;
87100108×
        acc = (acc << 6) | v;
88100108×
        bits += 6;
89100108×
        if bits >= 8 {
9075061×
            bits -= 8;
9175061×
            out.push((acc >> bits) as u8);
9275061×
            acc &= (1 << bits) - 1;
9375061×
        }
94
    }
9576×
    Some(out)
9677×
}
97
98
#[cfg(test)]
99
mod tests {
100
    use super::*;
101
102
    #[test]
1031×
    fn pae_and_base64_are_the_specs() {
104
        // DSSE's own example: PAE("http://example.com/HelloWorld", "hello world").
1051×
        assert_eq!(
1061×
            pae("http://example.com/HelloWorld", b"hello world"),
1071×
            b"DSSEv1 29 http://example.com/HelloWorld 11 hello world".to_vec()
108
        );
1095×
        for (plain, coded) in [
1101×
            ("", ""),
1111×
            ("f", "Zg=="),
1121×
            ("fo", "Zm8="),
1131×
            ("foo", "Zm9v"),
1141×
            ("foobar", "Zm9vYmFy"),
1151×
        ] {
1165×
            assert_eq!(base64(plain.as_bytes()), coded);
1175×
            assert_eq!(base64_decode(coded).unwrap(), plain.as_bytes());
118
        }
1191×
        assert!(base64_decode("Zm9v!").is_none());
1201×
    }
121
122
    #[test]
1231×
    fn an_envelope_verifies_with_its_key_and_nothing_else() {
1241×
        let sk = SecretKey::generate();
1251×
        let id = sk.identity("Ada", "[email protected]", 1);
1261×
        let other = SecretKey::generate().identity("Eve", "[email protected]", 1);
1271×
        let payload = br#"{"_type":"https://in-toto.io/Statement/v1"}"#;
1281×
        let env = envelope(IN_TOTO, payload, "k", &sk);
1291×
        let sig_b64 = env
1301×
            .rsplit("\"sig\":\"")
1311×
            .next()
1321×
            .unwrap()
1331×
            .trim_end_matches("\"}]}");
1341×
        let sig = base64_decode(sig_b64).unwrap();
1351×
        assert!(env.contains(&format!("\"payload\":\"{}\"", base64(payload))));
1361×
        assert!(verify(&id, IN_TOTO, payload, &sig).is_ok());
1371×
        assert!(verify(&other, IN_TOTO, payload, &sig).is_err());
1381×
        assert!(verify(&id, IN_TOTO, b"{}", &sig).is_err());
1391×
        assert!(verify(&id, "text/plain", payload, &sig).is_err());
1401×
    }
141
142
    #[test]
1431×
    fn the_pem_holds_the_ed25519_key() {
1441×
        let id = SecretKey::generate().identity("Ada", "[email protected]", 1);
1451×
        let pem = public_pem(&id);
1463×
        let body: String = pem.lines().filter(|l| !l.starts_with("-----")).collect();
1471×
        let der = base64_decode(&body).unwrap();
1481×
        assert_eq!(der.len(), 44);
1491×
        assert_eq!(&der[12..], &id.ed25519_pk);
1501×
    }
151
}