OXYGENHUB
# aded4110f0bd oxygen / crates / oxy-core / src / dsse.rs
5.3 KiB · Rust · last changed oxy receipt export --format slsa: SLSA provenance cosign can check 19 days ago source blame history raw
a39754d0 Andy McCall ↶ 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.
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
/// A DSSE envelope over `payload`, signed by `sk`, as JSON. `keyid` names
27
/// the key to a reader; DSSE gives it no meaning.
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
/// Check one envelope signature, its parts already decoded.
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
/// The identity's Ed25519 public key as a PEM SubjectPublicKeyInfo.
50
pub fn public_pem(identity: &Identity) -> String {
51
    // SEQUENCE { SEQUENCE { OID 1.3.101.112 }, BIT STRING { key } }
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
/// Standard base64 with padding, as DSSE and PEM use.
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
/// [`base64`] back to bytes; `None` for anything that is not.
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
        // DSSE's own example: PAE("http://example.com/HelloWorld", "hello world").
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
}