OXYGENHUB

Coverage / crates/hub/src/provenance.rs 100.0%

49 of 49 lines run, 6 of 12 functions · run #522 on main · run missed

1
//! Provenance others can verify (oxygenhub#239): a Flow artifact's in-toto
2
//! Statement with an SLSA provenance v1 predicate, in a DSSE envelope signed
3
//! with the hub's Ed25519 key, as `cosign verify-blob-attestation` and other
4
//! in-toto and SLSA verifiers read it. The runner signed the job's receipt
5
//! with a key the hub holds no secret of; the hub checked that receipt when
6
//! it kept it, and this envelope is the hub's statement of what the receipt
7
//! says, in the shape the wider tools understand. `/.well-known/oxyhub-signing.pem`
8
//! is the public key, as cosign's `--key` takes it.
9
10
use crate::Shared;
11
pub use oxy_core::dsse::pae;
12
13
/// DSSE's payload type for in-toto statements.
14
pub const PAYLOAD_TYPE: &str = oxy_core::dsse::IN_TOTO;
15
16
/// What SLSA calls the build type of a Flow job: one URI wherever the job
17
/// ran, the same `oxy receipt export --format slsa` writes.
18
pub const BUILD_TYPE: &str = "https://oxygenhub.io/oxygenflow/job/v1";
19
20
/// What the statement says of one artifact.
21
pub struct Subject<'a> {
22
    /// The artifact's file name.
23
    pub name: &'a str,
24
    pub bytes: &'a [u8],
25
    /// `owner/name` and the hub's URL of the run.
26
    pub repo: &'a str,
27
    pub run_url: &'a str,
28
    pub bookmark: &'a str,
29
    /// The state the run built, and the heads it is.
30
    pub state: &'a str,
31
    pub heads: &'a str,
32
    pub pipeline_hash: &'a str,
33
    pub job: &'a str,
34
    /// The runner that ran it and signed its receipt.
35
    pub runner: &'a str,
36
    pub receipt_signer: &'a str,
37
    pub receipt_attest: &'a str,
38
    pub started_ms: Option<i64>,
39
    pub finished_ms: Option<i64>,
40
}
41
42
/// The in-toto Statement v1 with an SLSA provenance v1 predicate.
431×
pub fn statement(base_url: &str, s: &Subject) -> serde_json::Value {
44
    use sha2::Digest;
451×
    let sha256 = hex::encode(sha2::Sha256::digest(s.bytes));
462×
    let when = |ms: Option<i64>| {
472×
        ms.and_then(|t| time::OffsetDateTime::from_unix_timestamp_nanos(t as i128 * 1_000_000).ok())
482×
            .and_then(|t| t.format(&time::format_description::well_known::Rfc3339).ok())
492×
    };
501×
    let mut metadata = serde_json::json!({ "invocationId": s.run_url });
511×
    if let Some(t) = when(s.started_ms) {
521×
        metadata["startedOn"] = t.into();
531×
    }
541×
    if let Some(t) = when(s.finished_ms) {
551×
        metadata["finishedOn"] = t.into();
561×
    }
571×
    serde_json::json!({
581×
        "_type": "https://in-toto.io/Statement/v1",
591×
        "subject": [{ "name": s.name, "digest": { "sha256": sha256, "blake3": blake3::hash(s.bytes).to_hex().to_string() } }],
601×
        "predicateType": "https://slsa.dev/provenance/v1",
611×
        "predicate": {
621×
            "buildDefinition": {
631×
                "buildType": BUILD_TYPE,
641×
                "externalParameters": {
651×
                    "repository": format!("{base_url}/{}", s.repo),
661×
                    "bookmark": s.bookmark,
671×
                    "job": s.job,
681×
                    "pipeline": s.pipeline_hash,
69
                },
701×
                "resolvedDependencies": [{
711×
                    "uri": format!("{base_url}/{}", s.repo),
721×
                    "digest": { "oxygen-state": s.state },
731×
                    "annotations": { "heads": s.heads },
74
                }],
75
            },
761×
            "runDetails": {
771×
                "builder": {
781×
                    "id": format!("{base_url}/oxygenflow/runner/{}", s.runner),
79
                },
801×
                "metadata": metadata,
811×
                "byproducts": [{
821×
                    "name": "oxygen-receipt",
831×
                    "digest": { "oxygen-attestation": s.receipt_attest },
841×
                    "annotations": { "signedBy": s.receipt_signer },
85
                }],
86
            },
87
        },
88
    })
891×
}
90
91
/// `statement` in a DSSE envelope signed with the hub's key.
921×
pub fn envelope(app: &Shared, statement: &serde_json::Value) -> anyhow::Result<serde_json::Value> {
931×
    let payload = serde_json::to_vec(statement)?;
941×
    let (key, secret) = crate::keys::load_secret(&app.config.keys_dir())?;
951×
    Ok(serde_json::from_str(&oxy_core::dsse::envelope(PAYLOAD_TYPE, &payload, &key.to_hex(), &secret))?)
961×
}
97
98
/// The hub's Ed25519 public key as a PEM SubjectPublicKeyInfo.
991×
pub fn public_pem(app: &Shared) -> String {
1001×
    oxy_core::dsse::public_pem(&app.hub_key.identity)
1011×
}