Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <binary> 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"
}
]
}
95 changes: 93 additions & 2 deletions cmd/relay/conform.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"flag"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
Expand Down Expand Up @@ -56,21 +57,23 @@ type conformResult struct {
}

// runConform implements
// `relay conform [--format text|json] [--strict] [--manifest] <binary>`.
// `relay conform [--format text|json] [--strict] [--manifest] [--attestation] <binary>`.
//
//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] <binary>")
fmt.Fprintln(stderr, "Usage: relay conform [--format text|json] [--strict] [--manifest] [--attestation] <binary>")
return exitCode(2)
}
if fs.NArg() > 1 {
Expand All @@ -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 {
Expand Down Expand Up @@ -299,6 +318,7 @@ func buildManifest(binary string) conformManifest {
{13, statusNotObservable, verifierBuildAndTest},
{14, statusNotObservable, verifierCI},
{15, statusNotObservable, verifierManifestSplit},
{16, statusNotObservable, verifierCI},
}

m.Overall = statusPass
Expand All @@ -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 {
Expand Down
146 changes: 141 additions & 5 deletions cmd/relay/conform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}

Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion cmd/relay/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 39 additions & 0 deletions spec/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 <binary>` 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
Expand Down
Loading
Loading