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") } }