From 4c4e31f28feebb5c41f120531838ef5f78667a92 Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Fri, 21 Aug 2026 05:54:39 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20add=20release=20conformance=20attestati?= =?UTF-8?q?on=20format=20(relay-conform-attestation/1),=20spec=20=C2=A720.?= =?UTF-8?q?6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New §20.6 defines the shape an implementation MAY publish to make §20.1's CI gates externally verifiable rather than self-asserted: an in-toto Statement whose subject digest is the SHA-256 of the attested binary file, and whose predicate bundles the full §17.2 conformance manifest, the attestation-generating tool's own embedded vectors_version (§15.8 - explicitly NOT observed from the attested binary, which a black-box invocation cannot introspect), and an optional implementation-defined safety_evidence_summary. - Scope decision, documented in the CHANGELOG rather than silently narrowed: this PR ships spec-only. The originating issue also proposed cryptographic signing, publishing alongside a release's container image, surfacing the digest in version --format json, and having relay probe/relay report treat an unverifiable release as non-conformant. None of that ships here - RELAY's own CI does not currently push container images to any registry, and fabricating an unreal 'signature' without real key management would be worse than not signing at all. predicate.signed MUST be false on an unsigned attestation; this section defines shape only. - New embedded schema spec/schemas/relay-conform-attestation.json. - Reference implementation: relay conform --attestation in cmd/relay/conform.go. New REQ-RELAY-098. - Real bug found and fixed while implementing this: buildManifest's Requirements slice was missing entry 16 (Vector manifest) - PR #181 added the §17 Requirement 16 spec text and verifier-table row but never added the corresponding code entry, so every relay-conform/1 manifest generated since v2.4 silently omitted it despite the spec's own text mandating 'exactly one entry per §17 requirement (1-16, ...)'. Fixed; the two manifest-shape tests that should have caught this (TestBuildManifestSelf, TestRunConformManifestFlag) were also stale at a hardcoded 15 and are corrected alongside it. Mutation-tested: reverted the fix, confirmed both tests failed, restored. - 6 new tests for the attestation feature, including independent cross-checks: subject digest matches a separately-computed sha256 of the binary file, vectors_version matches the tool's own parsed vectors_manifest.json, and schema validation against the new embedded schema. - SpecVersion bumped 2.5 -> 2.6 (MINOR, new optional format + CLI flag). Closes #142. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 10 ++ cmd/relay/conform.go | 95 ++++++++++++- cmd/relay/conform_test.go | 146 +++++++++++++++++++- cmd/relay/main.go | 2 +- spec/CHANGELOG.md | 39 ++++++ spec/relay-spec.md | 91 ++++++++++-- spec/schemas/relay-conform-attestation.json | 74 ++++++++++ spec/schemas/relay-conform-manifest.json | 2 +- spec/version.json | 2 +- version.go | 2 +- version_test.go | 4 +- 11 files changed, 441 insertions(+), 26 deletions(-) create mode 100644 spec/schemas/relay-conform-attestation.json diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 2ce2a08..99c8739 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -922,6 +922,16 @@ "verification": "test", "safety_goal": "SG-006", "asil": "ASIL-B" + }, + { + "id": "REQ-RELAY-098", + "title": "relay conform --attestation (relay-conform-attestation/1)", + "text": "relay conform --attestation MUST emit an unsigned relay-conform-attestation/1 predicate (spec \u00a720.6): an in-toto Statement whose subject digest is the SHA-256 of the attested binary's raw file bytes, and whose predicate bundles the full \u00a717.2 conformance manifest for that binary, the attestation-generating tool's own embedded vectors_version (\u00a715.8) -- not the attested binary's, which a black-box invocation cannot introspect -- and predicate.signed:false. This defines predicate shape only; signing and publication are explicitly out of scope.", + "category": "functional", + "criticality": "low", + "verification": "test", + "safety_goal": "SG-006", + "asil": "QM" } ] } diff --git a/cmd/relay/conform.go b/cmd/relay/conform.go index 4af9d2f..29a5a7b 100644 --- a/cmd/relay/conform.go +++ b/cmd/relay/conform.go @@ -13,6 +13,7 @@ import ( "flag" "fmt" "io" + "os" "os/exec" "strings" "time" @@ -56,21 +57,23 @@ type conformResult struct { } // runConform implements -// `relay conform [--format text|json] [--strict] [--manifest] `. +// `relay conform [--format text|json] [--strict] [--manifest] [--attestation] `. // //fusa:req REQ-RELAY-052 //fusa:req REQ-RELAY-095 +//fusa:req REQ-RELAY-098 func runConform(stdout, stderr io.Writer, args []string) error { fs := flag.NewFlagSet("conform", flag.ContinueOnError) fs.SetOutput(io.Discard) format := fs.String("format", "text", "Output format: text or json") strict := fs.Bool("strict", false, "Treat WARN as FAIL") manifest := fs.Bool("manifest", false, "Emit a relay-conform/1 manifest (spec §17.2) instead of findings") + attestation := fs.Bool("attestation", false, "Emit an unsigned relay-conform-attestation/1 predicate (spec §20.6) instead of findings") if err := fs.Parse(args); err != nil { return fmt.Errorf("relay conform: %w", err) } if fs.NArg() == 0 { - fmt.Fprintln(stderr, "Usage: relay conform [--format text|json] [--strict] [--manifest] ") + fmt.Fprintln(stderr, "Usage: relay conform [--format text|json] [--strict] [--manifest] [--attestation] ") return exitCode(2) } if fs.NArg() > 1 { @@ -96,6 +99,22 @@ func runConform(stdout, stderr io.Writer, args []string) error { return nil } + if *attestation { + a, err := buildAttestation(fs.Arg(0)) + if err != nil { + return fmt.Errorf("relay conform --attestation: %w", err) + } + enc := json.NewEncoder(stdout) + enc.SetIndent("", " ") + if err := enc.Encode(a); err != nil { + return err + } + if a.Predicate.ConformanceManifest.Overall == statusFail { + return exitCode(1) + } + return nil + } + cr := conformBinary(fs.Arg(0), *strict) switch *format { @@ -299,6 +318,7 @@ func buildManifest(binary string) conformManifest { {13, statusNotObservable, verifierBuildAndTest}, {14, statusNotObservable, verifierCI}, {15, statusNotObservable, verifierManifestSplit}, + {16, statusNotObservable, verifierCI}, } m.Overall = statusPass @@ -311,6 +331,77 @@ func buildManifest(binary string) conformManifest { return m } +// attestationDigest is one subject's content-addressed digest in an in-toto +// Statement (spec §20.6). +type attestationDigest struct { + SHA256 string `json:"sha256"` +} + +// attestationSubject identifies the artifact an attestation is about. +type attestationSubject struct { + Name string `json:"name"` + Digest attestationDigest `json:"digest"` +} + +// attestationPredicate is the relay-conform-attestation/1 predicate body +// (spec §20.6). +type attestationPredicate struct { + SpecVersion string `json:"spec_version"` + VectorsVersion string `json:"vectors_version"` + ConformanceManifest conformManifest `json:"conformance_manifest"` + SafetyEvidenceSummary interface{} `json:"safety_evidence_summary"` + Signed bool `json:"signed"` +} + +// conformAttestation is an in-toto Statement carrying a relay-conform-attestation/1 +// predicate (spec §20.6). It is unsigned: this type has no signature field, +// and Signed is always false — signing and publication are explicitly out of +// scope for RELAY's own reference tooling (spec §20.6). +type conformAttestation struct { + Type string `json:"_type"` + PredicateType string `json:"predicateType"` + Subject []attestationSubject `json:"subject"` + Predicate attestationPredicate `json:"predicate"` +} + +// buildAttestation generates an unsigned relay-conform-attestation/1 +// predicate (spec §20.6) for one binary: an in-toto Statement bundling the +// §17.2 conformance manifest for that binary, this tool's own embedded +// vectors_version (§15.8 — NOT observed from the attested binary, which a +// black-box invocation cannot introspect), and the attested binary's own +// SHA-256 as the subject digest. +// +//fusa:req REQ-RELAY-098 +func buildAttestation(binary string) (conformAttestation, error) { + raw, err := os.ReadFile(binary) + if err != nil { + return conformAttestation{}, fmt.Errorf("read binary: %w", err) + } + sum := sha256.Sum256(raw) + + m := buildManifest(binary) + + vectorsVersion := "" + if vm, err := relay.ParsedVectorsManifest(); err == nil { + vectorsVersion = vm.VectorsVersion + } + + return conformAttestation{ + Type: "https://in-toto.io/Statement/v1", + PredicateType: "https://relay.dev/attestation/relay-conform/1", + Subject: []attestationSubject{ + {Name: m.Tool, Digest: attestationDigest{SHA256: hex.EncodeToString(sum[:])}}, + }, + Predicate: attestationPredicate{ + SpecVersion: m.SpecVersion, + VectorsVersion: vectorsVersion, + ConformanceManifest: m, + SafetyEvidenceSummary: nil, + Signed: false, + }, + }, nil +} + // hasFail reports whether any finding in fs is FAIL-severity. func hasFail(fs []conformFinding) bool { for _, f := range fs { diff --git a/cmd/relay/conform_test.go b/cmd/relay/conform_test.go index 03c9911..4f09d9b 100644 --- a/cmd/relay/conform_test.go +++ b/cmd/relay/conform_test.go @@ -382,8 +382,8 @@ func TestBuildManifestSelf(t *testing.T) { if m.SpecVersion == "" { t.Error("SpecVersion is empty") } - if len(m.Requirements) != 15 { - t.Fatalf("len(Requirements) = %d, want 15", len(m.Requirements)) + if len(m.Requirements) != 16 { + t.Fatalf("len(Requirements) = %d, want 16", len(m.Requirements)) } for i, r := range m.Requirements { if r.ID != i+1 { @@ -414,7 +414,7 @@ func TestBuildManifestNotObservableDefaults(t *testing.T) { bin := buildTestBinary(t) m := buildManifest(bin) - wantNotObservable := []int{2, 3, 4, 5, 8, 9, 10, 11, 13, 14, 15} + wantNotObservable := []int{2, 3, 4, 5, 8, 9, 10, 11, 13, 14, 15, 16} byID := map[int]manifestRequirement{} for _, r := range m.Requirements { byID[r.ID] = r @@ -508,8 +508,8 @@ func TestRunConformManifestFlag(t *testing.T) { if m.Overall != statusPass { t.Errorf("Overall = %s, want %s", m.Overall, statusPass) } - if len(m.Requirements) != 15 { - t.Errorf("len(Requirements) = %d, want 15", len(m.Requirements)) + if len(m.Requirements) != 16 { + t.Errorf("len(Requirements) = %d, want 16", len(m.Requirements)) } } @@ -535,6 +535,142 @@ func TestRunConformManifestFlagFail(t *testing.T) { } } +// --- tests for --attestation / buildAttestation (spec §20.6) --- + +//fusa:test REQ-RELAY-098 +func TestBuildAttestationSelf(t *testing.T) { + bin := buildTestBinary(t) + raw, err := os.ReadFile(bin) + if err != nil { + t.Fatalf("read binary: %v", err) + } + sum := sha256.Sum256(raw) + wantDigest := hex.EncodeToString(sum[:]) + + a, err := buildAttestation(bin) + if err != nil { + t.Fatalf("buildAttestation: %v", err) + } + + if a.Type != "https://in-toto.io/Statement/v1" { + t.Errorf("Type = %q, want in-toto Statement v1", a.Type) + } + if a.PredicateType != "https://relay.dev/attestation/relay-conform/1" { + t.Errorf("PredicateType = %q, want relay-conform/1", a.PredicateType) + } + if len(a.Subject) != 1 { + t.Fatalf("len(Subject) = %d, want 1", len(a.Subject)) + } + if a.Subject[0].Digest.SHA256 != wantDigest { + t.Errorf("Subject[0].Digest.SHA256 = %q, want %q (independently computed sha256 of the binary file)", a.Subject[0].Digest.SHA256, wantDigest) + } + if a.Predicate.SpecVersion == "" { + t.Error("Predicate.SpecVersion is empty") + } + if a.Predicate.ConformanceManifest.Kind != "relay-conform-manifest" { + t.Errorf("Predicate.ConformanceManifest.Kind = %q, want relay-conform-manifest", a.Predicate.ConformanceManifest.Kind) + } + if len(a.Predicate.ConformanceManifest.Requirements) != 16 { + t.Errorf("len(Predicate.ConformanceManifest.Requirements) = %d, want 16", len(a.Predicate.ConformanceManifest.Requirements)) + } + // §20.6: MUST be false — this type has no signing mechanism at all. + if a.Predicate.Signed { + t.Error("Predicate.Signed = true, want false (relay conform --attestation never signs)") + } + if a.Predicate.SafetyEvidenceSummary != nil { + t.Errorf("Predicate.SafetyEvidenceSummary = %v, want nil (RELAY's reference tooling cannot generate one)", a.Predicate.SafetyEvidenceSummary) + } +} + +//fusa:test REQ-RELAY-098 +func TestBuildAttestationVectorsVersionMatchesEmbedded(t *testing.T) { + bin := buildTestBinary(t) + a, err := buildAttestation(bin) + if err != nil { + t.Fatalf("buildAttestation: %v", err) + } + vm, err := relay.ParsedVectorsManifest() + if err != nil { + t.Fatalf("ParsedVectorsManifest: %v", err) + } + if a.Predicate.VectorsVersion != vm.VectorsVersion { + t.Errorf("Predicate.VectorsVersion = %q, want %q (this tool's own embedded vectors_manifest.json)", a.Predicate.VectorsVersion, vm.VectorsVersion) + } +} + +//fusa:test REQ-RELAY-098 +func TestBuildAttestationSchemaValid(t *testing.T) { + bin := buildTestBinary(t) + a, err := buildAttestation(bin) + if err != nil { + t.Fatalf("buildAttestation: %v", err) + } + data, err := json.Marshal(a) + if err != nil { + t.Fatalf("json.Marshal(attestation): %v", err) + } + var asAny interface{} + if err := json.Unmarshal(data, &asAny); err != nil { + t.Fatalf("re-unmarshal attestation JSON: %v", err) + } + schemaJSON, err := relay.Schema("relay-conform-attestation") + if err != nil { + t.Fatalf("no embedded relay-conform-attestation schema: %v", err) + } + violations := validateSchema(schemaJSON, asAny) + if len(violations) != 0 { + t.Errorf("attestation does not conform to relay-conform-attestation schema: %v\nattestation: %s", violations, data) + } +} + +//fusa:test REQ-RELAY-098 +func TestBuildAttestationUnreadableBinary(t *testing.T) { + _, err := buildAttestation(filepath.Join(t.TempDir(), "does-not-exist")) + if err == nil { + t.Fatal("expected an error for a nonexistent binary, got nil") + } +} + +//fusa:test REQ-RELAY-098 +func TestRunConformAttestationFlag(t *testing.T) { + bin := buildTestBinary(t) + var out bytes.Buffer + var errbuf bytes.Buffer + err := runConform(&out, &errbuf, []string{"--attestation", bin}) + if err != nil { + t.Fatalf("relay conform --attestation relay: unexpected error: %v\noutput: %s", err, out.String()) + } + var a conformAttestation + if jsonErr := json.Unmarshal(out.Bytes(), &a); jsonErr != nil { + t.Fatalf("conform --attestation output is not valid JSON: %v\noutput: %s", jsonErr, out.String()) + } + if a.Predicate.ConformanceManifest.Overall != statusPass { + t.Errorf("Predicate.ConformanceManifest.Overall = %s, want %s", a.Predicate.ConformanceManifest.Overall, statusPass) + } +} + +//fusa:test REQ-RELAY-098 +func TestRunConformAttestationFlagFail(t *testing.T) { + bin := buildFailingBinary(t) + var out bytes.Buffer + var errbuf bytes.Buffer + err := runConform(&out, &errbuf, []string{"--attestation", bin}) + if err == nil { + t.Fatalf("expected non-nil error for a FAIL-manifest attestation, got nil\noutput: %s", out.String()) + } + var code exitCode + if !errors.As(err, &code) || int(code) != 1 { + t.Errorf("expected exitCode(1) for a FAIL-manifest attestation, got %v", err) + } + var a conformAttestation + if jsonErr := json.Unmarshal(out.Bytes(), &a); jsonErr != nil { + t.Fatalf("conform --attestation output is not valid JSON: %v\noutput: %s", jsonErr, out.String()) + } + if a.Predicate.ConformanceManifest.Overall != statusFail { + t.Errorf("Predicate.ConformanceManifest.Overall = %s, want %s", a.Predicate.ConformanceManifest.Overall, statusFail) + } +} + func countBySeverity(fs []conformFinding, sev conformSeverity) int { n := 0 for _, f := range fs { diff --git a/cmd/relay/main.go b/cmd/relay/main.go index 6576fe9..299bf13 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go @@ -18,7 +18,7 @@ import ( relay "github.com/SoundMatt/RELAY/v2" ) -const toolVersion = "2.5.0" +const toolVersion = "2.6.0" func main() { if err := run(os.Stdout, os.Stderr, os.Args[1:]); err != nil { diff --git a/spec/CHANGELOG.md b/spec/CHANGELOG.md index a6a60ce..cd78813 100644 --- a/spec/CHANGELOG.md +++ b/spec/CHANGELOG.md @@ -1,5 +1,44 @@ # RELAY Spec Changelog +## v2.6 — 2026-08-21 (MINOR — new release conformance attestation format) + +- **New §20.6 — Release conformance attestation (`relay-conform-attestation/1`).** + Defines the *shape* an implementation MAY publish to make §20.1's CI + gates externally verifiable rather than self-asserted: an + [in-toto](https://in-toto.io/) Statement whose `subject` digest is the + SHA-256 of the attested binary file, and whose predicate bundles the + full §17.2 conformance manifest, the attestation-generating tool's own + embedded `vectors_version` (§15.8 — explicitly **not** observed from the + attested binary, which a black-box invocation cannot introspect), and an + optional implementation-defined `safety_evidence_summary`. +- **Scope decision (documented, not silently narrowed)**: this is a + spec-only release. The originating issue proposed cryptographic + signing, publishing the attestation alongside a release's container + image, surfacing its digest in `version --format json`, and having + `relay probe`/`relay report` treat an unverifiable release as + non-conformant. None of that ships here: RELAY's own CI does not + currently push container images to any registry, and fabricating a + "signature" without real key management would be worse than not + signing at all. `predicate.signed` MUST be `false` on an unsigned + attestation; this section defines shape only and does not mandate a + signing mechanism or publication channel. Closing this properly is + future work. +- **New embedded schema** `spec/schemas/relay-conform-attestation.json`. +- **Reference implementation**: `relay conform --attestation ` in + `cmd/relay/conform.go`, generating the unsigned predicate for a given + binary. New `REQ-RELAY-098`. +- **Bug found and fixed while implementing this**: `buildManifest`'s + `Requirements` slice was missing entry 16 (Vector manifest) — PR #181 + added the §17 Requirement 16 spec text and the verifier-table row but + never added the corresponding code entry, so every `relay-conform/1` + manifest generated since v2.4 silently omitted it despite the spec's + own text mandating "exactly one entry per §17 requirement (1–16, ...)". + Fixed; both manifest-shape tests that should have caught this + (`TestBuildManifestSelf`, `TestRunConformManifestFlag`) were also + stale at a hardcoded `15` and are corrected alongside it. +- `SpecVersion` bumped `2.5` → `2.6` (MINOR). Closes [NEW-SPEC-4] (partial + — signing, publishing, and probe/report enforcement remain open). + ## v2.5 — 2026-08-21 (MINOR — new optional capabilities field, tightened §17 Requirements 1 and 6) - **New optional `multi_protocol` capabilities field (§12.2).** Defaults to diff --git a/spec/relay-spec.md b/spec/relay-spec.md index 8ef0e0a..a9b92f8 100644 --- a/spec/relay-spec.md +++ b/spec/relay-spec.md @@ -1,4 +1,4 @@ -# RELAY Specification — v2.5 +# RELAY Specification — v2.6 **Real-time Embedded Link Abstraction Yoke** @@ -1182,7 +1182,7 @@ failure rather than a skip. Exit: `0` all equivalent, `1` any mismatch/error, "protocol": "CAN", "protocol_int": 1, "version": "1.2.3", - "spec_version": "2.5", + "spec_version": "2.6", "language": "go", "runtime": "go1.25.0", "commit": "a1b2c3d4" @@ -1207,7 +1207,7 @@ conformance failure, but §17.2's conformance manifest cannot populate its own "protocol": "CAN", "protocol_int": 1, "version": "1.2.3", - "spec_version": "2.5", + "spec_version": "2.6", "commands": ["version", "capabilities", "status", "connect", "send", "subscribe"], "transports": ["socketcan", "virtual"], "features": ["fd", "isotp", "j1939"], @@ -1237,7 +1237,7 @@ CLI: "protocol_int": null, "multi_protocol": true, "version": "2.5.0", - "spec_version": "2.5", + "spec_version": "2.6", "commands": ["version", "capabilities", "status", "conform", "convert", "..."], "transports": [], "features": [], @@ -1306,7 +1306,7 @@ $ go-can version --format json "protocol": "CAN", "protocol_int": 1, "version": "1.2.3", - "spec_version": "2.5", + "spec_version": "2.6", "language": "go", "runtime": "go1.25.0" } @@ -1318,7 +1318,7 @@ $ go-can capabilities "protocol": "CAN", "protocol_int": 1, "version": "1.2.3", - "spec_version": "2.5", + "spec_version": "2.6", "commands": ["version", "capabilities", "status", "connect", "send", "subscribe"], "transports": ["socketcan", "virtual"], "features": ["fd", "isotp", "j1939"], @@ -1394,11 +1394,11 @@ LABEL org.opencontainers.image.licenses="MPL-2.0" LABEL io.relay.tool="" LABEL io.relay.language="go|cpp|rust|c" LABEL io.relay.binary="" -LABEL io.relay.spec-version="2.5" +LABEL io.relay.spec-version="2.6" ``` The `io.relay.spec-version` label MUST always match the value of `SpecVersion` -exported by the package (§17.12 / §19.4). The `"2.5"` shown above is an example; +exported by the package (§17.12 / §19.4). The `"2.6"` shown above is an example; update it on each spec minor release. The project directory is mounted at `/project` by convention: @@ -2365,7 +2365,7 @@ diffable artifact. "manifest_version": "relay-conform/1", "tool": "go-can", "binary_version": "1.2.3", - "spec_version": "2.5", + "spec_version": "2.6", "git_sha": "a1b2c3d4", "capabilities_sha256": "e3b0c44298fc1c14...", "requirements": [ @@ -3114,11 +3114,11 @@ clarifications and fixes in PATCH releases. `spec/version.json` is authoritative. The spec document title is informational. -Current version: **v2.5** +Current version: **v2.6** -**Go:** `const SpecVersion = "2.5"` (update in implementations targeting v2.5) -**C++:** `constexpr std::string_view kRelaySpecVersion = "2.5";` -**Rust:** `pub const RELAY_SPEC_VERSION: &str = "2.5";` +**Go:** `const SpecVersion = "2.6"` (update in implementations targeting v2.6) +**C++:** `constexpr std::string_view kRelaySpecVersion = "2.6";` +**Rust:** `pub const RELAY_SPEC_VERSION: &str = "2.6";` --- @@ -3194,6 +3194,71 @@ build provenance for each release (SLSA build track), and MUST tag releases from signed, reviewed commits. RELAY's own `relay sbom` and `gofusa release` satisfy this for the reference tooling. +### 20.6 Release conformance attestation (`relay-conform-attestation/1`) + +§20.1's CI gates are self-asserted by the port's own pipeline: a green run +today gives no consumer of a released binary an externally verifiable claim +about what was checked. This subsection defines the *format* an +implementation MAY publish to close that gap — a +`relay-conform-attestation/1` predicate bundling the §17.2 conformance +manifest, the §15.8 vector manifest's `vectors_version`, and the targeted +`spec_version`, shaped as an [in-toto](https://in-toto.io/) Statement: + +```json +{ + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://relay.dev/attestation/relay-conform/1", + "subject": [ + {"name": "go-can", "digest": {"sha256": "9f86d081884c7d65..."}} + ], + "predicate": { + "spec_version": "2.6", + "vectors_version": "2.4", + "conformance_manifest": { "...": "the full §17.2 relay-conform/1 document" }, + "safety_evidence_summary": null, + "signed": false + } +} +``` + +`subject[0].digest.sha256` is the SHA-256 of the artifact being attested — +the binary file itself, or a container image digest if the implementation +publishes one. `predicate.conformance_manifest` is the exact document +`relay conform --manifest` emits (§17.2) for that same binary. Unlike the +conformance manifest's own fields, `predicate.vectors_version` is **not** +observed from the attested binary — a black-box CLI invocation has no way +to read an arbitrary external binary's own embedded `vectors_manifest.json` +(§15.8). It MUST instead be the `vectors_version` of the vector set the +*attestation-generating tool itself* embeds, documenting which canonical +vector set was current when the attestation was produced, or `""` if it +embeds none. An implementation generating its own attestation about +itself (the common case: a release pipeline attesting its own binary) +naturally reports its own true embedded value here. `predicate.safety_evidence_summary` +is OPTIONAL and implementation-defined — a place for an x-FuSa +safety-evidence digest (`gofusa`/`rsfusa`/`cfusa`/`cpfusa`); RELAY's own +reference tooling does not populate it, since generating one requires the +implementation's own pinned x-FuSa tool, not something a black-box `relay +conform` invocation can produce. + +`predicate.signed` MUST be `false` on an attestation that is not +cryptographically signed. This section defines the predicate's *shape* +only: it does NOT mandate a signing mechanism, a publication channel, or +that `relay probe`/`relay report` treat an unsigned or absent attestation +as non-conformant. Those are genuinely separate, larger commitments — +choosing a signing scheme (e.g. Sigstore/cosign keyless signing) and +standing up release-artifact publishing infrastructure — deliberately left +open rather than mandated here without a concrete, verified reference +implementation to point to. An implementation that signs its own +attestations MAY set `predicate.signed: true` and MUST then publish the +signature and the identity/transparency-log record a verifier needs, +using whatever mechanism its own release process already trusts; this +document does not standardize that mechanism. + +This is a SHOULD, not a MUST, for the same reason as §17.1: there is no +CI gate for it yet, and RELAY's own reference tooling emits the predicate +unsigned (`relay conform --attestation `) without publishing or +signing it. + --- ## Appendix A — Project alignment as of v0.2 (2026-06-16); not maintained thereafter diff --git a/spec/schemas/relay-conform-attestation.json b/spec/schemas/relay-conform-attestation.json new file mode 100644 index 0000000..850f006 --- /dev/null +++ b/spec/schemas/relay-conform-attestation.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/SoundMatt/RELAY/spec/schemas/relay-conform-attestation.json", + "title": "RELAY release conformance attestation", + "description": "JSON output of `relay conform --attestation ` (spec §20.6). An in-toto Statement whose predicate bundles the §17.2 conformance manifest and §15.8 vectors_version. Defines shape only — signing and publication are out of scope.", + "type": "object", + "properties": { + "_type": { + "type": "string", + "const": "https://in-toto.io/Statement/v1" + }, + "predicateType": { + "type": "string", + "const": "https://relay.dev/attestation/relay-conform/1" + }, + "subject": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Binary name, from the conformance manifest's tool field." + }, + "digest": { + "type": "object", + "properties": { + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "SHA-256 (lowercase hex) of the attested binary's raw file bytes." + } + }, + "required": ["sha256"], + "additionalProperties": false + } + }, + "required": ["name", "digest"], + "additionalProperties": false + } + }, + "predicate": { + "type": "object", + "properties": { + "spec_version": { + "type": "string", + "description": "Declared spec_version, from the conformance manifest." + }, + "vectors_version": { + "type": "string", + "description": "vectors_version from the attestation-generating tool's own embedded vectors_manifest.json (§15.8) -- NOT observed from the attested binary, which a black-box invocation cannot introspect. \"\" if the generator embeds none." + }, + "conformance_manifest": { + "type": "object", + "description": "The full relay-conform/1 document (§17.2), exactly as relay conform --manifest emits it." + }, + "safety_evidence_summary": { + "type": ["object", "null"], + "description": "OPTIONAL, implementation-defined x-FuSa safety-evidence digest. Null when not populated (e.g. by RELAY's own reference tooling, which cannot generate one generically)." + }, + "signed": { + "type": "boolean", + "const": false, + "description": "MUST be false on an unsigned attestation. This schema does not define a signed variant; signing mechanisms are out of scope (spec §20.6)." + } + }, + "required": ["spec_version", "vectors_version", "conformance_manifest", "signed"], + "additionalProperties": false + } + }, + "required": ["_type", "predicateType", "subject", "predicate"], + "additionalProperties": false +} diff --git a/spec/schemas/relay-conform-manifest.json b/spec/schemas/relay-conform-manifest.json index 36edf82..fbb1409 100644 --- a/spec/schemas/relay-conform-manifest.json +++ b/spec/schemas/relay-conform-manifest.json @@ -56,7 +56,7 @@ "required": ["id", "status", "verifier"], "additionalProperties": false }, - "description": "One entry per §17 requirement (1-15, extended as the requirement list grows)." + "description": "One entry per §17 requirement (1-16, extended as the requirement list grows)." }, "overall": { "type": "string", diff --git a/spec/version.json b/spec/version.json index 7dc3f4f..95fad38 100644 --- a/spec/version.json +++ b/spec/version.json @@ -1,5 +1,5 @@ { - "version": "2.5", + "version": "2.6", "date": "2026-08-21", "status": "stable", "networks": [ diff --git a/version.go b/version.go index 0f4a3c7..2df23f2 100644 --- a/version.go +++ b/version.go @@ -8,4 +8,4 @@ package relay // Every RELAY-conformant implementation must export this constant (spec §17 req 12). // //fusa:req REQ-RELAY-020 -const SpecVersion = "2.5" +const SpecVersion = "2.6" diff --git a/version_test.go b/version_test.go index cfd2224..e7b575b 100644 --- a/version_test.go +++ b/version_test.go @@ -8,7 +8,7 @@ import "testing" //fusa:test REQ-RELAY-020 func TestSpecVersion(t *testing.T) { - if SpecVersion != "2.5" { - t.Errorf("SpecVersion = %q, want %q", SpecVersion, "2.5") + if SpecVersion != "2.6" { + t.Errorf("SpecVersion = %q, want %q", SpecVersion, "2.6") } }