OXYGENHUB

Coverage / crates/oxy-core/src/attest.rs 94.5%

225 of 238 lines run, 23 of 28 functions · run #427 on main · run missed

1
//! Attestations (spec §14.4): signed statements about references and batches
2
//! of changes, and runs (receipts, §14.6).
3
4
use crate::cbor::{self, Value};
5
use crate::hash::{hash_object, ChangeHash, KeyId, ObjectRef, ObjectType};
6
use crate::objects::{
7
    array, bytes, check_version, field, map, oref, text, uint, Hlc, ObjectError, FORMAT_VERSION,
8
};
9
10
pub const MAX_SEAL_HASHES: usize = 4096;
11
12
#[derive(Clone, Debug, PartialEq, Eq)]
13
pub enum AttestBody {
14
    /// A bookmark moved from `old_state` to `new_state` (spec §10.2).
15
    Ref {
16
        name: String,
17
        old_state: ObjectRef,
18
        new_state: ObjectRef,
19
        serial: u64,
20
    },
21
    /// One signature covering a batch of changes (spec §14.4).
22
    Seal { hashes: Vec<ChangeHash> },
23
    /// An immutable named state (spec §10.1).
24
    Tag {
25
        name: String,
26
        state: ObjectRef,
27
        message: String,
28
    },
29
    /// Tag `name`, naming `state`, is withdrawn (spec §10.1): signed by a
30
    /// key the policy lets create the tag. The name is never used again.
31
    Untag {
32
        name: String,
33
        state: ObjectRef,
34
        reason: String,
35
    },
36
    /// The signing key's identity was replaced by `new` (spec §14.2).
37
    Rotate { new: KeyId },
38
    /// `key` must no longer be trusted (spec §14.2).
39
    Revoke { key: KeyId, reason: String },
40
    /// A runner ran the job of `receipt` on `state` (spec §14.6); `run`
41
    /// names the run, usually by URL.
42
    Receipt {
43
        receipt: ObjectRef,
44
        state: ObjectRef,
45
        run: String,
46
    },
47
    /// A person approved (or rejected) one job of one run (OxygenFlow
48
    /// approval gates; oxygenhub#235): the job as hashed with its sandbox,
49
    /// on that state, taking exactly those input blobs (a reviewed plan),
50
    /// for that environment. It approves nothing else.
51
    Approval {
52
        run: String,
53
        job: String,
54
        job_hash: ObjectRef,
55
        state: ObjectRef,
56
        inputs: Vec<ObjectRef>,
57
        environment: String,
58
        approve: bool,
59
    },
60
    /// A host accepted the reference attestation `attest`, at this
61
    /// attestation's own time, the host's clock (§14.7): the time a
62
    /// certificate of a short-lived key is checked at on a fetch.
63
    Accepted { attest: ObjectRef },
64
}
65
66
#[derive(Clone, Debug, PartialEq, Eq)]
67
pub struct Attest {
68
    /// Repository ID chosen at init.
69
    pub repo: [u8; 16],
70
    pub hlc: Hlc,
71
    pub body: AttestBody,
72
}
73
74
impl Attest {
751407×
    pub fn kind(&self) -> &'static str {
761407×
        match self.body {
77753×
            AttestBody::Ref { .. } => "ref",
78180×
            AttestBody::Seal { .. } => "seal",
79292×
            AttestBody::Tag { .. } => "tag",
8024×
            AttestBody::Untag { .. } => "untag",
8120×
            AttestBody::Rotate { .. } => "rotate",
824×
            AttestBody::Revoke { .. } => "revoke",
8341×
            AttestBody::Receipt { .. } => "receipt",
8412×
            AttestBody::Approval { .. } => "approval",
8581×
            AttestBody::Accepted { .. } => "accepted",
86
        }
871407×
    }
88
8956×
    pub fn seal(repo: [u8; 16], mut hashes: Vec<ChangeHash>) -> Attest {
9056×
        hashes.sort();
9156×
        hashes.dedup();
9256×
        Attest {
9356×
            repo,
9456×
            hlc: Hlc::now(),
9556×
            body: AttestBody::Seal { hashes },
9656×
        }
9756×
    }
98
991404×
    pub fn to_value(&self) -> Value {
1001404×
        let body = match &self.body {
101
            AttestBody::Ref {
102752×
                name,
103752×
                old_state,
104752×
                new_state,
105752×
                serial,
106752×
            } => Value::map([
107752×
                (0, Value::Text(name.clone())),
108752×
                (1, Value::Bytes(old_state.0.to_vec())),
109752×
                (2, Value::Bytes(new_state.0.to_vec())),
110752×
                (3, Value::Uint(*serial)),
111752×
            ]),
112180×
            AttestBody::Seal { hashes } => Value::map([(
113
                0,
114191×
                Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
115
            )]),
116
            AttestBody::Tag {
117291×
                name,
118291×
                state,
119291×
                message,
120291×
            } => Value::map([
121291×
                (0, Value::Text(name.clone())),
122291×
                (1, Value::Bytes(state.0.to_vec())),
123291×
                (2, Value::Text(message.clone())),
124291×
            ]),
125
            AttestBody::Untag {
12623×
                name,
12723×
                state,
12823×
                reason,
12923×
            } => Value::map([
13023×
                (0, Value::Text(name.clone())),
13123×
                (1, Value::Bytes(state.0.to_vec())),
13223×
                (2, Value::Text(reason.clone())),
13323×
            ]),
13420×
            AttestBody::Rotate { new } => Value::map([(0, Value::Bytes(new.0.to_vec()))]),
1354×
            AttestBody::Revoke { key, reason } => Value::map([
1364×
                (0, Value::Bytes(key.0.to_vec())),
1374×
                (1, Value::Text(reason.clone())),
1384×
            ]),
139
            AttestBody::Receipt {
14041×
                receipt,
14141×
                state,
14241×
                run,
14341×
            } => Value::map([
14441×
                (0, Value::Bytes(receipt.0.to_vec())),
14541×
                (1, Value::Bytes(state.0.to_vec())),
14641×
                (2, Value::Text(run.clone())),
14741×
            ]),
14881×
            AttestBody::Accepted { attest } => Value::map([(0, Value::Bytes(attest.0.to_vec()))]),
149
            AttestBody::Approval {
15012×
                run,
15112×
                job,
15212×
                job_hash,
15312×
                state,
15412×
                inputs,
15512×
                environment,
15612×
                approve,
15712×
            } => Value::map([
15812×
                (0, Value::Text(run.clone())),
15912×
                (1, Value::Text(job.clone())),
16012×
                (2, Value::Bytes(job_hash.0.to_vec())),
16112×
                (3, Value::Bytes(state.0.to_vec())),
162
                (
163
                    4,
16418×
                    Value::Array(inputs.iter().map(|i| Value::Bytes(i.0.to_vec())).collect()),
165
                ),
16612×
                (5, Value::Text(environment.clone())),
16712×
                (6, Value::Uint(u64::from(*approve))),
168
            ]),
169
        };
1701404×
        Value::map([
1711404×
            (0, Value::Uint(FORMAT_VERSION)),
1721404×
            (1, Value::Text(self.kind().into())),
1731404×
            (2, Value::Bytes(self.repo.to_vec())),
1741404×
            (3, self.hlc.to_value()),
1751404×
            (4, body),
1761404×
        ])
1771404×
    }
178
1791404×
    pub fn encode(&self) -> Vec<u8> {
1801404×
        cbor::encode(&self.to_value())
1811404×
    }
182
183752×
    pub fn hash(&self) -> ObjectRef {
184752×
        hash_object(ObjectType::Attest, &self.encode())
185752×
    }
186
1871315×
    pub fn decode(b: &[u8]) -> Result<Attest, ObjectError> {
1881315×
        let v = cbor::decode(b)?;
1891315×
        let m = map(&v, "attest")?;
1901315×
        check_version(m)?;
1911315×
        let kind = text(field(m, 1, "kind")?, "kind")?;
1921315×
        let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?
1931315×
            .try_into()
1941315×
            .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?;
1951315×
        let hlc = Hlc::from_value(field(m, 3, "hlc")?)?;
1961315×
        let b = map(field(m, 4, "body")?, "body")?;
1971315×
        let body = match kind {
1981315×
            "ref" => AttestBody::Ref {
199500×
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
200500×
                old_state: oref(field(b, 1, "old_state")?, "old_state")?,
201500×
                new_state: oref(field(b, 2, "new_state")?, "new_state")?,
202500×
                serial: uint(field(b, 3, "serial")?, "serial")?,
203
            },
204815×
            "seal" => {
205389×
                let hashes = array(field(b, 0, "hashes")?, "hashes")?
206389×
                    .iter()
207425×
                    .map(|h| oref(h, "hash"))
208389×
                    .collect::<Result<Vec<_>, _>>()?;
209389×
                if hashes.is_empty()
210388×
                    || hashes.len() > MAX_SEAL_HASHES
211388×
                    || !hashes.windows(2).all(|w| w[0] < w[1])
212
                {
2132×
                    return Err(ObjectError::Schema(
2142×
                        "seal: hashes must be 1..4096, sorted and unique".into(),
2152×
                    ));
216387×
                }
217387×
                AttestBody::Seal { hashes }
218
            }
219426×
            "tag" => AttestBody::Tag {
220325×
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
221325×
                state: oref(field(b, 1, "state")?, "state")?,
222325×
                message: text(field(b, 2, "message")?, "message")?.to_owned(),
223
            },
224101×
            "untag" => AttestBody::Untag {
22513×
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
22613×
                state: oref(field(b, 1, "state")?, "state")?,
22713×
                reason: text(field(b, 2, "reason")?, "reason")?.to_owned(),
228
            },
22988×
            "rotate" => AttestBody::Rotate {
2300×
                new: oref(field(b, 0, "new")?, "new")?,
231
            },
23288×
            "revoke" => AttestBody::Revoke {
2330×
                key: oref(field(b, 0, "key")?, "key")?,
2340×
                reason: text(field(b, 1, "reason")?, "reason")?.to_owned(),
235
            },
23688×
            "receipt" => AttestBody::Receipt {
23721×
                receipt: oref(field(b, 0, "receipt")?, "receipt")?,
23821×
                state: oref(field(b, 1, "state")?, "state")?,
23921×
                run: text(field(b, 2, "run")?, "run")?.to_owned(),
240
            },
24167×
            "approval" => {
2423×
                let inputs = array(field(b, 4, "inputs")?, "inputs")?
2433×
                    .iter()
2444×
                    .map(|h| oref(h, "input"))
2453×
                    .collect::<Result<Vec<_>, _>>()?;
2463×
                if !inputs.windows(2).all(|w| w[0] < w[1]) {
2470×
                    return Err(ObjectError::Schema(
2480×
                        "approval: inputs must be sorted and unique".into(),
2490×
                    ));
2503×
                }
251
                AttestBody::Approval {
2523×
                    run: text(field(b, 0, "run")?, "run")?.to_owned(),
2533×
                    job: text(field(b, 1, "job")?, "job")?.to_owned(),
2543×
                    job_hash: oref(field(b, 2, "job_hash")?, "job_hash")?,
2553×
                    state: oref(field(b, 3, "state")?, "state")?,
2563×
                    inputs,
2573×
                    environment: text(field(b, 5, "environment")?, "environment")?.to_owned(),
2583×
                    approve: match uint(field(b, 6, "approve")?, "approve")? {
2590×
                        0 => false,
2603×
                        1 => true,
2610×
                        _ => return Err(ObjectError::Schema("approval: approve is 0 or 1".into())),
262
                    },
263
                }
264
            }
26564×
            "accepted" => AttestBody::Accepted {
26664×
                attest: oref(field(b, 0, "attest")?, "attest")?,
267
            },
2680×
            other => {
2690×
                return Err(ObjectError::Schema(format!(
2700×
                    "unknown attestation kind {other:?}"
2710×
                )))
272
            }
273
        };
2741313×
        Ok(Attest { repo, hlc, body })
2751315×
    }
276
}
277
278
#[cfg(test)]
279
mod tests {
280
    use super::*;
281
282
    #[test]
2831×
    fn round_trips() {
2841×
        let r = Attest {
2851×
            repo: [1; 16],
2861×
            hlc: Hlc {
2871×
                wall_ms: 3,
2881×
                logical: 0,
2891×
            },
2901×
            body: AttestBody::Ref {
2911×
                name: "main".into(),
2921×
                old_state: ObjectRef([1; 32]),
2931×
                new_state: ObjectRef([2; 32]),
2941×
                serial: 7,
2951×
            },
2961×
        };
2971×
        assert_eq!(Attest::decode(&r.encode()).unwrap(), r);
2981×
        assert_eq!(r.kind(), "ref");
2991×
        let s = Attest::seal(
3001×
            [1; 16],
3011×
            vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])],
302
        );
3031×
        let d = Attest::decode(&s.encode()).unwrap();
3041×
        assert!(
3051×
            matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])])
306
        );
3071×
        assert_ne!(s.hash(), r.hash());
3081×
        let t = Attest {
3091×
            repo: [1; 16],
3101×
            hlc: Hlc::default(),
3111×
            body: AttestBody::Tag {
3121×
                name: "v1".into(),
3131×
                state: ObjectRef([5; 32]),
3141×
                message: "release".into(),
3151×
            },
3161×
        };
3171×
        assert_eq!(Attest::decode(&t.encode()).unwrap(), t);
3181×
        assert_eq!(t.kind(), "tag");
3191×
        let u = Attest {
3201×
            repo: [1; 16],
3211×
            hlc: Hlc::default(),
3221×
            body: AttestBody::Untag {
3231×
                name: "v1".into(),
3241×
                state: ObjectRef([5; 32]),
3251×
                reason: "tagged the wrong change".into(),
3261×
            },
3271×
        };
3281×
        assert_eq!(Attest::decode(&u.encode()).unwrap(), u);
3291×
        assert_eq!(u.kind(), "untag");
3301×
        assert_ne!(u.hash(), t.hash());
3311×
    }
332
333
    #[test]
3341×
    fn rejects_bad_seals() {
3351×
        let empty = Attest {
3361×
            repo: [0; 16],
3371×
            hlc: Hlc::default(),
3381×
            body: AttestBody::Seal { hashes: vec![] },
3391×
        };
3401×
        assert!(Attest::decode(&empty.encode()).is_err());
3411×
        let unsorted = Attest {
3421×
            repo: [0; 16],
3431×
            hlc: Hlc::default(),
3441×
            body: AttestBody::Seal {
3451×
                hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])],
3461×
            },
3471×
        };
3481×
        assert!(Attest::decode(&unsorted.encode()).is_err());
3491×
    }
350
}