| 1 | |
|
| 2 | |
|
| 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 | | |
| 15 | | Ref { |
| 16 | | name: String, |
| 17 | | old_state: ObjectRef, |
| 18 | | new_state: ObjectRef, |
| 19 | | serial: u64, |
| 20 | | }, |
| 21 | | |
| 22 | | Seal { hashes: Vec<ChangeHash> }, |
| 23 | | |
| 24 | | Tag { |
| 25 | | name: String, |
| 26 | | state: ObjectRef, |
| 27 | | message: String, |
| 28 | | }, |
| 29 | | |
| 30 | | |
| 31 | | Untag { |
| 32 | | name: String, |
| 33 | | state: ObjectRef, |
| 34 | | reason: String, |
| 35 | | }, |
| 36 | | |
| 37 | | Rotate { new: KeyId }, |
| 38 | | |
| 39 | | Revoke { key: KeyId, reason: String }, |
| 40 | | |
| 41 | | |
| 42 | | Receipt { |
| 43 | | receipt: ObjectRef, |
| 44 | | state: ObjectRef, |
| 45 | | run: String, |
| 46 | | }, |
| 47 | | |
| 48 | | |
| 49 | | |
| 50 | | |
| 51 | | Approval { |
| 52 | | run: String, |
| 53 | | job: String, |
| 54 | | job_hash: ObjectRef, |
| 55 | | state: ObjectRef, |
| 56 | | inputs: Vec<ObjectRef>, |
| 57 | | environment: String, |
| 58 | | approve: bool, |
| 59 | | }, |
| 60 | | |
| 61 | | |
| 62 | | |
| 63 | | Accepted { attest: ObjectRef }, |
| 64 | | } |
| 65 | |
|
| 66 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 67 | | pub struct Attest { |
| 68 | | |
| 69 | | pub repo: [u8; 16], |
| 70 | | pub hlc: Hlc, |
| 71 | | pub body: AttestBody, |
| 72 | | } |
| 73 | |
|
| 74 | | impl Attest { |
| 75 | 1407× | pub fn kind(&self) -> &'static str { |
| 76 | 1407× | match self.body { |
| 77 | 753× | AttestBody::Ref { .. } => "ref", |
| 78 | 180× | AttestBody::Seal { .. } => "seal", |
| 79 | 292× | AttestBody::Tag { .. } => "tag", |
| 80 | 24× | AttestBody::Untag { .. } => "untag", |
| 81 | 20× | AttestBody::Rotate { .. } => "rotate", |
| 82 | 4× | AttestBody::Revoke { .. } => "revoke", |
| 83 | 41× | AttestBody::Receipt { .. } => "receipt", |
| 84 | 12× | AttestBody::Approval { .. } => "approval", |
| 85 | 81× | AttestBody::Accepted { .. } => "accepted", |
| 86 | | } |
| 87 | 1407× | } |
| 88 | |
|
| 89 | 56× | pub fn seal(repo: [u8; 16], mut hashes: Vec<ChangeHash>) -> Attest { |
| 90 | 56× | hashes.sort(); |
| 91 | 56× | hashes.dedup(); |
| 92 | 56× | Attest { |
| 93 | 56× | repo, |
| 94 | 56× | hlc: Hlc::now(), |
| 95 | 56× | body: AttestBody::Seal { hashes }, |
| 96 | 56× | } |
| 97 | 56× | } |
| 98 | |
|
| 99 | 1404× | pub fn to_value(&self) -> Value { |
| 100 | 1404× | let body = match &self.body { |
| 101 | | AttestBody::Ref { |
| 102 | 752× | name, |
| 103 | 752× | old_state, |
| 104 | 752× | new_state, |
| 105 | 752× | serial, |
| 106 | 752× | } => Value::map([ |
| 107 | 752× | (0, Value::Text(name.clone())), |
| 108 | 752× | (1, Value::Bytes(old_state.0.to_vec())), |
| 109 | 752× | (2, Value::Bytes(new_state.0.to_vec())), |
| 110 | 752× | (3, Value::Uint(*serial)), |
| 111 | 752× | ]), |
| 112 | 180× | AttestBody::Seal { hashes } => Value::map([( |
| 113 | | 0, |
| 114 | 191× | Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()), |
| 115 | | )]), |
| 116 | | AttestBody::Tag { |
| 117 | 291× | name, |
| 118 | 291× | state, |
| 119 | 291× | message, |
| 120 | 291× | } => Value::map([ |
| 121 | 291× | (0, Value::Text(name.clone())), |
| 122 | 291× | (1, Value::Bytes(state.0.to_vec())), |
| 123 | 291× | (2, Value::Text(message.clone())), |
| 124 | 291× | ]), |
| 125 | | AttestBody::Untag { |
| 126 | 23× | name, |
| 127 | 23× | state, |
| 128 | 23× | reason, |
| 129 | 23× | } => Value::map([ |
| 130 | 23× | (0, Value::Text(name.clone())), |
| 131 | 23× | (1, Value::Bytes(state.0.to_vec())), |
| 132 | 23× | (2, Value::Text(reason.clone())), |
| 133 | 23× | ]), |
| 134 | 20× | AttestBody::Rotate { new } => Value::map([(0, Value::Bytes(new.0.to_vec()))]), |
| 135 | 4× | AttestBody::Revoke { key, reason } => Value::map([ |
| 136 | 4× | (0, Value::Bytes(key.0.to_vec())), |
| 137 | 4× | (1, Value::Text(reason.clone())), |
| 138 | 4× | ]), |
| 139 | | AttestBody::Receipt { |
| 140 | 41× | receipt, |
| 141 | 41× | state, |
| 142 | 41× | run, |
| 143 | 41× | } => Value::map([ |
| 144 | 41× | (0, Value::Bytes(receipt.0.to_vec())), |
| 145 | 41× | (1, Value::Bytes(state.0.to_vec())), |
| 146 | 41× | (2, Value::Text(run.clone())), |
| 147 | 41× | ]), |
| 148 | 81× | AttestBody::Accepted { attest } => Value::map([(0, Value::Bytes(attest.0.to_vec()))]), |
| 149 | | AttestBody::Approval { |
| 150 | 12× | run, |
| 151 | 12× | job, |
| 152 | 12× | job_hash, |
| 153 | 12× | state, |
| 154 | 12× | inputs, |
| 155 | 12× | environment, |
| 156 | 12× | approve, |
| 157 | 12× | } => Value::map([ |
| 158 | 12× | (0, Value::Text(run.clone())), |
| 159 | 12× | (1, Value::Text(job.clone())), |
| 160 | 12× | (2, Value::Bytes(job_hash.0.to_vec())), |
| 161 | 12× | (3, Value::Bytes(state.0.to_vec())), |
| 162 | | ( |
| 163 | | 4, |
| 164 | 18× | Value::Array(inputs.iter().map(|i| Value::Bytes(i.0.to_vec())).collect()), |
| 165 | | ), |
| 166 | 12× | (5, Value::Text(environment.clone())), |
| 167 | 12× | (6, Value::Uint(u64::from(*approve))), |
| 168 | | ]), |
| 169 | | }; |
| 170 | 1404× | Value::map([ |
| 171 | 1404× | (0, Value::Uint(FORMAT_VERSION)), |
| 172 | 1404× | (1, Value::Text(self.kind().into())), |
| 173 | 1404× | (2, Value::Bytes(self.repo.to_vec())), |
| 174 | 1404× | (3, self.hlc.to_value()), |
| 175 | 1404× | (4, body), |
| 176 | 1404× | ]) |
| 177 | 1404× | } |
| 178 | |
|
| 179 | 1404× | pub fn encode(&self) -> Vec<u8> { |
| 180 | 1404× | cbor::encode(&self.to_value()) |
| 181 | 1404× | } |
| 182 | |
|
| 183 | 752× | pub fn hash(&self) -> ObjectRef { |
| 184 | 752× | hash_object(ObjectType::Attest, &self.encode()) |
| 185 | 752× | } |
| 186 | |
|
| 187 | 1315× | pub fn decode(b: &[u8]) -> Result<Attest, ObjectError> { |
| 188 | 1315× | let v = cbor::decode(b)?; |
| 189 | 1315× | let m = map(&v, "attest")?; |
| 190 | 1315× | check_version(m)?; |
| 191 | 1315× | let kind = text(field(m, 1, "kind")?, "kind")?; |
| 192 | 1315× | let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")? |
| 193 | 1315× | .try_into() |
| 194 | 1315× | .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?; |
| 195 | 1315× | let hlc = Hlc::from_value(field(m, 3, "hlc")?)?; |
| 196 | 1315× | let b = map(field(m, 4, "body")?, "body")?; |
| 197 | 1315× | let body = match kind { |
| 198 | 1315× | "ref" => AttestBody::Ref { |
| 199 | 500× | name: text(field(b, 0, "name")?, "name")?.to_owned(), |
| 200 | 500× | old_state: oref(field(b, 1, "old_state")?, "old_state")?, |
| 201 | 500× | new_state: oref(field(b, 2, "new_state")?, "new_state")?, |
| 202 | 500× | serial: uint(field(b, 3, "serial")?, "serial")?, |
| 203 | | }, |
| 204 | 815× | "seal" => { |
| 205 | 389× | let hashes = array(field(b, 0, "hashes")?, "hashes")? |
| 206 | 389× | .iter() |
| 207 | 425× | .map(|h| oref(h, "hash")) |
| 208 | 389× | .collect::<Result<Vec<_>, _>>()?; |
| 209 | 389× | if hashes.is_empty() |
| 210 | 388× | || hashes.len() > MAX_SEAL_HASHES |
| 211 | 388× | || !hashes.windows(2).all(|w| w[0] < w[1]) |
| 212 | | { |
| 213 | 2× | return Err(ObjectError::Schema( |
| 214 | 2× | "seal: hashes must be 1..4096, sorted and unique".into(), |
| 215 | 2× | )); |
| 216 | 387× | } |
| 217 | 387× | AttestBody::Seal { hashes } |
| 218 | | } |
| 219 | 426× | "tag" => AttestBody::Tag { |
| 220 | 325× | name: text(field(b, 0, "name")?, "name")?.to_owned(), |
| 221 | 325× | state: oref(field(b, 1, "state")?, "state")?, |
| 222 | 325× | message: text(field(b, 2, "message")?, "message")?.to_owned(), |
| 223 | | }, |
| 224 | 101× | "untag" => AttestBody::Untag { |
| 225 | 13× | name: text(field(b, 0, "name")?, "name")?.to_owned(), |
| 226 | 13× | state: oref(field(b, 1, "state")?, "state")?, |
| 227 | 13× | reason: text(field(b, 2, "reason")?, "reason")?.to_owned(), |
| 228 | | }, |
| 229 | 88× | "rotate" => AttestBody::Rotate { |
| 230 | 0× | new: oref(field(b, 0, "new")?, "new")?, |
| 231 | | }, |
| 232 | 88× | "revoke" => AttestBody::Revoke { |
| 233 | 0× | key: oref(field(b, 0, "key")?, "key")?, |
| 234 | 0× | reason: text(field(b, 1, "reason")?, "reason")?.to_owned(), |
| 235 | | }, |
| 236 | 88× | "receipt" => AttestBody::Receipt { |
| 237 | 21× | receipt: oref(field(b, 0, "receipt")?, "receipt")?, |
| 238 | 21× | state: oref(field(b, 1, "state")?, "state")?, |
| 239 | 21× | run: text(field(b, 2, "run")?, "run")?.to_owned(), |
| 240 | | }, |
| 241 | 67× | "approval" => { |
| 242 | 3× | let inputs = array(field(b, 4, "inputs")?, "inputs")? |
| 243 | 3× | .iter() |
| 244 | 4× | .map(|h| oref(h, "input")) |
| 245 | 3× | .collect::<Result<Vec<_>, _>>()?; |
| 246 | 3× | if !inputs.windows(2).all(|w| w[0] < w[1]) { |
| 247 | 0× | return Err(ObjectError::Schema( |
| 248 | 0× | "approval: inputs must be sorted and unique".into(), |
| 249 | 0× | )); |
| 250 | 3× | } |
| 251 | | AttestBody::Approval { |
| 252 | 3× | run: text(field(b, 0, "run")?, "run")?.to_owned(), |
| 253 | 3× | job: text(field(b, 1, "job")?, "job")?.to_owned(), |
| 254 | 3× | job_hash: oref(field(b, 2, "job_hash")?, "job_hash")?, |
| 255 | 3× | state: oref(field(b, 3, "state")?, "state")?, |
| 256 | 3× | inputs, |
| 257 | 3× | environment: text(field(b, 5, "environment")?, "environment")?.to_owned(), |
| 258 | 3× | approve: match uint(field(b, 6, "approve")?, "approve")? { |
| 259 | 0× | 0 => false, |
| 260 | 3× | 1 => true, |
| 261 | 0× | _ => return Err(ObjectError::Schema("approval: approve is 0 or 1".into())), |
| 262 | | }, |
| 263 | | } |
| 264 | | } |
| 265 | 64× | "accepted" => AttestBody::Accepted { |
| 266 | 64× | attest: oref(field(b, 0, "attest")?, "attest")?, |
| 267 | | }, |
| 268 | 0× | other => { |
| 269 | 0× | return Err(ObjectError::Schema(format!( |
| 270 | 0× | "unknown attestation kind {other:?}" |
| 271 | 0× | ))) |
| 272 | | } |
| 273 | | }; |
| 274 | 1313× | Ok(Attest { repo, hlc, body }) |
| 275 | 1315× | } |
| 276 | | } |
| 277 | |
|
| 278 | | #[cfg(test)] |
| 279 | | mod tests { |
| 280 | | use super::*; |
| 281 | |
|
| 282 | | #[test] |
| 283 | 1× | fn round_trips() { |
| 284 | 1× | let r = Attest { |
| 285 | 1× | repo: [1; 16], |
| 286 | 1× | hlc: Hlc { |
| 287 | 1× | wall_ms: 3, |
| 288 | 1× | logical: 0, |
| 289 | 1× | }, |
| 290 | 1× | body: AttestBody::Ref { |
| 291 | 1× | name: "main".into(), |
| 292 | 1× | old_state: ObjectRef([1; 32]), |
| 293 | 1× | new_state: ObjectRef([2; 32]), |
| 294 | 1× | serial: 7, |
| 295 | 1× | }, |
| 296 | 1× | }; |
| 297 | 1× | assert_eq!(Attest::decode(&r.encode()).unwrap(), r); |
| 298 | 1× | assert_eq!(r.kind(), "ref"); |
| 299 | 1× | let s = Attest::seal( |
| 300 | 1× | [1; 16], |
| 301 | 1× | vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])], |
| 302 | | ); |
| 303 | 1× | let d = Attest::decode(&s.encode()).unwrap(); |
| 304 | 1× | assert!( |
| 305 | 1× | matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])]) |
| 306 | | ); |
| 307 | 1× | assert_ne!(s.hash(), r.hash()); |
| 308 | 1× | let t = Attest { |
| 309 | 1× | repo: [1; 16], |
| 310 | 1× | hlc: Hlc::default(), |
| 311 | 1× | body: AttestBody::Tag { |
| 312 | 1× | name: "v1".into(), |
| 313 | 1× | state: ObjectRef([5; 32]), |
| 314 | 1× | message: "release".into(), |
| 315 | 1× | }, |
| 316 | 1× | }; |
| 317 | 1× | assert_eq!(Attest::decode(&t.encode()).unwrap(), t); |
| 318 | 1× | assert_eq!(t.kind(), "tag"); |
| 319 | 1× | let u = Attest { |
| 320 | 1× | repo: [1; 16], |
| 321 | 1× | hlc: Hlc::default(), |
| 322 | 1× | body: AttestBody::Untag { |
| 323 | 1× | name: "v1".into(), |
| 324 | 1× | state: ObjectRef([5; 32]), |
| 325 | 1× | reason: "tagged the wrong change".into(), |
| 326 | 1× | }, |
| 327 | 1× | }; |
| 328 | 1× | assert_eq!(Attest::decode(&u.encode()).unwrap(), u); |
| 329 | 1× | assert_eq!(u.kind(), "untag"); |
| 330 | 1× | assert_ne!(u.hash(), t.hash()); |
| 331 | 1× | } |
| 332 | |
|
| 333 | | #[test] |
| 334 | 1× | fn rejects_bad_seals() { |
| 335 | 1× | let empty = Attest { |
| 336 | 1× | repo: [0; 16], |
| 337 | 1× | hlc: Hlc::default(), |
| 338 | 1× | body: AttestBody::Seal { hashes: vec![] }, |
| 339 | 1× | }; |
| 340 | 1× | assert!(Attest::decode(&empty.encode()).is_err()); |
| 341 | 1× | let unsorted = Attest { |
| 342 | 1× | repo: [0; 16], |
| 343 | 1× | hlc: Hlc::default(), |
| 344 | 1× | body: AttestBody::Seal { |
| 345 | 1× | hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])], |
| 346 | 1× | }, |
| 347 | 1× | }; |
| 348 | 1× | assert!(Attest::decode(&unsorted.encode()).is_err()); |
| 349 | 1× | } |
| 350 | | } |