| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 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 |
|
| 14 |
#[derive(Clone, Debug, PartialEq, Eq)] |
| 15 |
pub struct Statement { |
| 16 |
pub repo: [u8; 16], |
| 17 |
|
| 18 |
pub run: String, |
| 19 |
pub job: String, |
| 20 |
|
| 21 |
pub job_hash: ObjectRef, |
| 22 |
pub state: ObjectRef, |
| 23 |
|
| 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 |
|
| 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 |
|
| 107 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
} |