//! Provenance others can verify (oxygenhub#239): a Flow artifact's in-toto //! Statement with an SLSA provenance v1 predicate, in a DSSE envelope signed //! with the hub's Ed25519 key, as `cosign verify-blob-attestation` and other //! in-toto and SLSA verifiers read it. The runner signed the job's receipt //! with a key the hub holds no secret of; the hub checked that receipt when //! it kept it, and this envelope is the hub's statement of what the receipt //! says, in the shape the wider tools understand. `/.well-known/oxyhub-signing.pem` //! is the public key, as cosign's `--key` takes it. use crate::Shared; pub use oxy_core::dsse::pae; /// DSSE's payload type for in-toto statements. pub const PAYLOAD_TYPE: &str = oxy_core::dsse::IN_TOTO; /// What SLSA calls the build type of a Flow job: one URI wherever the job /// ran, the same `oxy receipt export --format slsa` writes. pub const BUILD_TYPE: &str = "https://oxygenhub.io/oxygenflow/job/v1"; /// What the statement says of one artifact. pub struct Subject<'a> { /// The artifact's file name. pub name: &'a str, pub bytes: &'a [u8], /// `owner/name` and the hub's URL of the run. pub repo: &'a str, pub run_url: &'a str, pub bookmark: &'a str, /// The state the run built, and the heads it is. pub state: &'a str, pub heads: &'a str, pub pipeline_hash: &'a str, pub job: &'a str, /// The runner that ran it and signed its receipt. pub runner: &'a str, pub receipt_signer: &'a str, pub receipt_attest: &'a str, pub started_ms: Option, pub finished_ms: Option, } /// The in-toto Statement v1 with an SLSA provenance v1 predicate. pub fn statement(base_url: &str, s: &Subject) -> serde_json::Value { use sha2::Digest; let sha256 = hex::encode(sha2::Sha256::digest(s.bytes)); let when = |ms: Option| { ms.and_then(|t| time::OffsetDateTime::from_unix_timestamp_nanos(t as i128 * 1_000_000).ok()) .and_then(|t| t.format(&time::format_description::well_known::Rfc3339).ok()) }; let mut metadata = serde_json::json!({ "invocationId": s.run_url }); if let Some(t) = when(s.started_ms) { metadata["startedOn"] = t.into(); } if let Some(t) = when(s.finished_ms) { metadata["finishedOn"] = t.into(); } serde_json::json!({ "_type": "https://in-toto.io/Statement/v1", "subject": [{ "name": s.name, "digest": { "sha256": sha256, "blake3": blake3::hash(s.bytes).to_hex().to_string() } }], "predicateType": "https://slsa.dev/provenance/v1", "predicate": { "buildDefinition": { "buildType": BUILD_TYPE, "externalParameters": { "repository": format!("{base_url}/{}", s.repo), "bookmark": s.bookmark, "job": s.job, "pipeline": s.pipeline_hash, }, "resolvedDependencies": [{ "uri": format!("{base_url}/{}", s.repo), "digest": { "oxygen-state": s.state }, "annotations": { "heads": s.heads }, }], }, "runDetails": { "builder": { "id": format!("{base_url}/oxygenflow/runner/{}", s.runner), }, "metadata": metadata, "byproducts": [{ "name": "oxygen-receipt", "digest": { "oxygen-attestation": s.receipt_attest }, "annotations": { "signedBy": s.receipt_signer }, }], }, }, }) } /// `statement` in a DSSE envelope signed with the hub's key. pub fn envelope(app: &Shared, statement: &serde_json::Value) -> anyhow::Result { let payload = serde_json::to_vec(statement)?; let (key, secret) = crate::keys::load_secret(&app.config.keys_dir())?; Ok(serde_json::from_str(&oxy_core::dsse::envelope(PAYLOAD_TYPE, &payload, &key.to_hex(), &secret))?) } /// The hub's Ed25519 public key as a PEM SubjectPublicKeyInfo. pub fn public_pem(app: &Shared) -> String { oxy_core::dsse::public_pem(&app.hub_key.identity) }