OXYGENHUB
# c96354484bd9 oxygen / crates / oxy-core / src / approval.rs
7.2 KiB · Rust · last changed Sealed approvals: the approval attestation and oxy seal --flow 20 days ago source blame history raw
b582948f Andy McCall ↶ 1
//! A person's approval of one job of one run, signed (oxygenhub#235): the
2
//! approval attestation, the signer's signature over it, and the signer's
3
//! identity, so it verifies with nothing else. The hub decides whether the
4
//! signer may approve; the signature makes the decision theirs, and binds
5
//! it to exactly that job, state, plan and environment.
6
7
use crate::attest::{Attest, AttestBody};
8
use crate::cbor::{self, Value};
9
use crate::hash::ObjectRef;
10
use crate::identity::{verify, Domain, Identity, SecretKey, Signature};
11
use crate::objects::{array, bytes, Hlc, ObjectError};
12
13
/// What is approved.
14
#[derive(Clone, Debug, PartialEq, Eq)]
15
pub struct Statement {
16
    pub repo: [u8; 16],
17
    /// The run, as its URL on the hub.
18
    pub run: String,
19
    pub job: String,
20
    /// The job's hash with its sandbox (OxygenFlow spec 6.2).
21
    pub job_hash: ObjectRef,
22
    pub state: ObjectRef,
23
    /// The blobs the job takes as inputs (a plan), any order.
24
    pub inputs: Vec<ObjectRef>,
25
    pub environment: String,
26
    pub approve: bool,
27
}
28
29
#[derive(Debug, thiserror::Error)]
30
pub enum ApprovalError {
31
    #[error("the attestation is not an approval")]
32
    NotApproval,
33
    #[error("no signature by the signer {0}")]
34
    Unsigned(String),
35
    #[error("the signature does not verify: {0}")]
36
    BadSignature(String),
37
    #[error("{0}")]
38
    Schema(String),
39
}
40
41
impl From<ObjectError> for ApprovalError {
42
    fn from(e: ObjectError) -> Self {
43
        ApprovalError::Schema(e.to_string())
44
    }
45
}
46
47
#[derive(Clone, Debug, PartialEq, Eq)]
48
pub struct SignedApproval {
49
    pub attest: Attest,
50
    pub sigs: Vec<Signature>,
51
    pub signer: Identity,
52
}
53
54
impl SignedApproval {
55
    pub fn sign(s: &Statement, signer: &Identity, sk: &SecretKey) -> SignedApproval {
56
        let mut inputs = s.inputs.clone();
57
        inputs.sort();
58
        inputs.dedup();
59
        let attest = Attest {
60
            repo: s.repo,
61
            hlc: Hlc::now(),
62
            body: AttestBody::Approval {
63
                run: s.run.clone(),
64
                job: s.job.clone(),
65
                job_hash: s.job_hash,
66
                state: s.state,
67
                inputs,
68
                environment: s.environment.clone(),
69
                approve: s.approve,
70
            },
71
        };
72
        let sig = sk.sign(Domain::Attest, &signer.hash(), &attest.hash());
73
        SignedApproval {
74
            attest,
75
            sigs: vec![sig],
76
            signer: signer.clone(),
77
        }
78
    }
79
80
    /// What was approved, read back from the attestation.
81
    pub fn statement(&self) -> Result<Statement, ApprovalError> {
82
        let AttestBody::Approval {
83
            run,
84
            job,
85
            job_hash,
86
            state,
87
            inputs,
88
            environment,
89
            approve,
90
        } = &self.attest.body
91
        else {
92
            return Err(ApprovalError::NotApproval);
93
        };
94
        Ok(Statement {
95
            repo: self.attest.repo,
96
            run: run.clone(),
97
            job: job.clone(),
98
            job_hash: *job_hash,
99
            state: *state,
100
            inputs: inputs.clone(),
101
            environment: environment.clone(),
102
            approve: *approve,
103
        })
104
    }
105
106
    /// It is an approval and the signer signed it. Whether the signer may
107
    /// approve is the reader's call.
108
    pub fn verify(&self) -> Result<(), ApprovalError> {
109
        self.statement()?;
110
        let key = self.signer.hash();
111
        let sig = self
112
            .sigs
113
            .iter()
114
            .find(|s| s.key == key)
115
            .ok_or_else(|| ApprovalError::Unsigned(key.short(12)))?;
116
        verify(&self.signer, sig, Domain::Attest, &self.attest.hash())
117
            .map_err(|e| ApprovalError::BadSignature(e.to_string()))
118
    }
119
120
    /// Whether `s` is what this approves (the time it was made aside).
121
    pub fn covers(&self, s: &Statement) -> bool {
122
        let mut want = s.inputs.clone();
123
        want.sort();
124
        want.dedup();
125
        self.statement().is_ok_and(|mine| {
126
            mine == Statement {
127
                inputs: want,
128
                ..s.clone()
129
            }
130
        })
131
    }
132
133
    /// `[attest, [Signature], identity]`, each object as its bytes.
134
    pub fn encode(&self) -> Vec<u8> {
135
        cbor::encode(&Value::Array(vec![
136
            Value::Bytes(self.attest.encode()),
137
            Value::Array(self.sigs.iter().map(|s| s.to_value()).collect()),
138
            Value::Bytes(self.signer.encode()),
139
        ]))
140
    }
141
142
    pub fn decode(b: &[u8]) -> Result<SignedApproval, ApprovalError> {
143
        let v = cbor::decode(b).map_err(ObjectError::from)?;
144
        let a = array(&v, "signed approval")?;
145
        if a.len() != 3 {
146
            return Err(ApprovalError::Schema(
147
                "signed approval: expected [attest, sigs, identity]".into(),
148
            ));
149
        }
150
        Ok(SignedApproval {
151
            attest: Attest::decode(bytes(&a[0], "attest")?)?,
152
            sigs: array(&a[1], "sigs")?
153
                .iter()
154
                .map(Signature::from_value)
155
                .collect::<Result<_, _>>()?,
156
            signer: Identity::decode(bytes(&a[2], "identity")?)?,
157
        })
158
    }
159
}
160
161
#[cfg(test)]
162
mod tests {
163
    use super::*;
164
    use crate::identity::SecretKey;
165
166
    fn statement() -> Statement {
167
        Statement {
168
            repo: [7; 16],
169
            run: "https://hub.example/andy/infra/flow/8".into(),
170
            job: "apply".into(),
171
            job_hash: ObjectRef([1; 32]),
172
            state: ObjectRef([2; 32]),
173
            inputs: vec![ObjectRef([4; 32]), ObjectRef([3; 32])],
174
            environment: "prod".into(),
175
            approve: true,
176
        }
177
    }
178
179
    #[test]
180
    fn an_approval_signs_exactly_one_job_state_plan_and_environment() {
181
        let sk = SecretKey::generate();
182
        let id = sk.identity("Andy", "[email protected]", 1);
183
        let sa = SignedApproval::sign(&statement(), &id, &sk);
184
        sa.verify().unwrap();
185
        let back = SignedApproval::decode(&sa.encode()).unwrap();
186
        assert_eq!(back, sa, "round trip");
187
        assert!(back.covers(&statement()), "inputs in any order");
188
        for other in [
189
            Statement {
190
                inputs: vec![ObjectRef([9; 32])],
191
                ..statement()
192
            },
193
            Statement {
194
                state: ObjectRef([8; 32]),
195
                ..statement()
196
            },
197
            Statement {
198
                job_hash: ObjectRef([8; 32]),
199
                ..statement()
200
            },
201
            Statement {
202
                run: "https://hub.example/andy/infra/flow/9".into(),
203
                ..statement()
204
            },
205
            Statement {
206
                environment: "dev".into(),
207
                ..statement()
208
            },
209
            Statement {
210
                approve: false,
211
                ..statement()
212
            },
213
        ] {
214
            assert!(!back.covers(&other), "{other:?}");
215
        }
216
        // Changed after signing: the signature no longer holds.
217
        let mut forged = sa.clone();
218
        if let AttestBody::Approval { inputs, .. } = &mut forged.attest.body {
219
            inputs[0] = ObjectRef([9; 32]);
220
        }
221
        assert!(forged.verify().is_err());
222
        // Signed by someone else than the identity it carries.
223
        let other_sk = SecretKey::generate();
224
        let mut stolen = SignedApproval::sign(&statement(), &id, &other_sk);
225
        stolen.signer = id.clone();
226
        assert!(stolen.verify().is_err());
227
    }
228
}