OXYGENHUB
# 9bd98cfb4f7e oxygen / crates / oxy-core / src / attest.rs
11.2 KiB · Rust · last changed The issuer countersigns when it accepted a certified update (keyless step 4c) 18 days 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
    /// A person approved (or rejected) one job of one run (OxygenFlow
41
    /// approval gates; oxygenhub#235): the job as hashed with its sandbox,
42
    /// on that state, taking exactly those input blobs (a reviewed plan),
43
    /// for that environment. It approves nothing else.
44
    Approval {
45
        run: String,
46
        job: String,
47
        job_hash: ObjectRef,
48
        state: ObjectRef,
49
        inputs: Vec<ObjectRef>,
50
        environment: String,
51
        approve: bool,
52
    },
53
    /// A host accepted the reference attestation `attest`, at this
54
    /// attestation's own time, the host's clock (§14.7): the time a
55
    /// certificate of a short-lived key is checked at on a fetch.
56
    Accepted { attest: ObjectRef },
57
}
58
59
#[derive(Clone, Debug, PartialEq, Eq)]
60
pub struct Attest {
61
    /// Repository ID chosen at init.
62
    pub repo: [u8; 16],
63
    pub hlc: Hlc,
64
    pub body: AttestBody,
65
}
66
67
impl Attest {
68
    pub fn kind(&self) -> &'static str {
69
        match self.body {
70
            AttestBody::Ref { .. } => "ref",
71
            AttestBody::Seal { .. } => "seal",
72
            AttestBody::Tag { .. } => "tag",
73
            AttestBody::Rotate { .. } => "rotate",
74
            AttestBody::Revoke { .. } => "revoke",
75
            AttestBody::Receipt { .. } => "receipt",
76
            AttestBody::Approval { .. } => "approval",
77
            AttestBody::Accepted { .. } => "accepted",
78
        }
79
    }
80
81
    pub fn seal(repo: [u8; 16], mut hashes: Vec<ChangeHash>) -> Attest {
82
        hashes.sort();
83
        hashes.dedup();
84
        Attest {
85
            repo,
86
            hlc: Hlc::now(),
87
            body: AttestBody::Seal { hashes },
88
        }
89
    }
90
91
    pub fn to_value(&self) -> Value {
92
        let body = match &self.body {
93
            AttestBody::Ref {
94
                name,
95
                old_state,
96
                new_state,
97
                serial,
98
            } => Value::map([
99
                (0, Value::Text(name.clone())),
100
                (1, Value::Bytes(old_state.0.to_vec())),
101
                (2, Value::Bytes(new_state.0.to_vec())),
102
                (3, Value::Uint(*serial)),
103
            ]),
104
            AttestBody::Seal { hashes } => Value::map([(
105
                0,
106
                Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()),
107
            )]),
108
            AttestBody::Tag {
109
                name,
110
                state,
111
                message,
112
            } => Value::map([
113
                (0, Value::Text(name.clone())),
114
                (1, Value::Bytes(state.0.to_vec())),
115
                (2, Value::Text(message.clone())),
116
            ]),
117
            AttestBody::Rotate { new } => Value::map([(0, Value::Bytes(new.0.to_vec()))]),
118
            AttestBody::Revoke { key, reason } => Value::map([
119
                (0, Value::Bytes(key.0.to_vec())),
120
                (1, Value::Text(reason.clone())),
121
            ]),
122
            AttestBody::Receipt {
123
                receipt,
124
                state,
125
                run,
126
            } => Value::map([
127
                (0, Value::Bytes(receipt.0.to_vec())),
128
                (1, Value::Bytes(state.0.to_vec())),
129
                (2, Value::Text(run.clone())),
130
            ]),
131
            AttestBody::Accepted { attest } => Value::map([(0, Value::Bytes(attest.0.to_vec()))]),
132
            AttestBody::Approval {
133
                run,
134
                job,
135
                job_hash,
136
                state,
137
                inputs,
138
                environment,
139
                approve,
140
            } => Value::map([
141
                (0, Value::Text(run.clone())),
142
                (1, Value::Text(job.clone())),
143
                (2, Value::Bytes(job_hash.0.to_vec())),
144
                (3, Value::Bytes(state.0.to_vec())),
145
                (
146
                    4,
147
                    Value::Array(inputs.iter().map(|i| Value::Bytes(i.0.to_vec())).collect()),
148
                ),
149
                (5, Value::Text(environment.clone())),
150
                (6, Value::Uint(u64::from(*approve))),
151
            ]),
152
        };
153
        Value::map([
154
            (0, Value::Uint(FORMAT_VERSION)),
155
            (1, Value::Text(self.kind().into())),
156
            (2, Value::Bytes(self.repo.to_vec())),
157
            (3, self.hlc.to_value()),
158
            (4, body),
159
        ])
160
    }
161
162
    pub fn encode(&self) -> Vec<u8> {
163
        cbor::encode(&self.to_value())
164
    }
165
166
    pub fn hash(&self) -> ObjectRef {
167
        hash_object(ObjectType::Attest, &self.encode())
168
    }
169
170
    pub fn decode(b: &[u8]) -> Result<Attest, ObjectError> {
171
        let v = cbor::decode(b)?;
172
        let m = map(&v, "attest")?;
173
        check_version(m)?;
174
        let kind = text(field(m, 1, "kind")?, "kind")?;
175
        let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?
176
            .try_into()
177
            .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?;
178
        let hlc = Hlc::from_value(field(m, 3, "hlc")?)?;
179
        let b = map(field(m, 4, "body")?, "body")?;
180
        let body = match kind {
181
            "ref" => AttestBody::Ref {
182
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
183
                old_state: oref(field(b, 1, "old_state")?, "old_state")?,
184
                new_state: oref(field(b, 2, "new_state")?, "new_state")?,
185
                serial: uint(field(b, 3, "serial")?, "serial")?,
186
            },
187
            "seal" => {
188
                let hashes = array(field(b, 0, "hashes")?, "hashes")?
189
                    .iter()
190
                    .map(|h| oref(h, "hash"))
191
                    .collect::<Result<Vec<_>, _>>()?;
192
                if hashes.is_empty()
193
                    || hashes.len() > MAX_SEAL_HASHES
194
                    || !hashes.windows(2).all(|w| w[0] < w[1])
195
                {
196
                    return Err(ObjectError::Schema(
197
                        "seal: hashes must be 1..4096, sorted and unique".into(),
198
                    ));
199
                }
200
                AttestBody::Seal { hashes }
201
            }
202
            "tag" => AttestBody::Tag {
203
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
204
                state: oref(field(b, 1, "state")?, "state")?,
205
                message: text(field(b, 2, "message")?, "message")?.to_owned(),
206
            },
207
            "rotate" => AttestBody::Rotate {
208
                new: oref(field(b, 0, "new")?, "new")?,
209
            },
210
            "revoke" => AttestBody::Revoke {
211
                key: oref(field(b, 0, "key")?, "key")?,
212
                reason: text(field(b, 1, "reason")?, "reason")?.to_owned(),
213
            },
214
            "receipt" => AttestBody::Receipt {
215
                receipt: oref(field(b, 0, "receipt")?, "receipt")?,
216
                state: oref(field(b, 1, "state")?, "state")?,
217
                run: text(field(b, 2, "run")?, "run")?.to_owned(),
218
            },
219
            "approval" => {
220
                let inputs = array(field(b, 4, "inputs")?, "inputs")?
221
                    .iter()
222
                    .map(|h| oref(h, "input"))
223
                    .collect::<Result<Vec<_>, _>>()?;
224
                if !inputs.windows(2).all(|w| w[0] < w[1]) {
225
                    return Err(ObjectError::Schema(
226
                        "approval: inputs must be sorted and unique".into(),
227
                    ));
228
                }
229
                AttestBody::Approval {
230
                    run: text(field(b, 0, "run")?, "run")?.to_owned(),
231
                    job: text(field(b, 1, "job")?, "job")?.to_owned(),
232
                    job_hash: oref(field(b, 2, "job_hash")?, "job_hash")?,
233
                    state: oref(field(b, 3, "state")?, "state")?,
234
                    inputs,
235
                    environment: text(field(b, 5, "environment")?, "environment")?.to_owned(),
236
                    approve: match uint(field(b, 6, "approve")?, "approve")? {
237
                        0 => false,
238
                        1 => true,
239
                        _ => return Err(ObjectError::Schema("approval: approve is 0 or 1".into())),
240
                    },
241
                }
242
            }
243
            "accepted" => AttestBody::Accepted {
244
                attest: oref(field(b, 0, "attest")?, "attest")?,
245
            },
246
            other => {
247
                return Err(ObjectError::Schema(format!(
248
                    "unknown attestation kind {other:?}"
249
                )))
250
            }
251
        };
252
        Ok(Attest { repo, hlc, body })
253
    }
254
}
255
256
#[cfg(test)]
257
mod tests {
258
    use super::*;
259
260
    #[test]
261
    fn round_trips() {
262
        let r = Attest {
263
            repo: [1; 16],
264
            hlc: Hlc {
265
                wall_ms: 3,
266
                logical: 0,
267
            },
268
            body: AttestBody::Ref {
269
                name: "main".into(),
270
                old_state: ObjectRef([1; 32]),
271
                new_state: ObjectRef([2; 32]),
272
                serial: 7,
273
            },
274
        };
275
        assert_eq!(Attest::decode(&r.encode()).unwrap(), r);
276
        assert_eq!(r.kind(), "ref");
277
        let s = Attest::seal(
278
            [1; 16],
279
            vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])],
280
        );
281
        let d = Attest::decode(&s.encode()).unwrap();
282
        assert!(
283
            matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])])
284
        );
285
        assert_ne!(s.hash(), r.hash());
286
        let t = Attest {
287
            repo: [1; 16],
288
            hlc: Hlc::default(),
289
            body: AttestBody::Tag {
290
                name: "v1".into(),
291
                state: ObjectRef([5; 32]),
292
                message: "release".into(),
293
            },
294
        };
295
        assert_eq!(Attest::decode(&t.encode()).unwrap(), t);
296
        assert_eq!(t.kind(), "tag");
297
    }
298
299
    #[test]
300
    fn rejects_bad_seals() {
301
        let empty = Attest {
302
            repo: [0; 16],
303
            hlc: Hlc::default(),
304
            body: AttestBody::Seal { hashes: vec![] },
305
        };
306
        assert!(Attest::decode(&empty.encode()).is_err());
307
        let unsorted = Attest {
308
            repo: [0; 16],
309
            hlc: Hlc::default(),
310
            body: AttestBody::Seal {
311
                hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])],
312
            },
313
        };
314
        assert!(Attest::decode(&unsorted.encode()).is_err());
315
    }
316
}