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