OXYGENHUB
# da58bbbbc4d9 oxygen / crates / oxy-core / src / attest.rs
6.4 KiB · Rust · last changed Identity lifecycle: rotate, renew, delegates, revoke; expiry checks; policy roots, thresholds, proposals and oxy policy sign; policy chains served on refs and carried in push bundles (spec 0.6n) 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::{KeyId, hash_object, ChangeHash, ObjectRef, ObjectType};
6
use crate::objects::{array, bytes, check_version, field, map, oref, text, uint, Hlc, ObjectError, FORMAT_VERSION};
7
8
pub const MAX_SEAL_HASHES: usize = 4096;
9
10
#[derive(Clone, Debug, PartialEq, Eq)]
11
pub enum AttestBody {
12
    /// A bookmark moved from `old_state` to `new_state` (spec §10.2).
13
    Ref { name: String, old_state: ObjectRef, new_state: ObjectRef, serial: u64 },
14
    /// One signature covering a batch of changes (spec §14.4).
15
    Seal { hashes: Vec<ChangeHash> },
16
    /// An immutable named state (spec §10.1).
17
    Tag { name: String, state: ObjectRef, message: String },
18
    /// The signing key's identity was replaced by `new` (spec §14.2).
19
    Rotate { new: KeyId },
20
    /// `key` must no longer be trusted (spec §14.2).
21
    Revoke { key: KeyId, reason: String },
22
}
23
24
#[derive(Clone, Debug, PartialEq, Eq)]
25
pub struct Attest {
26
    /// Repository ID chosen at init.
27
    pub repo: [u8; 16],
28
    pub hlc: Hlc,
29
    pub body: AttestBody,
30
}
31
32
impl Attest {
33
    pub fn kind(&self) -> &'static str {
34
        match self.body {
35
            AttestBody::Ref { .. } => "ref",
36
            AttestBody::Seal { .. } => "seal",
37
            AttestBody::Tag { .. } => "tag",
38
            AttestBody::Rotate { .. } => "rotate",
39
            AttestBody::Revoke { .. } => "revoke",
40
        }
41
    }
42
43
    pub fn seal(repo: [u8; 16], mut hashes: Vec<ChangeHash>) -> Attest {
44
        hashes.sort();
45
        hashes.dedup();
46
        Attest { repo, hlc: Hlc::now(), body: AttestBody::Seal { hashes } }
47
    }
48
49
    pub fn to_value(&self) -> Value {
50
        let body = match &self.body {
51
            AttestBody::Ref { name, old_state, new_state, serial } => Value::map([
52
                (0, Value::Text(name.clone())),
53
                (1, Value::Bytes(old_state.0.to_vec())),
54
                (2, Value::Bytes(new_state.0.to_vec())),
55
                (3, Value::Uint(*serial)),
56
            ]),
57
            AttestBody::Seal { hashes } => {
58
                Value::map([(0, Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()))])
59
            }
60
            AttestBody::Tag { name, state, message } => {
61
                Value::map([(0, Value::Text(name.clone())), (1, Value::Bytes(state.0.to_vec())), (2, Value::Text(message.clone()))])
62
            }
63
            AttestBody::Rotate { new } => Value::map([(0, Value::Bytes(new.0.to_vec()))]),
64
            AttestBody::Revoke { key, reason } => Value::map([(0, Value::Bytes(key.0.to_vec())), (1, Value::Text(reason.clone()))]),
65
        };
66
        Value::map([
67
            (0, Value::Uint(FORMAT_VERSION)),
68
            (1, Value::Text(self.kind().into())),
69
            (2, Value::Bytes(self.repo.to_vec())),
70
            (3, self.hlc.to_value()),
71
            (4, body),
72
        ])
73
    }
74
75
    pub fn encode(&self) -> Vec<u8> {
76
        cbor::encode(&self.to_value())
77
    }
78
79
    pub fn hash(&self) -> ObjectRef {
80
        hash_object(ObjectType::Attest, &self.encode())
81
    }
82
83
    pub fn decode(b: &[u8]) -> Result<Attest, ObjectError> {
84
        let v = cbor::decode(b)?;
85
        let m = map(&v, "attest")?;
86
        check_version(m)?;
87
        let kind = text(field(m, 1, "kind")?, "kind")?;
88
        let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")?
89
            .try_into()
90
            .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?;
91
        let hlc = Hlc::from_value(field(m, 3, "hlc")?)?;
92
        let b = map(field(m, 4, "body")?, "body")?;
93
        let body = match kind {
94
            "ref" => AttestBody::Ref {
95
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
96
                old_state: oref(field(b, 1, "old_state")?, "old_state")?,
97
                new_state: oref(field(b, 2, "new_state")?, "new_state")?,
98
                serial: uint(field(b, 3, "serial")?, "serial")?,
99
            },
100
            "seal" => {
101
                let hashes = array(field(b, 0, "hashes")?, "hashes")?.iter().map(|h| oref(h, "hash")).collect::<Result<Vec<_>, _>>()?;
102
                if hashes.is_empty() || hashes.len() > MAX_SEAL_HASHES || !hashes.windows(2).all(|w| w[0] < w[1]) {
103
                    return Err(ObjectError::Schema("seal: hashes must be 1..4096, sorted and unique".into()));
104
                }
105
                AttestBody::Seal { hashes }
106
            }
107
            "tag" => AttestBody::Tag {
108
                name: text(field(b, 0, "name")?, "name")?.to_owned(),
109
                state: oref(field(b, 1, "state")?, "state")?,
110
                message: text(field(b, 2, "message")?, "message")?.to_owned(),
111
            },
112
            "rotate" => AttestBody::Rotate { new: oref(field(b, 0, "new")?, "new")? },
113
            "revoke" => AttestBody::Revoke { key: oref(field(b, 0, "key")?, "key")?, reason: text(field(b, 1, "reason")?, "reason")?.to_owned() },
114
            other => return Err(ObjectError::Schema(format!("unknown attestation kind {other:?}"))),
115
        };
116
        Ok(Attest { repo, hlc, body })
117
    }
118
}
119
120
#[cfg(test)]
121
mod tests {
122
    use super::*;
123
124
    #[test]
125
    fn round_trips() {
126
        let r = Attest {
127
            repo: [1; 16],
128
            hlc: Hlc { wall_ms: 3, logical: 0 },
129
            body: AttestBody::Ref { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), serial: 7 },
130
        };
131
        assert_eq!(Attest::decode(&r.encode()).unwrap(), r);
132
        assert_eq!(r.kind(), "ref");
133
        let s = Attest::seal([1; 16], vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])]);
134
        let d = Attest::decode(&s.encode()).unwrap();
135
        assert!(matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])]));
136
        assert_ne!(s.hash(), r.hash());
137
        let t = Attest { repo: [1; 16], hlc: Hlc::default(), body: AttestBody::Tag { name: "v1".into(), state: ObjectRef([5; 32]), message: "release".into() } };
138
        assert_eq!(Attest::decode(&t.encode()).unwrap(), t);
139
        assert_eq!(t.kind(), "tag");
140
    }
141
142
    #[test]
143
    fn rejects_bad_seals() {
144
        let empty = Attest { repo: [0; 16], hlc: Hlc::default(), body: AttestBody::Seal { hashes: vec![] } };
145
        assert!(Attest::decode(&empty.encode()).is_err());
146
        let unsorted = Attest { repo: [0; 16], hlc: Hlc::default(), body: AttestBody::Seal { hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])] } };
147
        assert!(Attest::decode(&unsorted.encode()).is_err());
148
    }
149
}