OXYGENHUB

Coverage / crates/oxy-core/src/approval.rs 94.8%

145 of 153 lines run, 12 of 26 functions · run #309 on main · run missed

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 {
420×
    fn from(e: ObjectError) -> Self {
430×
        ApprovalError::Schema(e.to_string())
440×
    }
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 {
554×
    pub fn sign(s: &Statement, signer: &Identity, sk: &SecretKey) -> SignedApproval {
564×
        let mut inputs = s.inputs.clone();
574×
        inputs.sort();
584×
        inputs.dedup();
594×
        let attest = Attest {
604×
            repo: s.repo,
614×
            hlc: Hlc::now(),
624×
            body: AttestBody::Approval {
634×
                run: s.run.clone(),
644×
                job: s.job.clone(),
654×
                job_hash: s.job_hash,
664×
                state: s.state,
674×
                inputs,
684×
                environment: s.environment.clone(),
694×
                approve: s.approve,
704×
            },
714×
        };
724×
        let sig = sk.sign(Domain::Attest, &signer.hash(), &attest.hash());
734×
        SignedApproval {
744×
            attest,
754×
            sigs: vec![sig],
764×
            signer: signer.clone(),
774×
        }
784×
    }
79
80
    /// What was approved, read back from the attestation.
8114×
    pub fn statement(&self) -> Result<Statement, ApprovalError> {
82
        let AttestBody::Approval {
8314×
            run,
8414×
            job,
8514×
            job_hash,
8614×
            state,
8714×
            inputs,
8814×
            environment,
8914×
            approve,
9014×
        } = &self.attest.body
91
        else {
920×
            return Err(ApprovalError::NotApproval);
93
        };
9414×
        Ok(Statement {
9514×
            repo: self.attest.repo,
9614×
            run: run.clone(),
9714×
            job: job.clone(),
9814×
            job_hash: *job_hash,
9914×
            state: *state,
10014×
            inputs: inputs.clone(),
10114×
            environment: environment.clone(),
10214×
            approve: *approve,
10314×
        })
10414×
    }
105
106
    /// It is an approval and the signer signed it. Whether the signer may
107
    /// approve is the reader's call.
1086×
    pub fn verify(&self) -> Result<(), ApprovalError> {
1096×
        self.statement()?;
1106×
        let key = self.signer.hash();
1116×
        let sig = self
1126×
            .sigs
1136×
            .iter()
1146×
            .find(|s| s.key == key)
1156×
            .ok_or_else(|| ApprovalError::Unsigned(key.short(12)))?;
1166×
        verify(&self.signer, sig, Domain::Attest, &self.attest.hash())
1176×
            .map_err(|e| ApprovalError::BadSignature(e.to_string()))
1186×
    }
119
120
    /// Whether `s` is what this approves (the time it was made aside).
1217×
    pub fn covers(&self, s: &Statement) -> bool {
1227×
        let mut want = s.inputs.clone();
1237×
        want.sort();
1247×
        want.dedup();
1257×
        self.statement().is_ok_and(|mine| {
1267×
            mine == Statement {
1277×
                inputs: want,
1287×
                ..s.clone()
1297×
            }
1307×
        })
1317×
    }
132
133
    /// `[attest, [Signature], identity]`, each object as its bytes.
1341×
    pub fn encode(&self) -> Vec<u8> {
1351×
        cbor::encode(&Value::Array(vec![
1361×
            Value::Bytes(self.attest.encode()),
1371×
            Value::Array(self.sigs.iter().map(|s| s.to_value()).collect()),
1381×
            Value::Bytes(self.signer.encode()),
139
        ]))
1401×
    }
141
1421×
    pub fn decode(b: &[u8]) -> Result<SignedApproval, ApprovalError> {
1431×
        let v = cbor::decode(b).map_err(ObjectError::from)?;
1441×
        let a = array(&v, "signed approval")?;
1451×
        if a.len() != 3 {
1460×
            return Err(ApprovalError::Schema(
1470×
                "signed approval: expected [attest, sigs, identity]".into(),
1480×
            ));
1491×
        }
150
        Ok(SignedApproval {
1511×
            attest: Attest::decode(bytes(&a[0], "attest")?)?,
1521×
            sigs: array(&a[1], "sigs")?
1531×
                .iter()
1541×
                .map(Signature::from_value)
1551×
                .collect::<Result<_, _>>()?,
1561×
            signer: Identity::decode(bytes(&a[2], "identity")?)?,
157
        })
1581×
    }
159
}
160
161
#[cfg(test)]
162
mod tests {
163
    use super::*;
164
    use crate::identity::SecretKey;
165
1669×
    fn statement() -> Statement {
1679×
        Statement {
1689×
            repo: [7; 16],
1699×
            run: "https://hub.example/andy/infra/flow/8".into(),
1709×
            job: "apply".into(),
1719×
            job_hash: ObjectRef([1; 32]),
1729×
            state: ObjectRef([2; 32]),
1739×
            inputs: vec![ObjectRef([4; 32]), ObjectRef([3; 32])],
1749×
            environment: "prod".into(),
1759×
            approve: true,
1769×
        }
1779×
    }
178
179
    #[test]
1801×
    fn an_approval_signs_exactly_one_job_state_plan_and_environment() {
1811×
        let sk = SecretKey::generate();
1821×
        let id = sk.identity("Andy", "[email protected]", 1);
1831×
        let sa = SignedApproval::sign(&statement(), &id, &sk);
1841×
        sa.verify().unwrap();
1851×
        let back = SignedApproval::decode(&sa.encode()).unwrap();
1861×
        assert_eq!(back, sa, "round trip");
1871×
        assert!(back.covers(&statement()), "inputs in any order");
1886×
        for other in [
1891×
            Statement {
1901×
                inputs: vec![ObjectRef([9; 32])],
1911×
                ..statement()
1921×
            },
1931×
            Statement {
1941×
                state: ObjectRef([8; 32]),
1951×
                ..statement()
1961×
            },
1971×
            Statement {
1981×
                job_hash: ObjectRef([8; 32]),
1991×
                ..statement()
2001×
            },
2011×
            Statement {
2021×
                run: "https://hub.example/andy/infra/flow/9".into(),
2031×
                ..statement()
2041×
            },
2051×
            Statement {
2061×
                environment: "dev".into(),
2071×
                ..statement()
2081×
            },
2091×
            Statement {
2101×
                approve: false,
2111×
                ..statement()
2121×
            },
2131×
        ] {
2146×
            assert!(!back.covers(&other), "{other:?}");
215
        }
216
        // Changed after signing: the signature no longer holds.
2171×
        let mut forged = sa.clone();
2181×
        if let AttestBody::Approval { inputs, .. } = &mut forged.attest.body {
2191×
            inputs[0] = ObjectRef([9; 32]);
2201×
        }
2211×
        assert!(forged.verify().is_err());
222
        // Signed by someone else than the identity it carries.
2231×
        let other_sk = SecretKey::generate();
2241×
        let mut stolen = SignedApproval::sign(&statement(), &id, &other_sk);
2251×
        stolen.signer = id.clone();
2261×
        assert!(stolen.verify().is_err());
2271×
    }
228
}