OXYGENHUB

Release assets link their SLSA provenance, so cosign checks them 97fc47fee9cf

An asset built by a job with a kept signed receipt now links the
provenance of its artifact (the same bytes, matched by BLAKE3): on the
release page and as built_by.provenance_url in the API. cosign
verify-blob-attestation --key oxyhub-signing.pem --type slsaprovenance1
--signature PROVENANCE ASSET checks a downloaded asset. Spec 6.x says so;
the release test checks both the unsigned and signed cases.

Refs #239
Author
Andy McCall @andymccall <[email protected]>
via
agent Claude Code <[email protected]> · delegate for changes, until 2026-11-13 19:00Z · key ea5f78160088
When
· 2026-10-17 05:19 UTC
Signature
✔ signed by Claude Code
Hash
97fc47fee9cf0c22e3a692e044097ed10987203d23ac1c208307af465c6ec467
Depends on
10708ab8f54d 3f4b0ea6eabf 42fa95540468 93be9ce4653e a3c8288b0141 d23464c0ac60 df940d75cd3d
Depended on by
d2c1d6a7d12e
In bookmarks
api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
In tags
v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
8 atoms, 0 tree operations, 5 files

Browse the tree at this change

modified crates/hub-api/src/lib.rs +6 −0
…
838 unchanged lines
839 839
    pub receipt: Option<String>,
840 840
    pub receipt_url: Option<String>,
841 841
    pub signer: Option<String>,
842 +
    /// The asset's SLSA provenance, signed by the hub (oxygenhub#239):
843 +
    /// `cosign verify-blob-attestation --key …/.well-known/oxyhub-signing.pem
844 +
    /// --type slsaprovenance1 --signature PROVENANCE ASSET`. `None` when the
845 +
    /// receipt was not signed.
846 +
    #[serde(default)]
847 +
    pub provenance_url: Option<String>,
842 848
}
843 849
844 850
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
…
74 unchanged lines
modified crates/hub/src/web/releases.rs +12 −0
…
501 unchanged lines
502 502
                        receipt: signed.then(|| p.receipt_attest.clone()),
503 503
                        receipt_url: signed.then(|| format!("{base}/receipts/{}", p.receipt_attest)),
504 504
                        signer: signed.then(|| p.receipt_signer.clone()),
505 +
                        provenance_url: signed.then(|| provenance_url(&base, p)),
505 506
                    }
506 507
                }),
507 508
            })
…
7 unchanged lines
515 516
pub async fn api_views(app: &Shared, repo: &RepoName, row: &db::repos::RepoRow) -> Result<Vec<hub_api::Release>, sqlx::Error> {
516 517
    Ok(views(app, repo, row).await?.iter().map(|v| to_api(app, row, v)).collect())
517 518
}
519 +
520 +
/// Where a built asset's SLSA provenance is: its artifact's, which states
521 +
/// the same bytes (the asset links to its build by BLAKE3).
522 +
pub fn provenance_url(base: &str, p: &db::flow::Provenance) -> String {
523 +
    format!(
524 +
        "{base}/flow/{}/artifacts/{}/{}/provenance",
525 +
        p.run_number,
526 +
        super::settings::urlencode(&p.job),
527 +
        super::settings::urlencode(&p.name)
528 +
    )
529 +
}
modified crates/hub/tests/flow.rs +14 −0
…
891 unchanged lines
892 892
        (Some("tool"), Some(run.number), Some("build"))
893 893
    );
894 894
    assert!(asset["built_by"]["receipt"].is_null(), "that receipt was not signed");
895 +
    assert!(asset["built_by"]["provenance_url"].is_null(), "no signed receipt, no provenance");
896 +
897 +
    // With a kept signed receipt, the asset links its provenance (#239).
898 +
    sqlx::query("UPDATE flow_jobs SET receipt_attest = 'a1b2', receipt_signer = 'earth-1' WHERE run_id = $1 AND name = 'build'")
899 +
        .bind(run.id)
900 +
        .execute(&h.app.db)
901 +
        .await
902 +
        .unwrap();
903 +
    let want = format!("/andy/demo/flow/{}/artifacts/build/tool/provenance", run.number);
904 +
    let (_, page) = call(&h.app, "GET", "/andy/demo/releases/tag/v1", None, None).await;
905 +
    assert!(page.contains(&format!("href=\"{want}\"")), "{page}");
906 +
    let (_, api) = call(&h.app, "GET", "/api/v1/repos/andy/demo/releases", None, None).await;
907 +
    let api: serde_json::Value = serde_json::from_str(&api).unwrap();
908 +
    assert!(api[0]["assets"][0]["built_by"]["provenance_url"].as_str().unwrap().ends_with(&want));
895 909
}
896 910
897 911
/// A release's trust report (hub spec §9.1): a signed tag, an asset built
…
90 unchanged lines
modified docs/spec.md +1 −1
…
402 unchanged lines
403 403
  A hand-uploaded asset, an unsigned tag or an unsigned change is a warning. A signature or receipt that does not verify, or a revoked key, is a failure.
404 404
- **Export:** `?format=json` downloads the report, and `GET /api/v1/repos/{owner}/{name}/releases/trust/{tag}` returns the same. The JSON carries each signed receipt as hex, so an auditor can check it offline with `oxy receipt verify`.
405 405
406 -
**Built by.** A release asset links to the build that produced it: the passed OxygenFlow job of this repository with an artifact of the same BLAKE3 (hub spec §13.2). A job with a kept signed receipt comes first, then the newest run. The release page shows the run, the job and the signed receipt with its signer, and the API gives the same as `built_by` on each asset. On a run page, a writer can attach an artifact to a release; it becomes an asset named after the artifact's path, so the link follows by content. A file uploaded by hand links the same way when its bytes match.
406 +
**Built by.** A release asset links to the build that produced it: the passed OxygenFlow job of this repository with an artifact of the same BLAKE3 (hub spec §13.2). A job with a kept signed receipt comes first, then the newest run. The release page shows the run, the job and the signed receipt with its signer, and the API gives the same as `built_by` on each asset. With a signed receipt it also links the asset's SLSA provenance (`provenance_url`, §13.2's provenance of that artifact, which states the same bytes), so a release asset is checked with `cosign verify-blob-attestation --key oxyhub-signing.pem --type slsaprovenance1 --signature PROVENANCE ASSET` (oxygenhub#239). On a run page, a writer can attach an artifact to a release; it becomes an asset named after the artifact's path, so the link follows by content. A file uploaded by hand links the same way when its bytes match.
407 407
408 408
## 10. Notifications
409 409
…
207 unchanged lines
modified templates/releases.html +1 −1
…
17 unchanged lines
18 18
  {% if !v.r.notes.is_empty() %}<article class="doc">{{ v.notes_html|safe }}</article>{% endif %}
19 19
  <h3>Assets</h3>
20 20
  <ul class="plain">
21 -
    {% for a in v.assets %}<li><a href="{{ repo.url }}/asset/{{ a.id }}/{{ a.name }}">{{ a.name }}</a> <span class="small mute">{{ a.size|bytes }} · blake3 <code title="{{ a.hash }}">{{ a.hash|short }}</code> · {{ a.downloads }} downloads</span>{% if let Some(p) = v.built_by(a.id) %} <span class="small" data-built>· built by <a href="{{ repo.url }}/flow/{{ p.run_number }}">run #{{ p.run_number }}</a> {{ p.job }}{% if p.receipt_attest.is_empty() %} <span class="mute">(unsigned receipt)</span>{% else %} · <a href="{{ repo.url }}/receipts/{{ p.receipt_attest }}" title="Check it with oxy receipt verify">signed receipt</a> by {{ p.receipt_signer }}{% endif %}</span>{% endif %}
21 +
    {% for a in v.assets %}<li><a href="{{ repo.url }}/asset/{{ a.id }}/{{ a.name }}">{{ a.name }}</a> <span class="small mute">{{ a.size|bytes }} · blake3 <code title="{{ a.hash }}">{{ a.hash|short }}</code> · {{ a.downloads }} downloads</span>{% if let Some(p) = v.built_by(a.id) %} <span class="small" data-built>· built by <a href="{{ repo.url }}/flow/{{ p.run_number }}">run #{{ p.run_number }}</a> {{ p.job }}{% if p.receipt_attest.is_empty() %} <span class="mute">(unsigned receipt)</span>{% else %} · <a href="{{ repo.url }}/receipts/{{ p.receipt_attest }}" title="Check it with oxy receipt verify">signed receipt</a> by {{ p.receipt_signer }} · <a href="{{ repo.url }}/flow/{{ p.run_number }}/artifacts/{{ p.job|urlencode }}/{{ p.name|urlencode }}/provenance" title="SLSA provenance signed by this hub: cosign verify-blob-attestation --key /.well-known/oxyhub-signing.pem --type slsaprovenance1">provenance</a>{% endif %}</span>{% endif %}
22 22
      {% if one && repo.can(crate::auth::Access::Write) %}<form class="tiny" method="post" action="{{ repo.url }}/releases/edit/{{ v.r.id }}"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="asset" value="{{ a.id }}"><button name="action" value="delete_asset">delete</button></form>{% endif %}</li>{% endfor %}
23 23
    <li><a href="{{ repo.url }}/archive?ref={{ v.r.tag }}&format=tar.zst">Source ({{ v.r.tag }}.tar.zst)</a></li>
24 24
    <li><a href="{{ repo.url }}/archive?ref={{ v.r.tag }}&format=zip">Source ({{ v.r.tag }}.zip)</a></li>
…
18 unchanged lines