OXYGENHUB
# d0d52107b074 oxygen / crates / oxy-core / src / attest.rs
8.3 KiB · Rust · last changed Execution receipts: the receipt object and attestation 1 month ago source blame history raw
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 {
51
    pub fn kind(&self) -> &'static str {
52
        match self.body {
53
            AttestBody::Ref { .. } => "ref",
54
            AttestBody::Seal { .. } => "seal",
55
            AttestBody::Tag { .. } => "tag",
56
            AttestBody::Rotate { .. } => "rotate",
57
            AttestBody::Revoke { .. } => "revoke",
58
            AttestBody::Receipt { .. } => "receipt",
59
        }
60
    }
61
62
    pub fn seal(repo: [u8; 16], mut hashes: Vec<ChangeHash>) -> Attest {
63
        hashes.sort();
64
        hashes.dedup();
65
        Attest {
66
            repo,
67
            hlc: Hlc::now(),
68
            body: AttestBody::Seal { hashes },
69
        }
70
    }
71
72
    pub fn to_value(&self) -> Value {
73
        let body = match &self.body {
74
            AttestBody::Ref {
75
                name,
76
                old_state,
77
                new_state,
78
                serial,
79
            } => Value::map([
80
                (0, Value::Text(name.clone())),
81
                (1, Value::Bytes(old_state.0.to_vec())),
82
                (2, Value::Bytes(new_state.0.to_vec())),
83
                (3, Value::Uint(*serial)),
84
            ]),
85
            AttestBody::Seal { hashes } => Value::map([(
86
                0,
87
                Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
88
            )]),
89
            AttestBody::Tag {
90
                name,
91
                state,
92
                message,
93
            } => Value::map([
94
                (0, Value::Text(name.clone())),
95
                (1, Value::Bytes(state.0.to_vec())),
96
                (2, Value::Text(message.clone())),
97
            ]),
98
            AttestBody::Rotate { new } => Value::map([(0, Value::Bytes(new.0.to_vec()))]),
99
            AttestBody::Revoke { key, reason } => Value::map([
100
                (0, Value::Bytes(key.0.to_vec())),
101
                (1, Value::Text(reason.clone())),
102
            ]),
103
            AttestBody::Receipt {
104
                receipt,
105
                state,
106
                run,
107
            } => Value::map([
108
                (0, Value::Bytes(receipt.0.to_vec())),
109
                (1, Value::Bytes(state.0.to_vec())),
110
                (2, Value::Text(run.clone())),
111
            ]),
112
        };
113
        Value::map([
114
            (0, Value::Uint(FORMAT_VERSION)),
115
            (1, Value::Text(self.kind().into())),
116
            (2, Value::Bytes(self.repo.to_vec())),
117
            (3, self.hlc.to_value()),
118
            (4, body),
119
        ])
120
    }
121
122
    pub fn encode(&self) -> Vec<u8> {
123
        cbor::encode(&self.to_value())
124
    }
125
126
    pub fn hash(&self) -> ObjectRef {
127
        hash_object(ObjectType::Attest, &self.encode())
128
    }
129
130
    pub fn decode(b: &[u8]) -> Result<Attest, ObjectError> {
131
        let v = cbor::decode(b)?;
132
        let m = map(&v, "attest")?;
133
        check_version(m)?;
134
        let kind = text(field(m, 1, "kind")?, "kind")?;
135
        let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?
136
            .try_into()
137
            .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?;
138
        let hlc = Hlc::from_value(field(m, 3, "hlc")?)?;
139
        let b = map(field(m, 4, "body")?, "body")?;
140
        let body = match kind {
141
            "ref" => AttestBody::Ref {
142
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
143
                old_state: oref(field(b, 1, "old_state")?, "old_state")?,
144
                new_state: oref(field(b, 2, "new_state")?, "new_state")?,
145
                serial: uint(field(b, 3, "serial")?, "serial")?,
146
            },
147
            "seal" => {
148
                let hashes = array(field(b, 0, "hashes")?, "hashes")?
149
                    .iter()
150
                    .map(|h| oref(h, "hash"))
151
                    .collect::<Result<Vec<_>, _>>()?;
152
                if hashes.is_empty()
153
                    || hashes.len() > MAX_SEAL_HASHES
154
                    || !hashes.windows(2).all(|w| w[0] < w[1])
155
                {
156
                    return Err(ObjectError::Schema(
157
                        "seal: hashes must be 1..4096, sorted and unique".into(),
158
                    ));
159
                }
160
                AttestBody::Seal { hashes }
161
            }
162
            "tag" => AttestBody::Tag {
163
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
164
                state: oref(field(b, 1, "state")?, "state")?,
165
                message: text(field(b, 2, "message")?, "message")?.to_owned(),
166
            },
167
            "rotate" => AttestBody::Rotate {
168
                new: oref(field(b, 0, "new")?, "new")?,
169
            },
170
            "revoke" => AttestBody::Revoke {
171
                key: oref(field(b, 0, "key")?, "key")?,
172
                reason: text(field(b, 1, "reason")?, "reason")?.to_owned(),
173
            },
174
            "receipt" => AttestBody::Receipt {
175
                receipt: oref(field(b, 0, "receipt")?, "receipt")?,
176
                state: oref(field(b, 1, "state")?, "state")?,
177
                run: text(field(b, 2, "run")?, "run")?.to_owned(),
178
            },
179
            other => {
180
                return Err(ObjectError::Schema(format!(
181
                    "unknown attestation kind {other:?}"
182
                )))
183
            }
184
        };
185
        Ok(Attest { repo, hlc, body })
186
    }
187
}
188
189
#[cfg(test)]
190
mod tests {
191
    use super::*;
192
193
    #[test]
194
    fn round_trips() {
195
        let r = Attest {
196
            repo: [1; 16],
197
            hlc: Hlc {
198
                wall_ms: 3,
199
                logical: 0,
200
            },
201
            body: AttestBody::Ref {
202
                name: "main".into(),
203
                old_state: ObjectRef([1; 32]),
204
                new_state: ObjectRef([2; 32]),
205
                serial: 7,
206
            },
207
        };
208
        assert_eq!(Attest::decode(&r.encode()).unwrap(), r);
209
        assert_eq!(r.kind(), "ref");
210
        let s = Attest::seal(
211
            [1; 16],
212
            vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])],
213
        );
214
        let d = Attest::decode(&s.encode()).unwrap();
215
        assert!(
216
            matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])])
217
        );
218
        assert_ne!(s.hash(), r.hash());
219
        let t = Attest {
220
            repo: [1; 16],
221
            hlc: Hlc::default(),
222
            body: AttestBody::Tag {
223
                name: "v1".into(),
224
                state: ObjectRef([5; 32]),
225
                message: "release".into(),
226
            },
227
        };
228
        assert_eq!(Attest::decode(&t.encode()).unwrap(), t);
229
        assert_eq!(t.kind(), "tag");
230
    }
231
232
    #[test]
233
    fn rejects_bad_seals() {
234
        let empty = Attest {
235
            repo: [0; 16],
236
            hlc: Hlc::default(),
237
            body: AttestBody::Seal { hashes: vec![] },
238
        };
239
        assert!(Attest::decode(&empty.encode()).is_err());
240
        let unsorted = Attest {
241
            repo: [0; 16],
242
            hlc: Hlc::default(),
243
            body: AttestBody::Seal {
244
                hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])],
245
            },
246
        };
247
        assert!(Attest::decode(&unsorted.encode()).is_err());
248
    }
249
}