OXYGENHUB
◆ v0.9.11 oxygenhub / crates / hub / src / provenance.rs
4.1 KiB · Rust · last changed Provenance is built on oxy-core's DSSE, with one build type everywhere 19 days ago source blame history raw
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.
43
pub fn statement(base_url: &str, s: &Subject) -> serde_json::Value {
44
    use sha2::Digest;
45
    let sha256 = hex::encode(sha2::Sha256::digest(s.bytes));
46
    let when = |ms: Option<i64>| {
47
        ms.and_then(|t| time::OffsetDateTime::from_unix_timestamp_nanos(t as i128 * 1_000_000).ok())
48
            .and_then(|t| t.format(&time::format_description::well_known::Rfc3339).ok())
49
    };
50
    let mut metadata = serde_json::json!({ "invocationId": s.run_url });
51
    if let Some(t) = when(s.started_ms) {
52
        metadata["startedOn"] = t.into();
53
    }
54
    if let Some(t) = when(s.finished_ms) {
55
        metadata["finishedOn"] = t.into();
56
    }
57
    serde_json::json!({
58
        "_type": "https://in-toto.io/Statement/v1",
59
        "subject": [{ "name": s.name, "digest": { "sha256": sha256, "blake3": blake3::hash(s.bytes).to_hex().to_string() } }],
60
        "predicateType": "https://slsa.dev/provenance/v1",
61
        "predicate": {
62
            "buildDefinition": {
63
                "buildType": BUILD_TYPE,
64
                "externalParameters": {
65
                    "repository": format!("{base_url}/{}", s.repo),
66
                    "bookmark": s.bookmark,
67
                    "job": s.job,
68
                    "pipeline": s.pipeline_hash,
69
                },
70
                "resolvedDependencies": [{
71
                    "uri": format!("{base_url}/{}", s.repo),
72
                    "digest": { "oxygen-state": s.state },
73
                    "annotations": { "heads": s.heads },
74
                }],
75
            },
76
            "runDetails": {
77
                "builder": {
78
                    "id": format!("{base_url}/oxygenflow/runner/{}", s.runner),
79
                },
80
                "metadata": metadata,
81
                "byproducts": [{
82
                    "name": "oxygen-receipt",
83
                    "digest": { "oxygen-attestation": s.receipt_attest },
84
                    "annotations": { "signedBy": s.receipt_signer },
85
                }],
86
            },
87
        },
88
    })
89
}
90
91
/// `statement` in a DSSE envelope signed with the hub's key.
92
pub fn envelope(app: &Shared, statement: &serde_json::Value) -> anyhow::Result<serde_json::Value> {
93
    let payload = serde_json::to_vec(statement)?;
94
    let (key, secret) = crate::keys::load_secret(&app.config.keys_dir())?;
95
    Ok(serde_json::from_str(&oxy_core::dsse::envelope(PAYLOAD_TYPE, &payload, &key.to_hex(), &secret))?)
96
}
97
98
/// The hub's Ed25519 public key as a PEM SubjectPublicKeyInfo.
99
pub fn public_pem(app: &Shared) -> String {
100
    oxy_core::dsse::public_pem(&app.hub_key.identity)
101
}