OXYGENHUB

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

162 of 170 lines run, 20 of 22 functions · run #259 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
    /// The signing key's identity was replaced by `new` (spec §14.2).
30
    Rotate { new: KeyId },
31
    /// `key` must no longer be trusted (spec §14.2).
32
    Revoke { key: KeyId, reason: String },
33
    /// A runner ran the job of `receipt` on `state` (spec §14.6); `run`
34
    /// names the run, usually by URL.
35
    Receipt {
36
        receipt: ObjectRef,
37
        state: ObjectRef,
38
        run: String,
39
    },
40
}
41
42
#[derive(Clone, Debug, PartialEq, Eq)]
43
pub struct Attest {
44
    /// Repository ID chosen at init.
45
    pub repo: [u8; 16],
46
    pub hlc: Hlc,
47
    pub body: AttestBody,
48
}
49
50
impl Attest {
51482×
    pub fn kind(&self) -> &'static str {
52482×
        match self.body {
53282×
            AttestBody::Ref { .. } => "ref",
54115×
            AttestBody::Seal { .. } => "seal",
5544×
            AttestBody::Tag { .. } => "tag",
5614×
            AttestBody::Rotate { .. } => "rotate",
574×
            AttestBody::Revoke { .. } => "revoke",
5823×
            AttestBody::Receipt { .. } => "receipt",
59
        }
60482×
    }
61
6239×
    pub fn seal(repo: [u8; 16], mut hashes: Vec<ChangeHash>) -> Attest {
6339×
        hashes.sort();
6439×
        hashes.dedup();
6539×
        Attest {
6639×
            repo,
6739×
            hlc: Hlc::now(),
6839×
            body: AttestBody::Seal { hashes },
6939×
        }
7039×
    }
71
72480×
    pub fn to_value(&self) -> Value {
73480×
        let body = match &self.body {
74
            AttestBody::Ref {
75281×
                name,
76281×
                old_state,
77281×
                new_state,
78281×
                serial,
79281×
            } => Value::map([
80281×
                (0, Value::Text(name.clone())),
81281×
                (1, Value::Bytes(old_state.0.to_vec())),
82281×
                (2, Value::Bytes(new_state.0.to_vec())),
83281×
                (3, Value::Uint(*serial)),
84281×
            ]),
85115×
            AttestBody::Seal { hashes } => Value::map([(
86
                0,
87125×
                Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
88
            )]),
89
            AttestBody::Tag {
9043×
                name,
9143×
                state,
9243×
                message,
9343×
            } => Value::map([
9443×
                (0, Value::Text(name.clone())),
9543×
                (1, Value::Bytes(state.0.to_vec())),
9643×
                (2, Value::Text(message.clone())),
9743×
            ]),
9814×
            AttestBody::Rotate { new } => Value::map([(0, Value::Bytes(new.0.to_vec()))]),
994×
            AttestBody::Revoke { key, reason } => Value::map([
1004×
                (0, Value::Bytes(key.0.to_vec())),
1014×
                (1, Value::Text(reason.clone())),
1024×
            ]),
103
            AttestBody::Receipt {
10423×
                receipt,
10523×
                state,
10623×
                run,
10723×
            } => Value::map([
10823×
                (0, Value::Bytes(receipt.0.to_vec())),
10923×
                (1, Value::Bytes(state.0.to_vec())),
11023×
                (2, Value::Text(run.clone())),
11123×
            ]),
112
        };
113480×
        Value::map([
114480×
            (0, Value::Uint(FORMAT_VERSION)),
115480×
            (1, Value::Text(self.kind().into())),
116480×
            (2, Value::Bytes(self.repo.to_vec())),
117480×
            (3, self.hlc.to_value()),
118480×
            (4, body),
119480×
        ])
120480×
    }
121
122480×
    pub fn encode(&self) -> Vec<u8> {
123480×
        cbor::encode(&self.to_value())
124480×
    }
125
126137×
    pub fn hash(&self) -> ObjectRef {
127137×
        hash_object(ObjectType::Attest, &self.encode())
128137×
    }
129
130787×
    pub fn decode(b: &[u8]) -> Result<Attest, ObjectError> {
131787×
        let v = cbor::decode(b)?;
132787×
        let m = map(&v, "attest")?;
133787×
        check_version(m)?;
134787×
        let kind = text(field(m, 1, "kind")?, "kind")?;
135787×
        let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?
136787×
            .try_into()
137787×
            .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?;
138787×
        let hlc = Hlc::from_value(field(m, 3, "hlc")?)?;
139787×
        let b = map(field(m, 4, "body")?, "body")?;
140787×
        let body = match kind {
141787×
            "ref" => AttestBody::Ref {
142324×
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
143324×
                old_state: oref(field(b, 1, "old_state")?, "old_state")?,
144324×
                new_state: oref(field(b, 2, "new_state")?, "new_state")?,
145324×
                serial: uint(field(b, 3, "serial")?, "serial")?,
146
            },
147463×
            "seal" => {
148332×
                let hashes = array(field(b, 0, "hashes")?, "hashes")?
149332×
                    .iter()
150367×
                    .map(|h| oref(h, "hash"))
151332×
                    .collect::<Result<Vec<_>, _>>()?;
152332×
                if hashes.is_empty()
153331×
                    || hashes.len() > MAX_SEAL_HASHES
154331×
                    || !hashes.windows(2).all(|w| w[0] < w[1])
155
                {
1562×
                    return Err(ObjectError::Schema(
1572×
                        "seal: hashes must be 1..4096, sorted and unique".into(),
1582×
                    ));
159330×
                }
160330×
                AttestBody::Seal { hashes }
161
            }
162131×
            "tag" => AttestBody::Tag {
163119×
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
164119×
                state: oref(field(b, 1, "state")?, "state")?,
165119×
                message: text(field(b, 2, "message")?, "message")?.to_owned(),
166
            },
16712×
            "rotate" => AttestBody::Rotate {
1680×
                new: oref(field(b, 0, "new")?, "new")?,
169
            },
17012×
            "revoke" => AttestBody::Revoke {
1710×
                key: oref(field(b, 0, "key")?, "key")?,
1720×
                reason: text(field(b, 1, "reason")?, "reason")?.to_owned(),
173
            },
17412×
            "receipt" => AttestBody::Receipt {
17512×
                receipt: oref(field(b, 0, "receipt")?, "receipt")?,
17612×
                state: oref(field(b, 1, "state")?, "state")?,
17712×
                run: text(field(b, 2, "run")?, "run")?.to_owned(),
178
            },
1790×
            other => {
1800×
                return Err(ObjectError::Schema(format!(
1810×
                    "unknown attestation kind {other:?}"
1820×
                )))
183
            }
184
        };
185785×
        Ok(Attest { repo, hlc, body })
186787×
    }
187
}
188
189
#[cfg(test)]
190
mod tests {
191
    use super::*;
192
193
    #[test]
1941×
    fn round_trips() {
1951×
        let r = Attest {
1961×
            repo: [1; 16],
1971×
            hlc: Hlc {
1981×
                wall_ms: 3,
1991×
                logical: 0,
2001×
            },
2011×
            body: AttestBody::Ref {
2021×
                name: "main".into(),
2031×
                old_state: ObjectRef([1; 32]),
2041×
                new_state: ObjectRef([2; 32]),
2051×
                serial: 7,
2061×
            },
2071×
        };
2081×
        assert_eq!(Attest::decode(&r.encode()).unwrap(), r);
2091×
        assert_eq!(r.kind(), "ref");
2101×
        let s = Attest::seal(
2111×
            [1; 16],
2121×
            vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])],
213
        );
2141×
        let d = Attest::decode(&s.encode()).unwrap();
2151×
        assert!(
2161×
            matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])])
217
        );
2181×
        assert_ne!(s.hash(), r.hash());
2191×
        let t = Attest {
2201×
            repo: [1; 16],
2211×
            hlc: Hlc::default(),
2221×
            body: AttestBody::Tag {
2231×
                name: "v1".into(),
2241×
                state: ObjectRef([5; 32]),
2251×
                message: "release".into(),
2261×
            },
2271×
        };
2281×
        assert_eq!(Attest::decode(&t.encode()).unwrap(), t);
2291×
        assert_eq!(t.kind(), "tag");
2301×
    }
231
232
    #[test]
2331×
    fn rejects_bad_seals() {
2341×
        let empty = Attest {
2351×
            repo: [0; 16],
2361×
            hlc: Hlc::default(),
2371×
            body: AttestBody::Seal { hashes: vec![] },
2381×
        };
2391×
        assert!(Attest::decode(&empty.encode()).is_err());
2401×
        let unsorted = Attest {
2411×
            repo: [0; 16],
2421×
            hlc: Hlc::default(),
2431×
            body: AttestBody::Seal {
2441×
                hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])],
2451×
            },
2461×
        };
2471×
        assert!(Attest::decode(&unsorted.encode()).is_err());
2481×
    }
249
}