diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..4ed598625 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,243 @@ +# Enterprise Contract CLI - Agent Instructions + +## Project Overview + +The `ec` (Enterprise Contract) CLI is a command-line tool for verifying artifacts and evaluating software supply chain policies. It validates container image signatures, provenance, and enforces policies across various types of software artifacts using Open Policy Agent (OPA)/Rego rules. + +## Essential Commands + +### Building +```bash +make build # Build ec binary for current platform (creates dist/ec) +make dist # Build for all supported architectures +make clean # Remove build artifacts +DEBUG_BUILD=1 make build # Build with debugging symbols for gdb/dlv +make debug-run # Run binary with delve debugger (requires debug build) +``` + +### Testing +```bash +make test # Run all tests (unit, integration, generative) +make acceptance # Run acceptance tests (Cucumber/Gherkin, 20m timeout) +make scenario_ # Run single acceptance scenario (replace spaces with underscores) +make feature_ # Run all scenarios in a single feature file + +# Running specific tests +go test -tags=unit ./internal/evaluator -run TestSpecificFunction +cd acceptance && go test -test.run 'TestFeatures/scenario_name' +``` + +### Code Quality +```bash +make lint # Run all linters (golangci-lint, addlicense, tekton-lint) +make lint-fix # Auto-fix linting issues +make ci # Run full CI suite (test + lint-fix + acceptance) +``` + +## Architecture + +### Command Structure +Main commands in `cmd/`: +- **validate** - Validate container images, attestations, and policies +- **test** - Test policies against data (similar to conftest) +- **fetch** - Download and inspect attestations +- **inspect** - Examine policy bundles and data +- **track** - Track compliance status +- **sigstore** - Sigstore-related operations +- **initialize** - Initialize policy configurations + +### Core Components + +#### Policy Evaluation (`internal/evaluator/`) +- **Conftest Evaluator**: Main evaluation engine using OPA/Rego +- **Pluggable Rule Filtering**: Extensible system for filtering which rules run based on: + - Pipeline intentions (build vs release vs production) + - Include/exclude lists (collections, packages, specific rules) + - Custom metadata criteria +- **Result Processing**: Complex rule result filtering with scoring, severity promotion/demotion, and effective time handling + +**Key Implementation Details:** +- PolicyResolver interface provides comprehensive policy resolution for pre and post-evaluation filtering +- UnifiedPostEvaluationFilter implements unified filtering logic +- Sophisticated scoring system for include/exclude decisions (collections: 10pts, packages: 10pts per level, rules: +100pts, terms: +100pts) +- Term-based filtering allows fine-grained control (e.g., `tasks.required_untrusted_task_found:clamav-scan`) +- See `.cursor/rules/rule_filtering_process.mdc` and `.cursor/rules/package_filtering_process.mdc` for detailed documentation + +#### Attestation Handling (`internal/attestation/`) +- Parsing and validation of in-toto attestations +- SLSA provenance processing (supports both v0.2 and v1.0) +- Integration with Sigstore for signature verification + +#### VSA (Verification Summary Attestation) (`internal/validate/vsa/`) +VSA creates cryptographically signed attestations containing validation metadata and policy information after successful image validation. + +**Layered Architecture:** +1. Core Interfaces (`interfaces.go`) - Fundamental VSA interfaces +2. Service Layer (`service.go`) - High-level VSA processing orchestration +3. Core Logic (`vsa.go`) - VSA data structures and predicate generation +4. Attestation (`attest.go`) - DSSE envelope creation and signing +5. Storage (`storage*.go`) - Abstract storage backends (local, Rekor) +6. Retrieval (`*_retriever.go`) - VSA retrieval mechanisms +7. Orchestration (`orchestrator.go`) - Complex VSA processing workflows +8. Validation (`validator.go`) - VSA validation with policy comparison +9. Command Interface (`cmd/validate/vsa.go`) - CLI for VSA validation + +**Key Features:** +- Policy comparison and equivalence checking +- DSSE envelope signature verification (enabled by default) +- Multiple storage backends (local filesystem, Rekor transparency log) +- VSA expiration checking with configurable thresholds +- Batch validation from application snapshots with parallel processing + +See `.cursor/rules/vsa_functionality.mdc` for comprehensive documentation. + +#### Input Processing (`internal/input/`) +- Multiple input sources: container images, files, Kubernetes resources +- Automatic detection and parsing of different artifact types + +#### Policy Management (`internal/policy/`) +- OCI-based policy bundle loading +- Git repository policy fetching +- Policy metadata extraction and rule discovery + +### Key Internal Packages +- `internal/signature/` - Container image signature verification +- `internal/image/` - Container image operations and metadata +- `internal/kubernetes/` - Kubernetes resource processing +- `internal/utils/` - Common utilities and helpers +- `internal/rego/` - Rego policy compilation and execution +- `internal/format/` - Output formatting (JSON, YAML, etc.) + +## Module Structure + +The project uses multiple Go modules: +- **Root module** - Main CLI application +- **acceptance/** - Acceptance test module with Cucumber integration +- **tools/** - Development tools and utilities + +## Testing Strategy + +### Test Types +- **Unit tests** (`-tags=unit`, 10s timeout) - Fast isolated tests +- **Integration tests** (`-tags=integration`, 15s timeout) - Component integration +- **Generative tests** (`-tags=generative`, 30s timeout) - Property-based testing +- **Acceptance tests** (20m timeout) - End-to-end Cucumber scenarios with real artifacts + - Use `-persist` flag to keep test environment after execution for debugging + - Use `-restore` to run tests against persisted environment + - Use `-tags=@focus` to run specific scenarios + +### Acceptance Test Framework +- Uses Cucumber/Gherkin syntax for feature definitions in `features/` directory +- Steps implemented in Go using Godog framework +- Self-contained test environment using Testcontainers +- WireMock for stubbing HTTP APIs (Kubernetes apiserver, Rekor) +- Snapshots stored in `features/__snapshots__/` (update with `UPDATE_SNAPS=true`) + +## Development Environment + +### Required Tools +- Go 1.24.4+ +- Make +- Podman/Docker for container operations +- Node.js for tekton-lint + +### Troubleshooting Common Issues + +1. **Go checksum mismatch** + ```bash + go env -w GOPROXY='https://proxy.golang.org,direct' + ``` + +2. **Container failures** - Ensure podman runs as user service, not system service + ```bash + systemctl status podman.socket podman.service + systemctl disable --now podman.socket podman.service + systemctl enable --user --now podman.socket podman.service + ``` + +3. **Too many containers** - Increase inotify watches + ```bash + echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + ``` + +4. **Key limits** - Increase max keys + ```bash + echo kernel.keys.maxkeys=1000 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + ``` + +5. **Host resolution** - Add to `/etc/hosts`: + ``` + 127.0.0.1 apiserver.localhost + 127.0.0.1 rekor.localhost + ``` + +## Key Configuration + +### Policy Sources +Policies can be loaded from: +- OCI registries: `oci::quay.io/repo/policy:tag` +- Git repositories: `git::https://github.com/repo//path` +- Local files/directories + +### Debug Mode +- Use `--debug` flag or `EC_DEBUG=1` environment variable +- Debug mode preserves temporary `ec-work-*` directories for inspection + +## Special Considerations + +### CGO and DNS Resolution +Binaries are built with `CGO_ENABLED=0` for OS compatibility, which affects DNS resolution. The Go native resolver cannot resolve second-level localhost domains like `apiserver.localhost`, requiring manual `/etc/hosts` entries for acceptance tests. + +### Multi-Architecture Support +The build system supports all major platforms and architectures. Use `make dist` to build for all supported targets or `make dist/ec__` for specific platforms. + +### Policy Rule Filtering System +The evaluation system includes sophisticated rule filtering that operates at multiple levels: + +#### Pre-Evaluation Filtering (Package Level) +1. **Pipeline Intention Filtering** (ECPolicyResolver only) + - When `pipeline_intention` is set: only include packages with matching metadata + - When not set: only include general-purpose rules (no pipeline_intention metadata) + +2. **Rule-by-Rule Evaluation** + - Each rule is scored against include/exclude criteria + - Scoring system: collections (10pts), packages (10pts/level), rules (+100pts), terms (+100pts) + - Higher score determines inclusion/exclusion + +3. **Package-Level Determination** + - If ANY rule in package is included → Package is included + - Package runs through conftest evaluation + +#### Post-Evaluation Filtering (Result Level) +- UnifiedPostEvaluationFilter processes all results using same PolicyResolver +- Filters warnings, failures, exceptions, skipped results +- Applies severity logic (promotion/demotion based on metadata) +- Handles effective time filtering (future-effective failures → warnings) + +#### Term-Based Filtering +Terms provide fine-grained control over specific rule instances: +- Example: `tasks.required_untrusted_task_found:clamav-scan` (scores 210pts) +- Can override general patterns like `tasks.*` (10pts) +- Terms are extracted from result metadata during filtering + +### Working with Rule Filtering Code +When modifying policy evaluation or filtering logic: +1. Read `.cursor/rules/package_filtering_process.mdc` for architecture overview +2. Read `.cursor/rules/rule_filtering_process.mdc` for detailed filtering flow +3. Main filtering code is in `internal/evaluator/filters.go` +4. Integration point is in `internal/evaluator/conftest_evaluator.go` + +### Working with VSA Code +When modifying VSA functionality: +1. Read `.cursor/rules/vsa_functionality.mdc` for complete documentation +2. Understand the layered architecture (9 layers from interfaces to CLI) +3. VSA code is in `internal/validate/vsa/` directory +4. CLI implementation in `cmd/validate/vsa.go` +5. Signature verification is enabled by default and implemented via DSSE envelopes + +## Additional Documentation + +For detailed implementation guides, see: +- `.cursor/rules/package_filtering_process.mdc` - Pluggable rule filtering system +- `.cursor/rules/rule_filtering_process.mdc` - Complete rule filtering process +- `.cursor/rules/vsa_functionality.mdc` - VSA architecture and workflows diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c81f3b5d0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,14 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +**Note:** The comprehensive agent instructions have been moved to [AGENTS.md](AGENTS.md). Please refer to that file for detailed documentation about: +- Essential commands (building, testing, linting) +- Architecture and core components +- Policy evaluation and rule filtering system +- VSA (Verification Summary Attestation) functionality +- Testing strategy and acceptance test framework +- Development environment setup and troubleshooting +- Special considerations (CGO/DNS resolution, multi-architecture support, rule filtering) + +This file is kept for backward compatibility. All future updates should be made to AGENTS.md. diff --git a/acceptance/attestation/attestation.go b/acceptance/attestation/attestation.go index 1d14c0f57..6464e81d5 100644 --- a/acceptance/attestation/attestation.go +++ b/acceptance/attestation/attestation.go @@ -38,6 +38,7 @@ const ( PredicateBuilderID = "https://tekton.dev/chains/v2" PredicateBuilderType = "https://tekton.dev/attestations/chains/pipelinerun@v2" PredicateType = "slsaprovenance" + PredicateTypeV1 = "slsaprovenance1" ) // CreateStatementFor creates an empty statement that can be further customized @@ -70,9 +71,44 @@ func CreateStatementFor(imageName string, image v1.Image) (*in_toto.ProvenanceSt return nil, fmt.Errorf("received statement of unsupported type: %v", obj) } +// CreateV1StatementFor creates an empty SLSA v1.0 statement that can be further customized +// and subsequently signed by SignStatement. +func CreateV1StatementFor(imageName string, image v1.Image) (*in_toto.ProvenanceStatementSLSA1, error) { + digest, err := image.Digest() + if err != nil { + return nil, err + } + + obj, err := attestation.GenerateStatement(attestation.GenerateOpts{ + Predicate: bytes.NewReader([]byte(fmt.Sprintf(`{ + "buildDefinition": { + "buildType": "%s", + "externalParameters": {} + }, + "runDetails": { + "builder": { + "id": "%s" + } + } + }`, PredicateBuilderType, PredicateBuilderID))), + Type: PredicateTypeV1, + Digest: digest.Hex, + Repo: imageName, + }) + if err != nil { + return nil, err + } + + if statement, ok := obj.(in_toto.ProvenanceStatementSLSA1); ok { + return &statement, nil + } + + return nil, fmt.Errorf("received statement of unsupported type: %v", obj) +} + // SignStatement signs the provided statement with the named key. The key needs // to be previously generated with the functionality from the crypto package. -func SignStatement(ctx context.Context, keyName string, statement in_toto.ProvenanceStatementSLSA02) ([]byte, error) { +func SignStatement(ctx context.Context, keyName string, statement any) ([]byte, error) { payload, err := json.Marshal(statement) if err != nil { return nil, err diff --git a/acceptance/examples/sigstore.rego b/acceptance/examples/sigstore.rego index a8ed11644..3b3218892 100644 --- a/acceptance/examples/sigstore.rego +++ b/acceptance/examples/sigstore.rego @@ -80,7 +80,8 @@ _errors contains error if { info := ec.sigstore.verify_attestation(_image_ref, _sigstore_opts) some att in info.attestations - att.statement.predicateType != "https://slsa.dev/provenance/v0.2" + # Support both SLSA v0.2 and v1 predicate types + not _is_supported_slsa_predicate(att.statement.predicateType) error := sprintf("unexpected statement predicate: %s", [att.statement.predicateType]) } @@ -115,5 +116,18 @@ valid_signature(sig) if { } _builder_id(att) := value if { + # SLSA v0.2: predicate.builder.id value := att.statement.predicate.builder.id +} else := value if { + # SLSA v1: predicate.runDetails.builder.id + value := att.statement.predicate.runDetails.builder.id } else := "MISSING" + +# Helper to check if predicate type is a supported SLSA version +_is_supported_slsa_predicate(predicate_type) if { + predicate_type == "https://slsa.dev/provenance/v0.2" +} + +_is_supported_slsa_predicate(predicate_type) if { + predicate_type == "https://slsa.dev/provenance/v1" +} diff --git a/acceptance/go.mod b/acceptance/go.mod index f76201932..75cdd38fe 100644 --- a/acceptance/go.mod +++ b/acceptance/go.mod @@ -14,7 +14,7 @@ require ( github.com/go-git/go-git/v5 v5.13.0 github.com/go-openapi/strfmt v0.23.0 github.com/google/go-containerregistry v0.20.7 - github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09 + github.com/in-toto/in-toto-golang v0.9.0 github.com/konflux-ci/application-api v0.0.0-20240812090716-e7eb2ecfb409 github.com/opencontainers/image-spec v1.1.1 github.com/otiai10/copy v1.14.0 @@ -26,7 +26,7 @@ require ( github.com/sigstore/rekor v1.3.6 github.com/sigstore/sigstore v1.8.9 github.com/stretchr/testify v1.11.1 - github.com/tektoncd/cli v0.38.0 + github.com/tektoncd/cli v0.37.1 github.com/tektoncd/pipeline v0.66.0 github.com/testcontainers/testcontainers-go v0.34.0 github.com/transparency-dev/merkle v0.0.2 @@ -140,7 +140,6 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect - github.com/in-toto/attestation v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect diff --git a/acceptance/go.sum b/acceptance/go.sum index d11369680..453e919bc 100644 --- a/acceptance/go.sum +++ b/acceptance/go.sum @@ -261,8 +261,8 @@ github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= -github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo= @@ -529,6 +529,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= @@ -580,8 +582,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/in-toto/attestation v1.1.0 h1:oRWzfmZPDSctChD0VaQV7MJrywKOzyNrtpENQFq//2Q= github.com/in-toto/attestation v1.1.0/go.mod h1:DB59ytd3z7cIHgXxwpSX2SABrU6WJUKg/grpdgHVgVs= -github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09 h1:cwCITdi9pF50CF8uh40qDbkJ/VrEVzx5AoaHP7OPdEo= -github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09/go.mod h1:yGCBn2JKF1m26FX8GmkcLSOFVjB6khWRxFsHwWIg7hw= +github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= +github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -737,6 +739,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -896,12 +900,12 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= -github.com/tektoncd/cli v0.38.0 h1:mH4xMxehfPOD2Ar0KZgWtb9Wh3r3S6wSAAZo8pTcruI= -github.com/tektoncd/cli v0.38.0/go.mod h1:MLcBL6RH70XgKeaKV3l3YcvHjASyTELZACG9ZfVA5yg= +github.com/tektoncd/cli v0.37.1 h1:bAf8sISiI7WGsS7Ov2pFeQ86+M8AQB/CjFbEfJOn+w0= +github.com/tektoncd/cli v0.37.1/go.mod h1:voq7pZzbA/dohTDE4l3iby3Wnnqt0XtkYUBbj1h3u5o= github.com/tektoncd/pipeline v0.66.0 h1:WLL98YEgWzblSAD2mPbpZN97tkOC50wiftaW+8+6zTY= github.com/tektoncd/pipeline v0.66.0/go.mod h1:V3cyfxxc7b3GLT2a13GX2mWA86qmxWhh4mOp4gfFQwQ= -github.com/tektoncd/triggers v0.29.0 h1:piRTJT1Sjq3xmGnR50V54oG0NlsszKETLxdCGhgSNQQ= -github.com/tektoncd/triggers v0.29.0/go.mod h1:CHE2QhjYkECFCpvPLpiANhI/hIlJUxL03ulTNEgbT10= +github.com/tektoncd/triggers v0.27.0 h1:c55e/YJF6Vs5BEarqDYksFYuR4sFbmAVEqrLNPZvXUk= +github.com/tektoncd/triggers v0.27.0/go.mod h1:DkkAkdSd9aAW9RklUVyFRKQ8kONmZQw4Ur2G1r3wFQo= github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= diff --git a/acceptance/image/image.go b/acceptance/image/image.go index 967d20039..7b8dd1231 100644 --- a/acceptance/image/image.go +++ b/acceptance/image/image.go @@ -296,7 +296,7 @@ func CreateAndPushImageSignature(ctx context.Context, imageName string, keyName // image, same as `cosign attest` or Tekton Chains would, and pushes it to the stub // registry as a new tag for that image akin to how cosign and Tekton Chains do it func CreateAndPushAttestation(ctx context.Context, imageName, keyName string) (context.Context, error) { - return createAndPushAttestationWithPatches(ctx, imageName, keyName, nil) + return createAndPushAttestationInternal(ctx, imageName, keyName, nil, false) } // createAndPushAttestation for a named image in the Context creates an attestation @@ -306,6 +306,11 @@ func CreateAndPushAttestation(ctx context.Context, imageName, keyName string) (c // statement as required by the tests. This implementation now includes transparency // log upload to generate bundle information like Tekton Chains does for attestations. func createAndPushAttestationWithPatches(ctx context.Context, imageName, keyName string, patches *godog.Table) (context.Context, error) { + return createAndPushAttestationInternal(ctx, imageName, keyName, patches, false) +} + +// createAndPushAttestationInternal is the internal implementation that supports both SLSA v0.2 and v1 +func createAndPushAttestationInternal(ctx context.Context, imageName, keyName string, patches *godog.Table, useV1 bool) (context.Context, error) { var state *imageState ctx, err := testenv.SetupState(ctx, &state) if err != nil { @@ -322,22 +327,28 @@ func createAndPushAttestationWithPatches(ctx context.Context, imageName, keyName return ctx, err } - // generates a mostly-empty statement, but with the required fields already filled in - // at this point we could add more data to the statement but the minimum works, we'll - // need to add more data to the attestation in more elaborate tests so: - // TODO: create a hook to add more data to the attestation - statement, err := attestation.CreateStatementFor(imageName, image) - if err != nil { - return ctx, err - } + var statement any - statement, err = applyPatches(statement, patches) - if err != nil { - return ctx, err + if useV1 { + // SLSA v1.0 + statement, err = attestation.CreateV1StatementFor(imageName, image) + if err != nil { + return ctx, err + } + } else { + // SLSA v0.2 + v02Statement, err := attestation.CreateStatementFor(imageName, image) + if err != nil { + return ctx, err + } + + statement, err = applyPatches(v02Statement, patches) + if err != nil { + return ctx, err + } } - // signs the attestation with the named key - signedAttestation, err := attestation.SignStatement(ctx, keyName, *statement) + signedAttestation, err := attestation.SignStatement(ctx, keyName, statement) if err != nil { return ctx, err } @@ -484,6 +495,12 @@ func createAndPushAttestationWithPatches(ctx context.Context, imageName, keyName return ctx, nil } +// CreateAndPushV1Attestation for a named image creates a SLSA v1.0 attestation +// and pushes it to the stub registry +func CreateAndPushV1Attestation(ctx context.Context, imageName, keyName string) (context.Context, error) { + return createAndPushAttestationInternal(ctx, imageName, keyName, nil, true) +} + // CreateAndPushImageWithParent creates a parent image and a test image for the given imageName. func CreateAndPushImageWithParent(ctx context.Context, imageName string) (context.Context, error) { var err error @@ -1164,6 +1181,7 @@ func AddStepsTo(sc *godog.ScenarioContext) { sc.Step(`^a valid image signature of "([^"]*)" image signed by the "([^"]*)" key$`, CreateAndPushImageSignature) sc.Step(`^a valid attestation of "([^"]*)" signed by the "([^"]*)" key$`, CreateAndPushAttestation) sc.Step(`^a valid attestation of "([^"]*)" signed by the "([^"]*)" key, patched with$`, createAndPushAttestationWithPatches) + sc.Step(`^a valid slsa v1 attestation of "([^"]*)" signed by the "([^"]*)" key$`, CreateAndPushV1Attestation) sc.Step(`^a signed and attested keyless image named "([^"]*)"$`, createAndPushKeylessImage) sc.Step(`^a OCI policy bundle named "([^"]*)" with$`, createAndPushPolicyBundle) sc.Step(`^an image named "([^"]*)" with signature from "([^"]*)"$`, steal("sig")) diff --git a/features/__snapshots__/validate_image.snap b/features/__snapshots__/validate_image.snap index 5d68f9fd0..bd401be6a 100755 --- a/features/__snapshots__/validate_image.snap +++ b/features/__snapshots__/validate_image.snap @@ -5274,3 +5274,80 @@ Error: success criteria not met [signatures with embedded bundles verify without external rekor queries:stderr - 1] --- + +[SLSA v1 attestation support:stdout - 1] +{ + "success": true, + "components": [ + { + "name": "Unnamed", + "containerImage": "${REGISTRY}/acceptance/slsa-v1-test@sha256:${REGISTRY_acceptance/slsa-v1-test:latest_DIGEST}", + "source": {}, + "successes": [ + { + "msg": "Pass", + "metadata": { + "code": "builtin.attestation.signature_check" + } + }, + { + "msg": "Pass", + "metadata": { + "code": "builtin.attestation.syntax_check" + } + }, + { + "msg": "Pass", + "metadata": { + "code": "builtin.image.signature_check" + } + }, + { + "msg": "Pass", + "metadata": { + "code": "sigstore.valid" + } + } + ], + "success": true, + "signatures": [ + { + "keyid": "", + "sig": "${IMAGE_SIGNATURE_acceptance/slsa-v1-test}" + } + ], + "attestations": [ + { + "type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v1", + "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", + "signatures": [ + { + "keyid": "", + "sig": "${ATTESTATION_SIGNATURE_acceptance/slsa-v1-test}" + } + ] + } + ] + } + ], + "key": "${known_PUBLIC_KEY_JSON}", + "policy": { + "sources": [ + { + "policy": [ + "git::${GITHOST}/git/sigstore-v1-policy.git?ref=${LATEST_COMMIT}" + ] + } + ], + "rekorUrl": "${REKOR}", + "publicKey": "${known_PUBLIC_KEY}" + }, + "ec-version": "${EC_VERSION}", + "effective-time": "${TIMESTAMP}" +} +--- + +[SLSA v1 attestation support:stderr - 1] + +--- diff --git a/features/validate_image.feature b/features/validate_image.feature index 7dc244400..6b9ad0f9e 100644 --- a/features/validate_image.feature +++ b/features/validate_image.feature @@ -1175,3 +1175,26 @@ Feature: evaluate enterprise contract Then the exit status should be 1 And the output should match the snapshot And the "${TMPDIR}/output.json" file should match the snapshot + + Scenario: SLSA v1 attestation support + Given a key pair named "known" + And an image named "acceptance/slsa-v1-test" + And a valid image signature of "acceptance/slsa-v1-test" image signed by the "known" key + And a valid slsa v1 attestation of "acceptance/slsa-v1-test" signed by the "known" key + And a git repository named "sigstore-v1-policy" with + | main.rego | examples/sigstore.rego | + And policy configuration named "ec-policy" with specification + """ + { + "sources": [ + { + "policy": [ + "git::https://${GITHOST}/git/sigstore-v1-policy.git" + ] + } + ] + } + """ + When ec command is run with "validate image --image ${REGISTRY}/acceptance/slsa-v1-test --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --rekor-url ${REKOR} --show-successes --output json" + Then the exit status should be 0 + And the output should match the snapshot diff --git a/features/vsa.feature b/features/vsa.feature index 7b11a8230..f95a213c6 100644 --- a/features/vsa.feature +++ b/features/vsa.feature @@ -151,6 +151,7 @@ Feature: VSA generation and storage } """ # First, generate a VSA and upload it to Rekor + Given VSA upload to Rekor should be expected When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-existing-image@sha256:${REGISTRY_acceptance/vsa-existing-image:latest_DIGEST} --policy acceptance/vsa-existing-ec-policy --public-key ${vsa-existing_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-existing_PRIVATE_KEY} --vsa-upload rekor@${REKOR} --vsa-expiration 0 --output json" Then the exit status should be 0 And VSA should be uploaded to Rekor successfully diff --git a/internal/attestation/__snapshots__/slsa_provenance_v1_test.snap b/internal/attestation/__snapshots__/slsa_provenance_v1_test.snap new file mode 100755 index 000000000..e245eaa75 --- /dev/null +++ b/internal/attestation/__snapshots__/slsa_provenance_v1_test.snap @@ -0,0 +1,102 @@ + +[TestSLSAProvenanceFromSignatureV1/valid_with_signature_from_payload - 1] +https://in-toto.io/Statement/v0.1 +[]signature.EntitySignature{ + { + KeyID: "6add046e38418d021a562c6a8633d5eca7379595", + Signature: "sig-1", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIG2TCCBl+gAwIBAgIUdtQgx3Mj6A3T0X7Oh8bS1nNABTEwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjMwNjA3MDMxNDEyWhcNMjMwNjA3MDMyNDEyWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEz6tsPZHx7njElmbGbMYxKiYneuofINbOE8Tg\n1gkyQcckWyu1xA/Fs0O1SpPkn/KJYLJ3J5ziqgd1EguuCqK3Z6OCBX4wggV6MA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUat0E\nbjhBjQIaVixqhjPV7Kc3lZUwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8waAYDVR0RAQH/BF4wXIZaaHR0cHM6Ly9naXRodWIuY29tL2NoYWluZ3VhcmQt\naW1hZ2VzL2ltYWdlcy8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnlhbWxAcmVm\ncy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9u\ncy5naXRodWJ1c2VyY29udGVudC5jb20wEgYKKwYBBAGDvzABAgQEcHVzaDA2Bgor\nBgEEAYO/MAEDBChlMWRjZGY3MGJlMzI2YTQ5NDI5NTc1NDYyMmZlMzQ2MzE2MDA1\nMzFhMCwGCisGAQQBg78wAQQEHi5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueWFt\nbDAmBgorBgEEAYO/MAEFBBhjaGFpbmd1YXJkLWltYWdlcy9pbWFnZXMwHQYKKwYB\nBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6\nLy90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTBqBgorBgEEAYO/\nMAEJBFwMWmh0dHBzOi8vZ2l0aHViLmNvbS9jaGFpbmd1YXJkLWltYWdlcy9pbWFn\nZXMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55YW1sQHJlZnMvaGVhZHMvbWFp\nbjA4BgorBgEEAYO/MAEKBCoMKGUxZGNkZjcwYmUzMjZhNDk0Mjk1NzU0NjIyZmUz\nNDYzMTYwMDUzMWEwHQYKKwYBBAGDvzABCwQPDA1naXRodWItaG9zdGVkMDsGCisG\nAQQBg78wAQwELQwraHR0cHM6Ly9naXRodWIuY29tL2NoYWluZ3VhcmQtaW1hZ2Vz\nL2ltYWdlczA4BgorBgEEAYO/MAENBCoMKGUxZGNkZjcwYmUzMjZhNDk0Mjk1NzU0\nNjIyZmUzNDYzMTYwMDUzMWEwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21h\naW4wGQYKKwYBBAGDvzABDwQLDAk1NjM1MTA5NTIwNAYKKwYBBAGDvzABEAQmDCRo\ndHRwczovL2dpdGh1Yi5jb20vY2hhaW5ndWFyZC1pbWFnZXMwGQYKKwYBBAGDvzAB\nEQQLDAkxMTMxOTg1NDUwagYKKwYBBAGDvzABEgRcDFpodHRwczovL2dpdGh1Yi5j\nb20vY2hhaW5ndWFyZC1pbWFnZXMvaW1hZ2VzLy5naXRodWIvd29ya2Zsb3dzL3Jl\nbGVhc2UueWFtbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABEwQqDChlMWRj\nZGY3MGJlMzI2YTQ5NDI5NTc1NDYyMmZlMzQ2MzE2MDA1MzFhMBQGCisGAQQBg78w\nARQEBgwEcHVzaDBeBgorBgEEAYO/MAEVBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9j\naGFpbmd1YXJkLWltYWdlcy9pbWFnZXMvYWN0aW9ucy9ydW5zLzUxOTU1MDc2MzYv\nYXR0ZW1wdHMvMTCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJln\nNwKiSl643jyt/4eKcoAvKe6OAAABiJPZADAAAAQDAEcwRQIgdHXB0QGS/GWkBnY1\nAZXSwb6/tbnnaVeWzde3t0fkkRMCIQC0bwdhWep548Cp4LzBPgGD0eioadqQdJHe\nXtVXBkD1dDAKBggqhkjOPQQDAwNoADBlAjBPpXDUSaAk5D6T1Eaqh+TRSQXr6rqV\nYxAJb/NgDbq8tTVLKustJDu2V9TQcpSzuKICMQDt0EAHmTISmKC8H3dciTrySh2l\nuS2rfl+L2AFS6DxAmVTBR3dlbrxQsUxshBWyH5s=\n-----END CERTIFICATE-----\n", + Chain: {"-----BEGIN CERTIFICATE-----\nMIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7\n7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS\n0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB\nBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp\nKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI\nzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR\nnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP\nmygUY7Ii2zbdCdliiow=\n-----END CERTIFICATE-----\n", "-----BEGIN CERTIFICATE-----\nMIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7\nXeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex\nX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j\nYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY\nwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ\nKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM\nWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9\nTNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\n-----END CERTIFICATE-----\n"}, + Metadata: {"Fulcio Build Config Digest":"e1dcdf70be326a494295754622fe34631600531a", "Fulcio Build Config URI":"https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main", "Fulcio Build Signer Digest":"e1dcdf70be326a494295754622fe34631600531a", "Fulcio Build Signer URI":"https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main", "Fulcio Build Trigger":"push", "Fulcio GitHub Workflow Name":".github/workflows/release.yaml", "Fulcio GitHub Workflow Ref":"refs/heads/main", "Fulcio GitHub Workflow Repository":"chainguard-images/images", "Fulcio GitHub Workflow SHA":"e1dcdf70be326a494295754622fe34631600531a", "Fulcio GitHub Workflow Trigger":"push", "Fulcio Issuer":"https://token.actions.githubusercontent.com", "Fulcio Issuer (V2)":"https://token.actions.githubusercontent.com", "Fulcio Run Invocation URI":"https://github.com/chainguard-images/images/actions/runs/5195507636/attempts/1", "Fulcio Runner Environment":"github-hosted", "Fulcio Source Repository Digest":"e1dcdf70be326a494295754622fe34631600531a", "Fulcio Source Repository Identifier":"563510952", "Fulcio Source Repository Owner Identifier":"113198545", "Fulcio Source Repository Owner URI":"https://github.com/chainguard-images", "Fulcio Source Repository Ref":"refs/heads/main", "Fulcio Source Repository URI":"https://github.com/chainguard-images/images", "Issuer":"CN=sigstore-intermediate,O=sigstore.dev", "Not After":"2023-06-07T03:24:12Z", "Not Before":"2023-06-07T03:14:12Z", "Serial Number":"76d420c77323e80dd3d17ece87c6d2d673400531", "Subject Alternative Name":"URIs:https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main"}, + }, + { + KeyID: "6add046e38418d021a562c6a8633d5eca7379595", + Signature: "sig-2", + Certificate: "-----BEGIN CERTIFICATE-----\nMIIG2TCCBl+gAwIBAgIUdtQgx3Mj6A3T0X7Oh8bS1nNABTEwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjMwNjA3MDMxNDEyWhcNMjMwNjA3MDMyNDEyWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEz6tsPZHx7njElmbGbMYxKiYneuofINbOE8Tg\n1gkyQcckWyu1xA/Fs0O1SpPkn/KJYLJ3J5ziqgd1EguuCqK3Z6OCBX4wggV6MA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUat0E\nbjhBjQIaVixqhjPV7Kc3lZUwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8waAYDVR0RAQH/BF4wXIZaaHR0cHM6Ly9naXRodWIuY29tL2NoYWluZ3VhcmQt\naW1hZ2VzL2ltYWdlcy8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnlhbWxAcmVm\ncy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9u\ncy5naXRodWJ1c2VyY29udGVudC5jb20wEgYKKwYBBAGDvzABAgQEcHVzaDA2Bgor\nBgEEAYO/MAEDBChlMWRjZGY3MGJlMzI2YTQ5NDI5NTc1NDYyMmZlMzQ2MzE2MDA1\nMzFhMCwGCisGAQQBg78wAQQEHi5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueWFt\nbDAmBgorBgEEAYO/MAEFBBhjaGFpbmd1YXJkLWltYWdlcy9pbWFnZXMwHQYKKwYB\nBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6\nLy90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTBqBgorBgEEAYO/\nMAEJBFwMWmh0dHBzOi8vZ2l0aHViLmNvbS9jaGFpbmd1YXJkLWltYWdlcy9pbWFn\nZXMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55YW1sQHJlZnMvaGVhZHMvbWFp\nbjA4BgorBgEEAYO/MAEKBCoMKGUxZGNkZjcwYmUzMjZhNDk0Mjk1NzU0NjIyZmUz\nNDYzMTYwMDUzMWEwHQYKKwYBBAGDvzABCwQPDA1naXRodWItaG9zdGVkMDsGCisG\nAQQBg78wAQwELQwraHR0cHM6Ly9naXRodWIuY29tL2NoYWluZ3VhcmQtaW1hZ2Vz\nL2ltYWdlczA4BgorBgEEAYO/MAENBCoMKGUxZGNkZjcwYmUzMjZhNDk0Mjk1NzU0\nNjIyZmUzNDYzMTYwMDUzMWEwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21h\naW4wGQYKKwYBBAGDvzABDwQLDAk1NjM1MTA5NTIwNAYKKwYBBAGDvzABEAQmDCRo\ndHRwczovL2dpdGh1Yi5jb20vY2hhaW5ndWFyZC1pbWFnZXMwGQYKKwYBBAGDvzAB\nEQQLDAkxMTMxOTg1NDUwagYKKwYBBAGDvzABEgRcDFpodHRwczovL2dpdGh1Yi5j\nb20vY2hhaW5ndWFyZC1pbWFnZXMvaW1hZ2VzLy5naXRodWIvd29ya2Zsb3dzL3Jl\nbGVhc2UueWFtbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABEwQqDChlMWRj\nZGY3MGJlMzI2YTQ5NDI5NTc1NDYyMmZlMzQ2MzE2MDA1MzFhMBQGCisGAQQBg78w\nARQEBgwEcHVzaDBeBgorBgEEAYO/MAEVBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9j\naGFpbmd1YXJkLWltYWdlcy9pbWFnZXMvYWN0aW9ucy9ydW5zLzUxOTU1MDc2MzYv\nYXR0ZW1wdHMvMTCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJln\nNwKiSl643jyt/4eKcoAvKe6OAAABiJPZADAAAAQDAEcwRQIgdHXB0QGS/GWkBnY1\nAZXSwb6/tbnnaVeWzde3t0fkkRMCIQC0bwdhWep548Cp4LzBPgGD0eioadqQdJHe\nXtVXBkD1dDAKBggqhkjOPQQDAwNoADBlAjBPpXDUSaAk5D6T1Eaqh+TRSQXr6rqV\nYxAJb/NgDbq8tTVLKustJDu2V9TQcpSzuKICMQDt0EAHmTISmKC8H3dciTrySh2l\nuS2rfl+L2AFS6DxAmVTBR3dlbrxQsUxshBWyH5s=\n-----END CERTIFICATE-----\n", + Chain: {"-----BEGIN CERTIFICATE-----\nMIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7\n7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS\n0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB\nBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp\nKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI\nzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR\nnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP\nmygUY7Ii2zbdCdliiow=\n-----END CERTIFICATE-----\n", "-----BEGIN CERTIFICATE-----\nMIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7\nXeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex\nX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j\nYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY\nwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ\nKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM\nWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9\nTNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\n-----END CERTIFICATE-----\n"}, + Metadata: {"Fulcio Build Config Digest":"e1dcdf70be326a494295754622fe34631600531a", "Fulcio Build Config URI":"https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main", "Fulcio Build Signer Digest":"e1dcdf70be326a494295754622fe34631600531a", "Fulcio Build Signer URI":"https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main", "Fulcio Build Trigger":"push", "Fulcio GitHub Workflow Name":".github/workflows/release.yaml", "Fulcio GitHub Workflow Ref":"refs/heads/main", "Fulcio GitHub Workflow Repository":"chainguard-images/images", "Fulcio GitHub Workflow SHA":"e1dcdf70be326a494295754622fe34631600531a", "Fulcio GitHub Workflow Trigger":"push", "Fulcio Issuer":"https://token.actions.githubusercontent.com", "Fulcio Issuer (V2)":"https://token.actions.githubusercontent.com", "Fulcio Run Invocation URI":"https://github.com/chainguard-images/images/actions/runs/5195507636/attempts/1", "Fulcio Runner Environment":"github-hosted", "Fulcio Source Repository Digest":"e1dcdf70be326a494295754622fe34631600531a", "Fulcio Source Repository Identifier":"563510952", "Fulcio Source Repository Owner Identifier":"113198545", "Fulcio Source Repository Owner URI":"https://github.com/chainguard-images", "Fulcio Source Repository Ref":"refs/heads/main", "Fulcio Source Repository URI":"https://github.com/chainguard-images/images", "Issuer":"CN=sigstore-intermediate,O=sigstore.dev", "Not After":"2023-06-07T03:24:12Z", "Not Before":"2023-06-07T03:14:12Z", "Serial Number":"76d420c77323e80dd3d17ece87c6d2d673400531", "Subject Alternative Name":"URIs:https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main"}, + }, +} +--- + +[TestMarshalV1 - 1] +{ + "predicateBuildType": "https://my.build.type", + "predicateType": "https://slsa.dev/provenance/v1", + "signatures": [ + { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIG2TCCBl+gAwIBAgIUdtQgx3Mj6A3T0X7Oh8bS1nNABTEwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjMwNjA3MDMxNDEyWhcNMjMwNjA3MDMyNDEyWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEz6tsPZHx7njElmbGbMYxKiYneuofINbOE8Tg\n1gkyQcckWyu1xA/Fs0O1SpPkn/KJYLJ3J5ziqgd1EguuCqK3Z6OCBX4wggV6MA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUat0E\nbjhBjQIaVixqhjPV7Kc3lZUwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8waAYDVR0RAQH/BF4wXIZaaHR0cHM6Ly9naXRodWIuY29tL2NoYWluZ3VhcmQt\naW1hZ2VzL2ltYWdlcy8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnlhbWxAcmVm\ncy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9u\ncy5naXRodWJ1c2VyY29udGVudC5jb20wEgYKKwYBBAGDvzABAgQEcHVzaDA2Bgor\nBgEEAYO/MAEDBChlMWRjZGY3MGJlMzI2YTQ5NDI5NTc1NDYyMmZlMzQ2MzE2MDA1\nMzFhMCwGCisGAQQBg78wAQQEHi5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueWFt\nbDAmBgorBgEEAYO/MAEFBBhjaGFpbmd1YXJkLWltYWdlcy9pbWFnZXMwHQYKKwYB\nBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6\nLy90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTBqBgorBgEEAYO/\nMAEJBFwMWmh0dHBzOi8vZ2l0aHViLmNvbS9jaGFpbmd1YXJkLWltYWdlcy9pbWFn\nZXMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55YW1sQHJlZnMvaGVhZHMvbWFp\nbjA4BgorBgEEAYO/MAEKBCoMKGUxZGNkZjcwYmUzMjZhNDk0Mjk1NzU0NjIyZmUz\nNDYzMTYwMDUzMWEwHQYKKwYBBAGDvzABCwQPDA1naXRodWItaG9zdGVkMDsGCisG\nAQQBg78wAQwELQwraHR0cHM6Ly9naXRodWIuY29tL2NoYWluZ3VhcmQtaW1hZ2Vz\nL2ltYWdlczA4BgorBgEEAYO/MAENBCoMKGUxZGNkZjcwYmUzMjZhNDk0Mjk1NzU0\nNjIyZmUzNDYzMTYwMDUzMWEwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21h\naW4wGQYKKwYBBAGDvzABDwQLDAk1NjM1MTA5NTIwNAYKKwYBBAGDvzABEAQmDCRo\ndHRwczovL2dpdGh1Yi5jb20vY2hhaW5ndWFyZC1pbWFnZXMwGQYKKwYBBAGDvzAB\nEQQLDAkxMTMxOTg1NDUwagYKKwYBBAGDvzABEgRcDFpodHRwczovL2dpdGh1Yi5j\nb20vY2hhaW5ndWFyZC1pbWFnZXMvaW1hZ2VzLy5naXRodWIvd29ya2Zsb3dzL3Jl\nbGVhc2UueWFtbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABEwQqDChlMWRj\nZGY3MGJlMzI2YTQ5NDI5NTc1NDYyMmZlMzQ2MzE2MDA1MzFhMBQGCisGAQQBg78w\nARQEBgwEcHVzaDBeBgorBgEEAYO/MAEVBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9j\naGFpbmd1YXJkLWltYWdlcy9pbWFnZXMvYWN0aW9ucy9ydW5zLzUxOTU1MDc2MzYv\nYXR0ZW1wdHMvMTCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJln\nNwKiSl643jyt/4eKcoAvKe6OAAABiJPZADAAAAQDAEcwRQIgdHXB0QGS/GWkBnY1\nAZXSwb6/tbnnaVeWzde3t0fkkRMCIQC0bwdhWep548Cp4LzBPgGD0eioadqQdJHe\nXtVXBkD1dDAKBggqhkjOPQQDAwNoADBlAjBPpXDUSaAk5D6T1Eaqh+TRSQXr6rqV\nYxAJb/NgDbq8tTVLKustJDu2V9TQcpSzuKICMQDt0EAHmTISmKC8H3dciTrySh2l\nuS2rfl+L2AFS6DxAmVTBR3dlbrxQsUxshBWyH5s=\n-----END CERTIFICATE-----\n", + "chain": [ + "-----BEGIN CERTIFICATE-----\nMIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7\n7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS\n0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB\nBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp\nKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI\nzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR\nnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP\nmygUY7Ii2zbdCdliiow=\n-----END CERTIFICATE-----\n", + "-----BEGIN CERTIFICATE-----\nMIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7\nXeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex\nX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j\nYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY\nwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ\nKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM\nWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9\nTNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\n-----END CERTIFICATE-----\n" + ], + "keyid": "6add046e38418d021a562c6a8633d5eca7379595", + "metadata": { + "Fulcio Build Config Digest": "e1dcdf70be326a494295754622fe34631600531a", + "Fulcio Build Config URI": "https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main", + "Fulcio Build Signer Digest": "e1dcdf70be326a494295754622fe34631600531a", + "Fulcio Build Signer URI": "https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main", + "Fulcio Build Trigger": "push", + "Fulcio GitHub Workflow Name": ".github/workflows/release.yaml", + "Fulcio GitHub Workflow Ref": "refs/heads/main", + "Fulcio GitHub Workflow Repository": "chainguard-images/images", + "Fulcio GitHub Workflow SHA": "e1dcdf70be326a494295754622fe34631600531a", + "Fulcio GitHub Workflow Trigger": "push", + "Fulcio Issuer": "https://token.actions.githubusercontent.com", + "Fulcio Issuer (V2)": "https://token.actions.githubusercontent.com", + "Fulcio Run Invocation URI": "https://github.com/chainguard-images/images/actions/runs/5195507636/attempts/1", + "Fulcio Runner Environment": "github-hosted", + "Fulcio Source Repository Digest": "e1dcdf70be326a494295754622fe34631600531a", + "Fulcio Source Repository Identifier": "563510952", + "Fulcio Source Repository Owner Identifier": "113198545", + "Fulcio Source Repository Owner URI": "https://github.com/chainguard-images", + "Fulcio Source Repository Ref": "refs/heads/main", + "Fulcio Source Repository URI": "https://github.com/chainguard-images/images", + "Issuer": "CN=sigstore-intermediate,O=sigstore.dev", + "Not After": "2023-06-07T03:24:12Z", + "Not Before": "2023-06-07T03:14:12Z", + "Serial Number": "76d420c77323e80dd3d17ece87c6d2d673400531", + "Subject Alternative Name": "URIs:https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main" + }, + "sig": "sig-from-cert" + }, + { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIG2TCCBl+gAwIBAgIUdtQgx3Mj6A3T0X7Oh8bS1nNABTEwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjMwNjA3MDMxNDEyWhcNMjMwNjA3MDMyNDEyWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEz6tsPZHx7njElmbGbMYxKiYneuofINbOE8Tg\n1gkyQcckWyu1xA/Fs0O1SpPkn/KJYLJ3J5ziqgd1EguuCqK3Z6OCBX4wggV6MA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUat0E\nbjhBjQIaVixqhjPV7Kc3lZUwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8waAYDVR0RAQH/BF4wXIZaaHR0cHM6Ly9naXRodWIuY29tL2NoYWluZ3VhcmQt\naW1hZ2VzL2ltYWdlcy8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnlhbWxAcmVm\ncy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9u\ncy5naXRodWJ1c2VyY29udGVudC5jb20wEgYKKwYBBAGDvzABAgQEcHVzaDA2Bgor\nBgEEAYO/MAEDBChlMWRjZGY3MGJlMzI2YTQ5NDI5NTc1NDYyMmZlMzQ2MzE2MDA1\nMzFhMCwGCisGAQQBg78wAQQEHi5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueWFt\nbDAmBgorBgEEAYO/MAEFBBhjaGFpbmd1YXJkLWltYWdlcy9pbWFnZXMwHQYKKwYB\nBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6\nLy90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTBqBgorBgEEAYO/\nMAEJBFwMWmh0dHBzOi8vZ2l0aHViLmNvbS9jaGFpbmd1YXJkLWltYWdlcy9pbWFn\nZXMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55YW1sQHJlZnMvaGVhZHMvbWFp\nbjA4BgorBgEEAYO/MAEKBCoMKGUxZGNkZjcwYmUzMjZhNDk0Mjk1NzU0NjIyZmUz\nNDYzMTYwMDUzMWEwHQYKKwYBBAGDvzABCwQPDA1naXRodWItaG9zdGVkMDsGCisG\nAQQBg78wAQwELQwraHR0cHM6Ly9naXRodWIuY29tL2NoYWluZ3VhcmQtaW1hZ2Vz\nL2ltYWdlczA4BgorBgEEAYO/MAENBCoMKGUxZGNkZjcwYmUzMjZhNDk0Mjk1NzU0\nNjIyZmUzNDYzMTYwMDUzMWEwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21h\naW4wGQYKKwYBBAGDvzABDwQLDAk1NjM1MTA5NTIwNAYKKwYBBAGDvzABEAQmDCRo\ndHRwczovL2dpdGh1Yi5jb20vY2hhaW5ndWFyZC1pbWFnZXMwGQYKKwYBBAGDvzAB\nEQQLDAkxMTMxOTg1NDUwagYKKwYBBAGDvzABEgRcDFpodHRwczovL2dpdGh1Yi5j\nb20vY2hhaW5ndWFyZC1pbWFnZXMvaW1hZ2VzLy5naXRodWIvd29ya2Zsb3dzL3Jl\nbGVhc2UueWFtbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABEwQqDChlMWRj\nZGY3MGJlMzI2YTQ5NDI5NTc1NDYyMmZlMzQ2MzE2MDA1MzFhMBQGCisGAQQBg78w\nARQEBgwEcHVzaDBeBgorBgEEAYO/MAEVBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9j\naGFpbmd1YXJkLWltYWdlcy9pbWFnZXMvYWN0aW9ucy9ydW5zLzUxOTU1MDc2MzYv\nYXR0ZW1wdHMvMTCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJln\nNwKiSl643jyt/4eKcoAvKe6OAAABiJPZADAAAAQDAEcwRQIgdHXB0QGS/GWkBnY1\nAZXSwb6/tbnnaVeWzde3t0fkkRMCIQC0bwdhWep548Cp4LzBPgGD0eioadqQdJHe\nXtVXBkD1dDAKBggqhkjOPQQDAwNoADBlAjBPpXDUSaAk5D6T1Eaqh+TRSQXr6rqV\nYxAJb/NgDbq8tTVLKustJDu2V9TQcpSzuKICMQDt0EAHmTISmKC8H3dciTrySh2l\nuS2rfl+L2AFS6DxAmVTBR3dlbrxQsUxshBWyH5s=\n-----END CERTIFICATE-----\n", + "chain": [ + "-----BEGIN CERTIFICATE-----\nMIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7\n7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS\n0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB\nBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp\nKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI\nzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR\nnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP\nmygUY7Ii2zbdCdliiow=\n-----END CERTIFICATE-----\n", + "-----BEGIN CERTIFICATE-----\nMIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7\nXeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex\nX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j\nYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY\nwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ\nKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM\nWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9\nTNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\n-----END CERTIFICATE-----\n" + ], + "keyid": "6add046e38418d021a562c6a8633d5eca7379595", + "metadata": { + "Fulcio Build Config Digest": "e1dcdf70be326a494295754622fe34631600531a", + "Fulcio Build Config URI": "https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main", + "Fulcio Build Signer Digest": "e1dcdf70be326a494295754622fe34631600531a", + "Fulcio Build Signer URI": "https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main", + "Fulcio Build Trigger": "push", + "Fulcio GitHub Workflow Name": ".github/workflows/release.yaml", + "Fulcio GitHub Workflow Ref": "refs/heads/main", + "Fulcio GitHub Workflow Repository": "chainguard-images/images", + "Fulcio GitHub Workflow SHA": "e1dcdf70be326a494295754622fe34631600531a", + "Fulcio GitHub Workflow Trigger": "push", + "Fulcio Issuer": "https://token.actions.githubusercontent.com", + "Fulcio Issuer (V2)": "https://token.actions.githubusercontent.com", + "Fulcio Run Invocation URI": "https://github.com/chainguard-images/images/actions/runs/5195507636/attempts/1", + "Fulcio Runner Environment": "github-hosted", + "Fulcio Source Repository Digest": "e1dcdf70be326a494295754622fe34631600531a", + "Fulcio Source Repository Identifier": "563510952", + "Fulcio Source Repository Owner Identifier": "113198545", + "Fulcio Source Repository Owner URI": "https://github.com/chainguard-images", + "Fulcio Source Repository Ref": "refs/heads/main", + "Fulcio Source Repository URI": "https://github.com/chainguard-images/images", + "Issuer": "CN=sigstore-intermediate,O=sigstore.dev", + "Not After": "2023-06-07T03:24:12Z", + "Not Before": "2023-06-07T03:14:12Z", + "Serial Number": "76d420c77323e80dd3d17ece87c6d2d673400531", + "Subject Alternative Name": "URIs:https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main" + }, + "sig": "sig-from-cert" + } + ], + "type": "https://in-toto.io/Statement/v0.1" +} +--- diff --git a/internal/attestation/attestation.go b/internal/attestation/attestation.go index 081dd5da7..9d14857ba 100644 --- a/internal/attestation/attestation.go +++ b/internal/attestation/attestation.go @@ -166,19 +166,3 @@ func (p provenance) Signatures() []signature.EntitySignature { func (p provenance) Subject() []in_toto.Subject { return p.statement.Subject } - -// Todo: It seems odd that this does not contain the statement. -// (See also the equivalent method in slsa_provenance_02.go) -func (p provenance) MarshalJSON() ([]byte, error) { - val := struct { - Type string `json:"type"` - PredicateType string `json:"predicateType"` - Signatures []signature.EntitySignature `json:"signatures"` - }{ - Type: p.Type(), - PredicateType: p.PredicateType(), - Signatures: p.Signatures(), - } - - return json.Marshal(val) -} diff --git a/internal/attestation/attestation_test.go b/internal/attestation/attestation_test.go index 34253ff41..9a7b4b2c7 100644 --- a/internal/attestation/attestation_test.go +++ b/internal/attestation/attestation_test.go @@ -20,7 +20,6 @@ package attestation import ( "crypto/x509" - "encoding/json" "fmt" "testing" @@ -277,98 +276,3 @@ func TestProvenance_Subject(t *testing.T) { }) } } - -func TestProvenance_MarshalJSON(t *testing.T) { - mockSig1 := signature.EntitySignature{ - KeyID: "key1", - Signature: "sig1", - } - mockSig2 := signature.EntitySignature{ - KeyID: "key2", - Signature: "sig2", - } - - tests := []struct { - name string - provenance provenance - expectedErr bool - validate func(*testing.T, []byte) - }{ - { - name: "marshals successfully with single signature", - provenance: provenance{ - statement: in_toto.Statement{ - StatementHeader: in_toto.StatementHeader{ - PredicateType: "https://example.com/predicate/v1", - }, - }, - signatures: []signature.EntitySignature{mockSig1}, - }, - expectedErr: false, - validate: func(t *testing.T, data []byte) { - var result map[string]interface{} - err := json.Unmarshal(data, &result) - assert.NoError(t, err) - assert.Equal(t, "https://in-toto.io/Statement/v0.1", result["type"]) - assert.Equal(t, "https://example.com/predicate/v1", result["predicateType"]) - assert.Len(t, result["signatures"], 1) - }, - }, - { - name: "marshals successfully with multiple signatures", - provenance: provenance{ - statement: in_toto.Statement{ - StatementHeader: in_toto.StatementHeader{ - PredicateType: "https://example.com/predicate/v2", - }, - }, - signatures: []signature.EntitySignature{mockSig1, mockSig2}, - }, - expectedErr: false, - validate: func(t *testing.T, data []byte) { - var result map[string]interface{} - err := json.Unmarshal(data, &result) - assert.NoError(t, err) - assert.Equal(t, "https://in-toto.io/Statement/v0.1", result["type"]) - assert.Equal(t, "https://example.com/predicate/v2", result["predicateType"]) - assert.Len(t, result["signatures"], 2) - }, - }, - { - name: "marshals successfully with empty signatures", - provenance: provenance{ - statement: in_toto.Statement{ - StatementHeader: in_toto.StatementHeader{ - PredicateType: "https://example.com/predicate/v3", - }, - }, - signatures: []signature.EntitySignature{}, - }, - expectedErr: false, - validate: func(t *testing.T, data []byte) { - var result map[string]interface{} - err := json.Unmarshal(data, &result) - assert.NoError(t, err) - assert.Equal(t, "https://in-toto.io/Statement/v0.1", result["type"]) - assert.Equal(t, "https://example.com/predicate/v3", result["predicateType"]) - assert.Len(t, result["signatures"], 0) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data, err := tt.provenance.MarshalJSON() - - if tt.expectedErr { - assert.Error(t, err) - return - } - - assert.NoError(t, err) - if tt.validate != nil { - tt.validate(t, data) - } - }) - } -} diff --git a/internal/attestation/slsa_provenance_02.go b/internal/attestation/slsa_provenance_02.go index f83bb00f8..773e679eb 100644 --- a/internal/attestation/slsa_provenance_02.go +++ b/internal/attestation/slsa_provenance_02.go @@ -25,6 +25,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/oci" "github.com/conforma/cli/internal/signature" + "github.com/conforma/cli/pkg/schema" ) const ( @@ -64,6 +65,14 @@ func SLSAProvenanceFromSignature(sig oci.Signature) (Attestation, error) { return nil, fmt.Errorf("cannot create signed entity: %w", err) } + // Validate against SLSA v0.2 schema + var schemaValidation any + if err := json.Unmarshal(embedded, &schemaValidation); err == nil { + if err := schema.SLSA_Provenance_v0_2.Validate(schemaValidation); err != nil { + return nil, fmt.Errorf("attestation does not conform to SLSA v0.2 schema: %w", err) + } + } + return slsaProvenance{statement: statement, data: embedded, signatures: signatures}, nil } @@ -97,21 +106,3 @@ func (a slsaProvenance) Signatures() []signature.EntitySignature { func (a slsaProvenance) Subject() []in_toto.Subject { return a.statement.Subject } - -// Todo: It seems odd that this does not contain the statement. -// (See also the equivalent method in attestation.go) -func (a slsaProvenance) MarshalJSON() ([]byte, error) { - val := struct { - Type string `json:"type"` - PredicateType string `json:"predicateType"` - PredicateBuildType string `json:"predicateBuildType"` - Signatures []signature.EntitySignature `json:"signatures"` - }{ - Type: a.statement.Type, - PredicateType: a.statement.PredicateType, - PredicateBuildType: a.statement.Predicate.BuildType, - Signatures: a.signatures, - } - - return json.Marshal(val) -} diff --git a/internal/attestation/slsa_provenance_02_test.go b/internal/attestation/slsa_provenance_02_test.go index 863cd5f13..04f0112c7 100644 --- a/internal/attestation/slsa_provenance_02_test.go +++ b/internal/attestation/slsa_provenance_02_test.go @@ -22,7 +22,6 @@ import ( "bytes" "crypto/x509" "encoding/base64" - "encoding/json" "errors" "fmt" "io" @@ -233,18 +232,53 @@ func TestSLSAProvenanceFromSignature(t *testing.T) { }, err: errors.New("unsupported attestation predicate type: kaboom"), }, + { + name: "schema validation fails - missing subject", + setup: func(l *mockSignature) { + payload := encode(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"} + }`) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil) + l.On("Base64Signature").Return("", nil) + l.On("Cert").Return(&x509.Certificate{}, nil) + l.On("Chain").Return([]*x509.Certificate{}, nil) + }, + err: errors.New("attestation does not conform to SLSA v0.2 schema: jsonschema: '' does not validate with https://slsa.dev/provenance/v0.2#/required: missing properties: 'subject'"), + }, + { + name: "schema validation fails - missing builder", + setup: func(l *mockSignature) { + payload := encode(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": {"buildType": "https://my.build.type"} + }`) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil) + l.On("Base64Signature").Return("", nil) + l.On("Cert").Return(&x509.Certificate{}, nil) + l.On("Chain").Return([]*x509.Certificate{}, nil) + }, + err: errors.New("attestation does not conform to SLSA v0.2 schema: jsonschema: '/predicate' does not validate with https://slsa.dev/provenance/v0.2#/properties/predicate/required: missing properties: 'builder'"), + }, { name: "cannot create entity signature", data: `{ "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], "predicateType": "https://slsa.dev/provenance/v0.2", - "predicate": {"buildType": "https://my.build.type"} + "predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"} }`, setup: func(l *mockSignature) { payload := encode(`{ "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], "predicateType": "https://slsa.dev/provenance/v0.2", - "predicate": {"buildType":"https://my.build.type"} + "predicate": {"builder": {"id": "https://my.builder"}, "buildType":"https://my.build.type"} }`) l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil) @@ -256,16 +290,18 @@ func TestSLSAProvenanceFromSignature(t *testing.T) { name: "valid with signature from payload", data: `{ "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], "predicateType": "https://slsa.dev/provenance/v0.2", - "predicate": {"buildType": "https://my.build.type"} + "predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"} }`, setup: func(l *mockSignature) { sig1 := `{"keyid": "key-id-1", "sig": "sig-1"}` sig2 := `{"keyid": "key-id-2", "sig": "sig-2"}` payload := encode(`{ "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], "predicateType": "https://slsa.dev/provenance/v0.2", - "predicate": {"buildType": "https://my.build.type"} + "predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"} }`) l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) l.On("Uncompressed").Return(buffy( @@ -280,16 +316,18 @@ func TestSLSAProvenanceFromSignature(t *testing.T) { name: "valid with signature from certificate", data: `{ "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], "predicateType": "https://slsa.dev/provenance/v0.2", - "predicate": {"buildType": "https://my.build.type"} + "predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"} }`, setup: func(l *mockSignature) { sig1 := `{"keyid": "ignored-1", "sig": "ignored-1"}` sig2 := `{"keyid": "ignored-2", "sig": "ignored-2"}` payload := encode(`{ "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], "predicateType": "https://slsa.dev/provenance/v0.2", - "predicate": {"buildType": "https://my.build.type"} + "predicate": {"builder": {"id": "https://my.builder"}, "buildType": "https://my.build.type"} }`) l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) l.On("Uncompressed").Return(buffy( @@ -330,35 +368,6 @@ func TestSLSAProvenanceFromSignature(t *testing.T) { } } -func TestMarshal(t *testing.T) { - sig := mockSignature{&mock.Mock{}} - - sig1 := `{"keyid": "ignored-1", "sig": "ignored-1"}` - sig2 := `{"keyid": "ignored-2", "sig": "ignored-2"}` - payload := encode(`{ - "_type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicate": {"buildType": "https://my.build.type"} - }`) - sig.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) - sig.On("Uncompressed").Return(buffy( - fmt.Sprintf(`{"payload": "%s", "signatures": [%s, %s]}`, payload, sig1, sig2), - ), nil) - sig.On("Base64Signature").Return("sig-from-cert", nil) - sig.On("Cert").Return(signature.ParseChainguardReleaseCert(), nil) - sig.On("Chain").Return(signature.ParseSigstoreChainCert(), nil) - - att, err := SLSAProvenanceFromSignature(sig) - - require.NoError(t, err) - - j, err := json.Marshal(att) - - require.NoError(t, err) - - snaps.MatchJSON(t, j) -} - func encode(payload string) string { return base64.StdEncoding.EncodeToString([]byte(payload)) } diff --git a/internal/attestation/slsa_provenance_v1.go b/internal/attestation/slsa_provenance_v1.go new file mode 100644 index 000000000..39c20b7e5 --- /dev/null +++ b/internal/attestation/slsa_provenance_v1.go @@ -0,0 +1,108 @@ +// Copyright The Conforma Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package attestation + +import ( + "encoding/json" + "fmt" + + "github.com/in-toto/in-toto-golang/in_toto" + v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/sigstore/cosign/v2/pkg/oci" + + "github.com/conforma/cli/internal/signature" + "github.com/conforma/cli/pkg/schema" +) + +const ( + // Make it visible elsewhere + PredicateSLSAProvenanceV1 = v1.PredicateSLSAProvenance +) + +// SLSAProvenanceFromSignatureV1 parses the SLSA Provenance v1 from the provided OCI +// layer. Expects that the layer contains DSSE JSON with the embedded SLSA +// Provenance v1 payload. +func SLSAProvenanceFromSignatureV1(sig oci.Signature) (Attestation, error) { + payload, err := payloadFromSig(sig) + if err != nil { + return nil, err + } + + embedded, err := decodedPayload(payload) + if err != nil { + return nil, err + } + + var statement in_toto.ProvenanceStatementSLSA1 + if err := json.Unmarshal(embedded, &statement); err != nil { + return nil, fmt.Errorf("malformed attestation data: %w", err) + } + + if statement.Type != in_toto.StatementInTotoV01 { + return nil, fmt.Errorf("unsupported attestation type: %s", statement.Type) + } + + if statement.PredicateType != v1.PredicateSLSAProvenance { + return nil, fmt.Errorf("unsupported attestation predicate type: %s", statement.PredicateType) + } + + signatures, err := createEntitySignatures(sig, payload) + if err != nil { + return nil, fmt.Errorf("cannot create signed entity: %w", err) + } + + // Validate against SLSA v1 schema + var schemaValidation any + if err := json.Unmarshal(embedded, &schemaValidation); err == nil { + if err := schema.SLSA_Provenance_v1.Validate(schemaValidation); err != nil { + return nil, fmt.Errorf("attestation does not conform to SLSA v1.0 schema: %w", err) + } + } + + return slsaProvenanceV1{statement: statement, data: embedded, signatures: signatures}, nil +} + +type slsaProvenanceV1 struct { + statement in_toto.ProvenanceStatementSLSA1 + data []byte + signatures []signature.EntitySignature +} + +func (a slsaProvenanceV1) Type() string { + return in_toto.StatementInTotoV01 +} + +func (a slsaProvenanceV1) PredicateType() string { + return v1.PredicateSLSAProvenance +} + +// This returns the raw json, not the content of a.statement +func (a slsaProvenanceV1) Statement() []byte { + return a.data +} + +func (a slsaProvenanceV1) PredicateBuildType() string { + return a.statement.Predicate.BuildDefinition.BuildType +} + +func (a slsaProvenanceV1) Signatures() []signature.EntitySignature { + return a.signatures +} + +func (a slsaProvenanceV1) Subject() []in_toto.Subject { + return a.statement.Subject +} diff --git a/internal/attestation/slsa_provenance_v1_test.go b/internal/attestation/slsa_provenance_v1_test.go new file mode 100644 index 000000000..3e9a4b8d7 --- /dev/null +++ b/internal/attestation/slsa_provenance_v1_test.go @@ -0,0 +1,470 @@ +// Copyright The Conforma Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build unit + +package attestation + +import ( + "errors" + "fmt" + "testing" + + "github.com/gkampitakis/go-snaps/snaps" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/in-toto/in-toto-golang/in_toto" + v1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + ct "github.com/sigstore/cosign/v2/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/conforma/cli/internal/signature" +) + +func TestSLSAProvenanceFromSignatureV1NilSignature(t *testing.T) { + sp, err := SLSAProvenanceFromSignatureV1(nil) + assert.True(t, assert.ErrorContains(t, err, "no attestation found"), "Expecting `%v` to be alike: `%v`", err, "no attestation found") + assert.Nil(t, sp) +} + +func TestSLSAProvenanceFromSignatureV1(t *testing.T) { + cases := []struct { + name string + setup func(l *mockSignature) + data string + err error + }{ + { + name: "media type error", + setup: func(l *mockSignature) { + l.On("MediaType").Return(types.MediaType(""), errors.New("expected")) + }, + err: errors.New("malformed attestation data: expected"), + }, + { + name: "no media type", + setup: func(l *mockSignature) { + l.On("MediaType").Return(types.MediaType(""), nil) + }, + err: errors.New("malformed attestation data: expecting media type of `application/vnd.dsse.envelope.v1+json`, received: ``"), + }, + { + name: "unsupported media type", + setup: func(l *mockSignature) { + l.On("MediaType").Return(types.MediaType("xxx"), nil) + }, + err: errors.New("malformed attestation data: expecting media type of `application/vnd.dsse.envelope.v1+json`, received: `xxx`"), + }, + { + name: "no payload JSON", + setup: func(l *mockSignature) { + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy(""), nil) + }, + err: errors.New("malformed attestation data: EOF"), + }, + { + name: "empty payload JSON", + data: "{}", + setup: func(l *mockSignature) { + payload := encode("{}") + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil) + }, + err: errors.New("unsupported attestation type: "), + }, + { + name: "invalid attestation payload JSON", + setup: func(l *mockSignature) { + sig1 := `{"keyid": "key-id-1", "sig": "sig-1"}` + payload := fmt.Sprintf(`{{"signatures": [%s]}`, sig1) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy(payload), nil) + }, + err: errors.New("malformed attestation data: invalid character '{' looking for beginning of object key string"), + }, + { + name: "invalid statement JSON base64", + setup: func(l *mockSignature) { + sig1 := `{"keyid": "key-id-1", "sig": "sig-1"}` + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy( + fmt.Sprintf(`{"signatures": [%s], "payload": "not-base64"}`, sig1), + ), nil) + }, + err: errors.New("malformed attestation data: illegal base64 data at input byte 3"), + }, + { + name: "invalid statement JSON", + setup: func(l *mockSignature) { + sig1 := `{"keyid": "key-id-1", "sig": "sig-1"}` + payload := encode(`{{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType":"https://slsa.dev/provenance/v1", + "predicate":{"buildDefinition":{"buildType":"https://my.build.type","externalParameters":{}},"runDetails":{"builder":{"id":"https://my.builder"}}} } + }`) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy( + fmt.Sprintf(`{"signatures": [%s], "payload": "%s"}`, sig1, payload), + ), nil) + }, + err: errors.New("malformed attestation data: invalid character '{' looking for beginning of object key string"), + }, + { + name: "unexpected predicate type", + setup: func(l *mockSignature) { + sig1 := `{"keyid": "key-id-1", "sig": "sig-1"}` + payload := encode(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType":"kaboom" + }`) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy( + fmt.Sprintf(`{"signatures": [%s], "payload": "%s"}`, sig1, payload), + ), nil) + }, + err: errors.New("unsupported attestation predicate type: kaboom"), + }, + { + name: "schema validation fails - missing subject", + setup: func(l *mockSignature) { + payload := encode(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://my.build.type", + "externalParameters": {} + }, + "runDetails": { + "builder": {"id": "https://my.builder"} + } + } + }`) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil) + l.On("Base64Signature").Return("", nil) + l.On("Cert").Return(signature.ParseChainguardReleaseCert(), nil) + l.On("Chain").Return(signature.ParseSigstoreChainCert(), nil) + }, + err: errors.New("attestation does not conform to SLSA v1.0 schema: jsonschema: '' does not validate with https://slsa.dev/provenance/v1#/required: missing properties: 'subject'"), + }, + { + name: "schema validation fails - missing buildDefinition", + setup: func(l *mockSignature) { + payload := encode(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "runDetails": { + "builder": {"id": "https://my.builder"} + } + } + }`) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil) + l.On("Base64Signature").Return("", nil) + l.On("Cert").Return(signature.ParseChainguardReleaseCert(), nil) + l.On("Chain").Return(signature.ParseSigstoreChainCert(), nil) + }, + err: errors.New("attestation does not conform to SLSA v1.0 schema: jsonschema: '/predicate' does not validate with https://slsa.dev/provenance/v1#/properties/predicate/required: missing properties: 'buildDefinition'"), + }, + { + name: "cannot create entity signature", + data: `{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://my.build.type", + "externalParameters": {} + }, + "runDetails": { + "builder": {"id": "https://my.builder"} + } + } + }`, + setup: func(l *mockSignature) { + payload := encode(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://my.build.type", + "externalParameters": {} + }, + "runDetails": { + "builder": {"id": "https://my.builder"} + } + } + }`) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy(fmt.Sprintf(`{"payload":"%s"}`, payload)), nil) + l.On("Base64Signature").Return("", errors.New("kaboom")) + }, + err: fmt.Errorf("cannot create signed entity: %s", "kaboom"), + }, + { + name: "valid with signature from payload", + data: `{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://my.build.type", + "externalParameters": {} + }, + "runDetails": { + "builder": {"id": "https://my.builder"} + } + } + }`, + setup: func(l *mockSignature) { + sig1 := `{"keyid": "key-id-1", "sig": "sig-1"}` + sig2 := `{"keyid": "key-id-2", "sig": "sig-2"}` + payload := encode(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [{"name": "example.com/test", "digest": {"sha256": "abc123"}}], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://my.build.type", + "externalParameters": {} + }, + "runDetails": { + "builder": {"id": "https://my.builder"} + } + } + }`) + l.On("MediaType").Return(types.MediaType(ct.DssePayloadType), nil) + l.On("Uncompressed").Return(buffy( + fmt.Sprintf(`{"payload": "%s", "signatures": [%s, %s]}`, payload, sig1, sig2), + ), nil) + l.On("Base64Signature").Return("", nil) + l.On("Cert").Return(signature.ParseChainguardReleaseCert(), nil) + l.On("Chain").Return(signature.ParseSigstoreChainCert(), nil) + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + sig := mockSignature{&mock.Mock{}} + + if c.setup != nil { + c.setup(&sig) + } + + sp, err := SLSAProvenanceFromSignatureV1(sig) + if c.err == nil { + require.Nil(t, err) + require.NotNil(t, sp) + } else { + require.Nil(t, sp) + assert.True(t, c.err.Error() == err.Error(), "Expecting `%v` to be alike: `%v`", err, c.err) + return + } + + if c.data == "" { + assert.Nil(t, sp.Statement()) + } else { + assert.JSONEq(t, c.data, string(sp.Statement())) + } + snaps.MatchSnapshot(t, sp.Type(), sp.Signatures()) + }) + } +} + +func TestSLSAProvenanceV1_Subject(t *testing.T) { + mockSubject1 := in_toto.Subject{ + Name: "registry.io/example/image@sha256:abc123", + Digest: map[string]string{ + "sha256": "abc123def456", + }, + } + mockSubject2 := in_toto.Subject{ + Name: "registry.io/example/artifact@sha256:def456", + Digest: map[string]string{ + "sha256": "def456abc123", + "sha512": "fea789bcd012", + }, + } + + tests := []struct { + name string + statement in_toto.ProvenanceStatementSLSA1 + expected []in_toto.Subject + wantPanic bool + }{ + { + name: "returns single subject successfully", + statement: in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Subject: []in_toto.Subject{mockSubject1}, + }, + }, + expected: []in_toto.Subject{mockSubject1}, + }, + { + name: "returns multiple subjects successfully", + statement: in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Subject: []in_toto.Subject{mockSubject1, mockSubject2}, + }, + }, + expected: []in_toto.Subject{mockSubject1, mockSubject2}, + }, + { + name: "returns empty slice when no subjects", + statement: in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Subject: []in_toto.Subject{}, + }, + }, + expected: []in_toto.Subject{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic but none occurred") + } + }() + } + + slsa := slsaProvenanceV1{statement: tt.statement} + result := slsa.Subject() + + if !tt.wantPanic { + assert.Equal(t, tt.expected, result) + // Verify that the returned slice is independent of the original + if len(result) > 0 && len(tt.expected) > 0 { + assert.Equal(t, tt.expected[0].Name, result[0].Name) + assert.Equal(t, tt.expected[0].Digest, result[0].Digest) + } + } + }) + } +} + +func TestSLSAProvenanceV1_Type(t *testing.T) { + slsa := slsaProvenanceV1{ + statement: in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: PredicateSLSAProvenanceV1, + }, + }, + } + + result := slsa.Type() + assert.Equal(t, in_toto.StatementInTotoV01, result) +} + +func TestSLSAProvenanceV1_PredicateType(t *testing.T) { + slsa := slsaProvenanceV1{ + statement: in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: PredicateSLSAProvenanceV1, + }, + }, + } + + result := slsa.PredicateType() + assert.Equal(t, v1.PredicateSLSAProvenance, result) +} + +func TestSLSAProvenanceV1_PredicateBuildType(t *testing.T) { + tests := []struct { + name string + buildType string + }{ + { + name: "returns buildType from buildDefinition", + buildType: "https://tekton.dev/chains/v2/slsa-tekton", + }, + { + name: "returns empty buildType", + buildType: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + slsa := slsaProvenanceV1{ + statement: in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: PredicateSLSAProvenanceV1, + }, + Predicate: v1.ProvenancePredicate{ + BuildDefinition: v1.ProvenanceBuildDefinition{ + BuildType: tt.buildType, + ExternalParameters: map[string]interface{}{}, + }, + RunDetails: v1.ProvenanceRunDetails{ + Builder: v1.Builder{ + ID: "https://my.builder", + }, + }, + }, + }, + } + + result := slsa.PredicateBuildType() + assert.Equal(t, tt.buildType, result) + }) + } +} + +func TestSLSAProvenanceV1_Statement(t *testing.T) { + expectedData := []byte(`{"test":"data"}`) + slsa := slsaProvenanceV1{ + data: expectedData, + } + + result := slsa.Statement() + assert.Equal(t, expectedData, result) +} + +func TestSLSAProvenanceV1_Signatures(t *testing.T) { + expectedSigs := []signature.EntitySignature{ + { + Signature: "sig1", + KeyID: "key1", + }, + { + Signature: "sig2", + KeyID: "key2", + }, + } + + slsa := slsaProvenanceV1{ + signatures: expectedSigs, + } + + result := slsa.Signatures() + assert.Equal(t, expectedSigs, result) +} diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go index 90601e830..2178706bf 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go @@ -46,6 +46,7 @@ import ( var attestationSchemas = map[string]*jsonschema.Schema{ "https://slsa.dev/provenance/v0.2": schema.SLSA_Provenance_v0_2, + "https://slsa.dev/provenance/v1": schema.SLSA_Provenance_v1, } // ApplicationSnapshotImage represents the structure needed to evaluate an Application Snapshot Image @@ -192,6 +193,14 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont } a.attestations = append(a.attestations, sp) + case attestation.PredicateSLSAProvenanceV1: + // SLSA Provenance v1.0 + sp, err := attestation.SLSAProvenanceFromSignatureV1(sig) + if err != nil { + return fmt.Errorf("unable to parse as SLSA v1: %w", err) + } + a.attestations = append(a.attestations, sp) + case attestation.PredicateSpdxDocument: // It's an SPDX format SBOM // Todo maybe: We could unmarshal it into a suitable SPDX struct diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go index 504ae038d..1f44fb159 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image_test.go @@ -37,6 +37,7 @@ import ( "github.com/in-toto/in-toto-golang/in_toto" "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + slsav1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" app "github.com/konflux-ci/application-api/api/v1alpha1" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/v2/pkg/cosign" @@ -756,3 +757,366 @@ kind: ClusterServiceVersion`), "manifests/csv.yaml": json.RawMessage(`{"apiVersion":"operators.coreos.com/v1alpha1","kind":"ClusterServiceVersion"}`), }, a.files) } + +func TestValidateImageAccess(t *testing.T) { + ref := name.MustParseReference("registry.io/repository/image:tag") + + cases := []struct { + name string + response any + err error + expectErr bool + errMsg string + }{ + { + name: "successful access", + response: &v1.Descriptor{}, + expectErr: false, + }, + { + name: "access error", + err: errors.New("failed to access image"), + expectErr: true, + errMsg: "failed to access image", + }, + { + name: "nil response", + response: nil, + expectErr: true, + errMsg: "no response received", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + a := ApplicationSnapshotImage{reference: ref} + + client := fake.FakeClient{} + client.On("Head", ref).Return(tc.response, tc.err) + + ctx := o.WithClient(context.Background(), &client) + + err := a.ValidateImageAccess(ctx) + if tc.expectErr { + assert.Error(t, err) + if tc.errMsg != "" { + assert.Equal(t, tc.errMsg, err.Error()) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAttestationDataMarshalJSON(t *testing.T) { + cases := []struct { + name string + statement json.RawMessage + signatures []signature.EntitySignature + expected string + }{ + { + name: "with signatures", + statement: json.RawMessage(`{"type":"test"}`), + signatures: []signature.EntitySignature{ + {KeyID: "key1", Signature: "sig1"}, + }, + expected: `{"statement":{"type":"test"},"signatures":[{"keyid":"key1","sig":"sig1"}]}`, + }, + { + name: "without signatures", + statement: json.RawMessage(`{"type":"test"}`), + signatures: []signature.EntitySignature{}, + expected: `{"statement":{"type":"test"}}`, + }, + { + name: "nil signatures", + statement: json.RawMessage(`{"type":"test"}`), + signatures: nil, + expected: `{"statement":{"type":"test"}}`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + data := attestationData{ + Statement: tc.statement, + Signatures: tc.signatures, + } + result, err := data.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, tc.expected, string(result)) + }) + } +} + +// createDSSESignature creates a test signature with a DSSE envelope containing the given statement +func createDSSESignature(t *testing.T, statement any) oci.Signature { + t.Helper() + + statementJSON, err := json.Marshal(statement) + require.NoError(t, err) + + encodedStatement := base64.StdEncoding.EncodeToString(statementJSON) + + dsseEnvelope := dsse.Envelope{ + Payload: encodedStatement, + PayloadType: "application/vnd.in-toto+json", + } + + payload, err := json.Marshal(dsseEnvelope) + require.NoError(t, err) + + sig, err := static.NewSignature( + payload, + "test-signature", + static.WithLayerMediaType(types.MediaType(cosignTypes.DssePayloadType)), + static.WithCertChain( + signature.ChainguardReleaseCert, + signature.SigstoreChainCert, + ), + ) + require.NoError(t, err) + + return sig +} + +func TestValidateAttestationSignature(t *testing.T) { + ref := name.MustParseReference("registry.io/repository/image:tag") + + // Create valid SLSA v0.2 statement + slsaV02Statement := in_toto.ProvenanceStatementSLSA02{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: v02.PredicateSLSAProvenance, + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{ + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + }, + }, + }, + }, + Predicate: v02.ProvenancePredicate{ + BuildType: pipelineRunBuildType, + Builder: common.ProvenanceBuilder{ + ID: "https://tekton.dev/chains/v2", + }, + }, + } + + // Create valid SLSA v1.0 statement + slsaV1Statement := in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: "https://slsa.dev/provenance/v1", + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{ + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + }, + }, + }, + }, + Predicate: slsav1.ProvenancePredicate{ + BuildDefinition: slsav1.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/attestations/chains/pipelinerun@v2", + ExternalParameters: json.RawMessage(`{}`), + }, + RunDetails: slsav1.ProvenanceRunDetails{ + Builder: slsav1.Builder{ + ID: "https://tekton.dev/chains/v2", + }, + }, + }, + } + + // Create valid SPDX statement + spdxStatement := in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: "https://spdx.dev/Document", + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{ + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + }, + }, + }, + }, + Predicate: json.RawMessage(`{"spdxVersion":"SPDX-2.3"}`), + } + + // Create statement with unknown predicate type + unknownStatement := in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: "https://example.com/unknown/v1", + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{ + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + }, + }, + }, + }, + Predicate: json.RawMessage(`{"custom":"data"}`), + } + + // Create invalid SLSA v0.2 statement (missing builder ID) + invalidSLSAV02Statement := in_toto.ProvenanceStatementSLSA02{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: v02.PredicateSLSAProvenance, + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{ + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + }, + }, + }, + }, + Predicate: v02.ProvenancePredicate{ + BuildType: pipelineRunBuildType, + Builder: common.ProvenanceBuilder{ + ID: "invalid-not-a-uri", + }, + }, + } + + // Create invalid SLSA v1.0 statement (missing required fields) + invalidSLSAV1Statement := in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: "https://slsa.dev/provenance/v1", + Subject: []in_toto.Subject{ + { + Name: "test-image", + Digest: common.DigestSet{ + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + }, + }, + }, + }, + Predicate: slsav1.ProvenancePredicate{ + BuildDefinition: slsav1.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/attestations/chains/pipelinerun@v2", + ExternalParameters: json.RawMessage(`{}`), + }, + RunDetails: slsav1.ProvenanceRunDetails{ + Builder: slsav1.Builder{ + ID: "invalid-not-a-uri", + }, + }, + }, + } + + cases := []struct { + name string + signatures []oci.Signature + verifyErr error + expectErr bool + errContains string + expectedAttCount int + expectedPredTypes []string + }{ + { + name: "no attestations", + signatures: []oci.Signature{}, + expectedAttCount: 0, + }, + { + name: "single SLSA v0.2 attestation", + signatures: []oci.Signature{createDSSESignature(t, slsaV02Statement)}, + expectedAttCount: 1, + expectedPredTypes: []string{v02.PredicateSLSAProvenance}, + }, + { + name: "single SLSA v1.0 attestation", + signatures: []oci.Signature{createDSSESignature(t, slsaV1Statement)}, + expectedAttCount: 1, + expectedPredTypes: []string{"https://slsa.dev/provenance/v1"}, + }, + { + name: "single SPDX attestation", + signatures: []oci.Signature{createDSSESignature(t, spdxStatement)}, + expectedAttCount: 1, + expectedPredTypes: []string{"https://spdx.dev/Document"}, + }, + { + name: "single unknown attestation", + signatures: []oci.Signature{createDSSESignature(t, unknownStatement)}, + expectedAttCount: 1, + expectedPredTypes: []string{"https://example.com/unknown/v1"}, + }, + { + name: "multiple mixed attestations", + signatures: []oci.Signature{ + createDSSESignature(t, slsaV02Statement), + createDSSESignature(t, slsaV1Statement), + createDSSESignature(t, spdxStatement), + createDSSESignature(t, unknownStatement), + }, + expectedAttCount: 4, + expectedPredTypes: []string{ + v02.PredicateSLSAProvenance, + "https://slsa.dev/provenance/v1", + "https://spdx.dev/Document", + "https://example.com/unknown/v1", + }, + }, + { + name: "verify attestations error", + verifyErr: errors.New("failed to verify attestations"), + expectErr: true, + errContains: "failed to verify attestations", + }, + { + name: "invalid SLSA v0.2 fails schema validation", + signatures: []oci.Signature{createDSSESignature(t, invalidSLSAV02Statement)}, + expectErr: true, + errContains: "unable to parse as SLSA v0.2", + }, + { + name: "invalid SLSA v1.0 fails schema validation", + signatures: []oci.Signature{createDSSESignature(t, invalidSLSAV1Statement)}, + expectErr: true, + errContains: "unable to parse as SLSA v1", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + a := ApplicationSnapshotImage{reference: ref} + + client := fake.FakeClient{} + client.On("VerifyImageAttestations", ref, mock.Anything).Return(tc.signatures, false, tc.verifyErr) + + ctx := o.WithClient(context.Background(), &client) + + err := a.ValidateAttestationSignature(ctx) + + if tc.expectErr { + require.Error(t, err) + if tc.errContains != "" { + assert.Contains(t, err.Error(), tc.errContains) + } + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedAttCount, len(a.attestations)) + + if tc.expectedPredTypes != nil { + for i, expectedType := range tc.expectedPredTypes { + assert.Equal(t, expectedType, a.attestations[i].PredicateType()) + } + } + } + }) + } +} diff --git a/pkg/schema/__snapshots__/slsa_provenance_v1_test.snap b/pkg/schema/__snapshots__/slsa_provenance_v1_test.snap new file mode 100755 index 000000000..75ecc51dd --- /dev/null +++ b/pkg/schema/__snapshots__/slsa_provenance_v1_test.snap @@ -0,0 +1,380 @@ + +[TestV1TypeMustBeInToto/case_0 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#] [S#/required] missing properties: '_type' +--- + +[TestV1TypeMustBeInToto/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/_type] [S#/properties/_type/const] value must be "https://in-toto.io/Statement/v0.1" +--- + +[TestV1TypeMustBeInToto/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/_type] [S#/properties/_type/const] value must be "https://in-toto.io/Statement/v0.1" +--- + +[TestV1TypeMustBeInToto/case_3 - 1] +nil +--- + +[TestV1SubjectMustBeProvided/case_0 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#] [S#/required] missing properties: 'subject' +--- + +[TestV1SubjectMustBeProvided/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/subject] [S#/properties/subject/minItems] minimum 1 items required, but found 0 items +--- + +[TestV1SubjectMustBeProvided/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/subject/0] [S#/properties/subject/items/required] missing properties: 'name', 'digest' +--- + +[TestV1SubjectMustBeProvided/case_3 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/subject/0] [S#/properties/subject/items/required] missing properties: 'digest' + [I#/subject/0/name] [S#/properties/subject/items/properties/name/minLength] length must be >= 1, but got 0 +--- + +[TestV1SubjectMustBeProvided/case_4 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/subject/0/digest] [S#/properties/subject/items/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/subject/0/digest/foo] [S#/$defs/DigestSet/propertyNames/enum] value must be one of "sha256", "sha224", "sha384", "sha512", "sha512_224", "sha512_256", "sha3_224", "sha3_256", "sha3_384", "sha3_512", "shake128", "shake256", "blake2b", "blake2s", "ripemd160", "sm3", "gost", "sha1", "md5", "gitCommit" +--- + +[TestV1SubjectMustBeProvided/case_5 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/subject/0/digest] [S#/properties/subject/items/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/subject/0/digest/sha256] [S#/$defs/DigestSet/additionalProperties/pattern] does not match pattern '^[a-f0-9]+$' +--- + +[TestV1SubjectMustBeProvided/case_6 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/subject/0/digest] [S#/properties/subject/items/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/subject/0/digest/sha256] [S#/$defs/DigestSet/additionalProperties/pattern] does not match pattern '^[a-f0-9]+$' +--- + +[TestV1PredicateTypeMustBeSLSAProvenancev1/case_0 - 1] +nil +--- + +[TestV1PredicateTypeMustBeSLSAProvenancev1/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicateType] [S#/properties/predicateType/const] value must be "https://slsa.dev/provenance/v1" +--- + +[TestV1PredicateTypeMustBeSLSAProvenancev1/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicateType] [S#/properties/predicateType/const] value must be "https://slsa.dev/provenance/v1" +--- + +[TestV1PredicateTypeMustBeSLSAProvenancev1/case_3 - 1] +nil +--- + +[TestV1BuildDefinitionBuildType/case_0 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition] [S#/properties/predicate/properties/buildDefinition/required] missing properties: 'buildType' +--- + +[TestV1BuildDefinitionBuildType/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition/buildType] [S#/properties/predicate/properties/buildDefinition/properties/buildType/minLength] length must be >= 1, but got 0 +--- + +[TestV1BuildDefinitionBuildType/case_2 - 1] +nil +--- + +[TestV1BuildDefinitionBuildType/case_3 - 1] +nil +--- + +[TestV1BuildDefinitionExternalParameters/case_0 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition] [S#/properties/predicate/properties/buildDefinition/required] missing properties: 'externalParameters' +--- + +[TestV1BuildDefinitionExternalParameters/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition/externalParameters] [S#/properties/predicate/properties/buildDefinition/properties/externalParameters/type] expected object, but got number +--- + +[TestV1BuildDefinitionExternalParameters/case_2 - 1] +nil +--- + +[TestV1BuildDefinitionExternalParameters/case_3 - 1] +nil +--- + +[TestV1BuildDefinitionInternalParameters/case_0 - 1] +nil +--- + +[TestV1BuildDefinitionInternalParameters/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition/internalParameters] [S#/properties/predicate/properties/buildDefinition/properties/internalParameters/type] expected object, but got number +--- + +[TestV1BuildDefinitionInternalParameters/case_2 - 1] +nil +--- + +[TestV1BuildDefinitionInternalParameters/case_3 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependencies/case_0 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependencies/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition/resolvedDependencies] [S#/properties/predicate/properties/buildDefinition/properties/resolvedDependencies/type] expected array, but got number +--- + +[TestV1BuildDefinitionResolvedDependencies/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition/resolvedDependencies] [S#/properties/predicate/properties/buildDefinition/properties/resolvedDependencies/type] expected array, but got object +--- + +[TestV1BuildDefinitionResolvedDependencies/case_3 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependencies/case_4 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependenciesUri/case_0 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependenciesUri/case_1 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependenciesUri/case_2 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependenciesUri/case_3 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependenciesDigest/case_0 - 1] +nil +--- + +[TestV1BuildDefinitionResolvedDependenciesDigest/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition/resolvedDependencies/0] [S#/properties/predicate/properties/buildDefinition/properties/resolvedDependencies/items/$ref] doesn't validate with '/$defs/ResourceDescriptor' + [I#/predicate/buildDefinition/resolvedDependencies/0/digest] [S#/$defs/ResourceDescriptor/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/predicate/buildDefinition/resolvedDependencies/0/digest/foo] [S#/$defs/DigestSet/propertyNames/enum] value must be one of "sha256", "sha224", "sha384", "sha512", "sha512_224", "sha512_256", "sha3_224", "sha3_256", "sha3_384", "sha3_512", "shake128", "shake256", "blake2b", "blake2s", "ripemd160", "sm3", "gost", "sha1", "md5", "gitCommit" +--- + +[TestV1BuildDefinitionResolvedDependenciesDigest/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition/resolvedDependencies/0] [S#/properties/predicate/properties/buildDefinition/properties/resolvedDependencies/items/$ref] doesn't validate with '/$defs/ResourceDescriptor' + [I#/predicate/buildDefinition/resolvedDependencies/0/digest] [S#/$defs/ResourceDescriptor/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/predicate/buildDefinition/resolvedDependencies/0/digest/sha256] [S#/$defs/DigestSet/additionalProperties/pattern] does not match pattern '^[a-f0-9]+$' +--- + +[TestV1BuildDefinitionResolvedDependenciesDigest/case_3 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/buildDefinition/resolvedDependencies/0] [S#/properties/predicate/properties/buildDefinition/properties/resolvedDependencies/items/$ref] doesn't validate with '/$defs/ResourceDescriptor' + [I#/predicate/buildDefinition/resolvedDependencies/0/digest] [S#/$defs/ResourceDescriptor/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/predicate/buildDefinition/resolvedDependencies/0/digest/sha256] [S#/$defs/DigestSet/additionalProperties/pattern] does not match pattern '^[a-f0-9]+$' +--- + +[TestV1BuildDefinitionResolvedDependenciesDigest/case_4 - 1] +nil +--- + +[TestV1RunDetailsBuilder/case_0 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails] [S#/properties/predicate/properties/runDetails/required] missing properties: 'builder' +--- + +[TestV1RunDetailsBuilder/case_1 - 1] +nil +--- + +[TestV1RunDetailsBuilder/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/builder] [S#/properties/predicate/properties/runDetails/properties/builder/required] missing properties: 'id' +--- + +[TestV1RunDetailsBuilder/case_3 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/builder/id] [S#/properties/predicate/properties/runDetails/properties/builder/properties/id/format] '' is not valid 'uri' +--- + +[TestV1RunDetailsBuilder/case_4 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/builder/id] [S#/properties/predicate/properties/runDetails/properties/builder/properties/id/format] 'not_uri' is not valid 'uri' +--- + +[TestV1RunDetailsBuilder/case_5 - 1] +nil +--- + +[TestV1RunDetailsMetadata/case_0 - 1] +nil +--- + +[TestV1RunDetailsMetadata/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata] [S#/properties/predicate/properties/runDetails/properties/metadata/type] expected object, but got number +--- + +[TestV1RunDetailsMetadata/case_2 - 1] +nil +--- + +[TestV1RunDetailsMetadata/case_3 - 1] +nil +--- + +[TestV1RunDetailsMetadataInvocationId/case_0 - 1] +nil +--- + +[TestV1RunDetailsMetadataInvocationId/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata/invocationId] [S#/properties/predicate/properties/runDetails/properties/metadata/properties/invocationId/minLength] length must be >= 1, but got 0 +--- + +[TestV1RunDetailsMetadataInvocationId/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata/invocationId] [S#/properties/predicate/properties/runDetails/properties/metadata/properties/invocationId/type] expected string, but got number +--- + +[TestV1RunDetailsMetadataInvocationId/case_3 - 1] +nil +--- + +[TestV1RunDetailsMetadataStartedOn/case_0 - 1] +nil +--- + +[TestV1RunDetailsMetadataStartedOn/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata/startedOn] [S#/properties/predicate/properties/runDetails/properties/metadata/properties/startedOn/$ref] doesn't validate with '/$defs/Timestamp' + [I#/predicate/runDetails/metadata/startedOn] [S#/$defs/Timestamp/format] '' is not valid 'date-time' + [I#/predicate/runDetails/metadata/startedOn] [S#/$defs/Timestamp/pattern] does not match pattern 'Z$' +--- + +[TestV1RunDetailsMetadataStartedOn/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata/startedOn] [S#/properties/predicate/properties/runDetails/properties/metadata/properties/startedOn/$ref] doesn't validate with '/$defs/Timestamp' + [I#/predicate/runDetails/metadata/startedOn] [S#/$defs/Timestamp/type] expected string, but got number +--- + +[TestV1RunDetailsMetadataStartedOn/case_3 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata/startedOn] [S#/properties/predicate/properties/runDetails/properties/metadata/properties/startedOn/$ref] doesn't validate with '/$defs/Timestamp' + [I#/predicate/runDetails/metadata/startedOn] [S#/$defs/Timestamp/pattern] does not match pattern 'Z$' +--- + +[TestV1RunDetailsMetadataStartedOn/case_4 - 1] +nil +--- + +[TestV1RunDetailsMetadataFinishedOn/case_0 - 1] +nil +--- + +[TestV1RunDetailsMetadataFinishedOn/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata/finishedOn] [S#/properties/predicate/properties/runDetails/properties/metadata/properties/finishedOn/$ref] doesn't validate with '/$defs/Timestamp' + [I#/predicate/runDetails/metadata/finishedOn] [S#/$defs/Timestamp/format] '' is not valid 'date-time' + [I#/predicate/runDetails/metadata/finishedOn] [S#/$defs/Timestamp/pattern] does not match pattern 'Z$' +--- + +[TestV1RunDetailsMetadataFinishedOn/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata/finishedOn] [S#/properties/predicate/properties/runDetails/properties/metadata/properties/finishedOn/$ref] doesn't validate with '/$defs/Timestamp' + [I#/predicate/runDetails/metadata/finishedOn] [S#/$defs/Timestamp/type] expected string, but got number +--- + +[TestV1RunDetailsMetadataFinishedOn/case_3 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/metadata/finishedOn] [S#/properties/predicate/properties/runDetails/properties/metadata/properties/finishedOn/$ref] doesn't validate with '/$defs/Timestamp' + [I#/predicate/runDetails/metadata/finishedOn] [S#/$defs/Timestamp/pattern] does not match pattern 'Z$' +--- + +[TestV1RunDetailsMetadataFinishedOn/case_4 - 1] +nil +--- + +[TestV1RunDetailsByproducts/case_0 - 1] +nil +--- + +[TestV1RunDetailsByproducts/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/byproducts] [S#/properties/predicate/properties/runDetails/properties/byproducts/type] expected array, but got number +--- + +[TestV1RunDetailsByproducts/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/byproducts] [S#/properties/predicate/properties/runDetails/properties/byproducts/type] expected array, but got object +--- + +[TestV1RunDetailsByproducts/case_3 - 1] +nil +--- + +[TestV1RunDetailsByproducts/case_4 - 1] +nil +--- + +[TestV1RunDetailsByproductsUri/case_0 - 1] +nil +--- + +[TestV1RunDetailsByproductsUri/case_1 - 1] +nil +--- + +[TestV1RunDetailsByproductsUri/case_2 - 1] +nil +--- + +[TestV1RunDetailsByproductsUri/case_3 - 1] +nil +--- + +[TestV1RunDetailsByproductsDigest/case_0 - 1] +nil +--- + +[TestV1RunDetailsByproductsDigest/case_1 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/byproducts/0] [S#/properties/predicate/properties/runDetails/properties/byproducts/items/$ref] doesn't validate with '/$defs/ResourceDescriptor' + [I#/predicate/runDetails/byproducts/0/digest] [S#/$defs/ResourceDescriptor/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/predicate/runDetails/byproducts/0/digest/foo] [S#/$defs/DigestSet/propertyNames/enum] value must be one of "sha256", "sha224", "sha384", "sha512", "sha512_224", "sha512_256", "sha3_224", "sha3_256", "sha3_384", "sha3_512", "shake128", "shake256", "blake2b", "blake2s", "ripemd160", "sm3", "gost", "sha1", "md5", "gitCommit" +--- + +[TestV1RunDetailsByproductsDigest/case_2 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/byproducts/0] [S#/properties/predicate/properties/runDetails/properties/byproducts/items/$ref] doesn't validate with '/$defs/ResourceDescriptor' + [I#/predicate/runDetails/byproducts/0/digest] [S#/$defs/ResourceDescriptor/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/predicate/runDetails/byproducts/0/digest/sha256] [S#/$defs/DigestSet/additionalProperties/pattern] does not match pattern '^[a-f0-9]+$' +--- + +[TestV1RunDetailsByproductsDigest/case_3 - 1] +[I#] [S#] doesn't validate with https://slsa.dev/provenance/v1# + [I#/predicate/runDetails/byproducts/0] [S#/properties/predicate/properties/runDetails/properties/byproducts/items/$ref] doesn't validate with '/$defs/ResourceDescriptor' + [I#/predicate/runDetails/byproducts/0/digest] [S#/$defs/ResourceDescriptor/properties/digest/$ref] doesn't validate with '/$defs/DigestSet' + [I#/predicate/runDetails/byproducts/0/digest/sha256] [S#/$defs/DigestSet/additionalProperties/pattern] does not match pattern '^[a-f0-9]+$' +--- + +[TestV1RunDetailsByproductsDigest/case_4 - 1] +nil +--- diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 58adeb452..0f69544d2 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -26,10 +26,17 @@ import ( //go:embed slsa_provenance_v0.2.json var slsa_provenance_v0_2_json string +//go:embed slsa_provenance_v1.json +var slsa_provenance_v1_json string + var SLSA_Provenance_v0_2 *jsonschema.Schema +var SLSA_Provenance_v1 *jsonschema.Schema + var SLSA_Provenance_v0_2_URI = "https://slsa.dev/provenance/v0.2" +var SLSA_Provenance_v1_URI = "https://slsa.dev/provenance/v1" + func init() { compiler := jsonschema.NewCompiler() compiler.AssertFormat = true @@ -38,4 +45,9 @@ func init() { panic(err) } SLSA_Provenance_v0_2 = compiler.MustCompile(SLSA_Provenance_v0_2_URI) + + if err := compiler.AddResource(SLSA_Provenance_v1_URI, strings.NewReader(slsa_provenance_v1_json)); err != nil { + panic(err) + } + SLSA_Provenance_v1 = compiler.MustCompile(SLSA_Provenance_v1_URI) } diff --git a/pkg/schema/slsa_provenance_v1.json b/pkg/schema/slsa_provenance_v1.json new file mode 100644 index 000000000..6ee4c13b5 --- /dev/null +++ b/pkg/schema/slsa_provenance_v1.json @@ -0,0 +1,187 @@ +{ + "$id": "https://slsa.dev/provenance/v1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "DigestSet": { + "type": "object", + "propertyNames": { + "enum": [ + "sha256", + "sha224", + "sha384", + "sha512", + "sha512_224", + "sha512_256", + "sha3_224", + "sha3_256", + "sha3_384", + "sha3_512", + "shake128", + "shake256", + "blake2b", + "blake2s", + "ripemd160", + "sm3", + "gost", + "sha1", + "md5", + "gitCommit" + ] + }, + "additionalProperties": { + "type": "string", + "pattern": "^[a-f0-9]+$" + } + }, + "Timestamp": { + "type": "string", + "format": "date-time", + "pattern": "Z$" + }, + "ResourceDescriptor": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "digest": { + "$ref": "#/$defs/DigestSet" + }, + "name": { + "type": "string" + }, + "downloadLocation": { + "type": "string" + }, + "mediaType": { + "type": "string" + }, + "content": { + "type": "string", + "contentEncoding": "base64" + }, + "annotations": { + "type": "object" + } + } + } + }, + "type": "object", + "properties": { + "_type": { + "const": "https://in-toto.io/Statement/v0.1" + }, + "subject": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "digest": { + "$ref": "#/$defs/DigestSet" + } + }, + "required": [ + "name", + "digest" + ] + } + }, + "predicateType": { + "const": "https://slsa.dev/provenance/v1" + }, + "predicate": { + "type": "object", + "properties": { + "buildDefinition": { + "type": "object", + "properties": { + "buildType": { + "type": "string", + "minLength": 1 + }, + "externalParameters": { + "type": "object" + }, + "internalParameters": { + "type": "object" + }, + "resolvedDependencies": { + "type": "array", + "items": { + "$ref": "#/$defs/ResourceDescriptor" + } + } + }, + "required": [ + "buildType", + "externalParameters" + ] + }, + "runDetails": { + "type": "object", + "properties": { + "builder": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "builderDependencies": { + "type": "array", + "items": { + "$ref": "#/$defs/ResourceDescriptor" + } + }, + "version": { + "type": "object" + } + }, + "required": [ + "id" + ] + }, + "metadata": { + "type": "object", + "properties": { + "invocationId": { + "type": "string", + "minLength": 1 + }, + "startedOn": { + "$ref": "#/$defs/Timestamp" + }, + "finishedOn": { + "$ref": "#/$defs/Timestamp" + } + } + }, + "byproducts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResourceDescriptor" + } + } + }, + "required": [ + "builder" + ] + } + }, + "required": [ + "buildDefinition", + "runDetails" + ] + } + }, + "required": [ + "_type", + "subject", + "predicate" + ] +} diff --git a/pkg/schema/slsa_provenance_v1_test.go b/pkg/schema/slsa_provenance_v1_test.go new file mode 100644 index 000000000..e91a5dfd7 --- /dev/null +++ b/pkg/schema/slsa_provenance_v1_test.go @@ -0,0 +1,233 @@ +// Copyright The Conforma Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build unit + +package schema + +import ( + "encoding/json" + "fmt" + "testing" + + jsonpatch "github.com/evanphx/json-patch" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/assert" +) + +var validV1 = []byte(`{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + { + "name": "subject_name", + "digest": { + "sha512": "abcdef0123456789" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "uri:val", + "externalParameters": {} + }, + "runDetails": { + "builder": { + "id": "uri:val" + } + } + } +}`) + +func checkV1(t *testing.T, patches ...string) { + for i, patch := range patches { + t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) { + j, err := jsonpatch.MergePatch(validV1, []byte(patch)) + assert.NoError(t, err) + + var v any + err = json.Unmarshal(j, &v) + assert.NoError(t, err) + + err = SLSA_Provenance_v1.Validate(v) + snaps.MatchSnapshot(t, err) + }) + } +} + +func TestV1TypeMustBeInToto(t *testing.T) { + checkV1(t, + `{"_type": null}`, + `{"_type": ""}`, + `{"_type": "something else"}`, + `{"_type": "https://in-toto.io/Statement/v0.1"}`, + ) +} + +func TestV1SubjectMustBeProvided(t *testing.T) { + checkV1(t, + `{"subject": null}`, + `{"subject": []}`, + `{"subject": [{"name": null, "digest": null}]}`, + `{"subject": [{"name": "", "digest": null}]}`, + `{"subject": [{"name": "a", "digest": {"foo": "abcdef0123456789"}}]}`, + `{"subject": [{"name": "a", "digest": {"sha256": ""}}]}`, + `{"subject": [{"name": "a", "digest": {"sha256": "g%-A"}}]}`, + ) +} + +func TestV1PredicateTypeMustBeSLSAProvenancev1(t *testing.T) { + checkV1(t, + `{"predicateType": null}`, + `{"predicateType": ""}`, + `{"predicateType": "something else"}`, + `{"predicateType": "https://slsa.dev/provenance/v1"}`, + ) +} + +func TestV1BuildDefinitionBuildType(t *testing.T) { + checkV1(t, + `{"predicate": {"buildDefinition": {"buildType": null}}}`, + `{"predicate": {"buildDefinition": {"buildType": ""}}}`, + `{"predicate": {"buildDefinition": {"buildType": "not_uri"}}}`, + `{"predicate": {"buildDefinition": {"buildType": "scheme:authority"}}}`, + ) +} + +func TestV1BuildDefinitionExternalParameters(t *testing.T) { + checkV1(t, + `{"predicate": {"buildDefinition": {"externalParameters": null}}}`, + `{"predicate": {"buildDefinition": {"externalParameters": 1}}}`, + `{"predicate": {"buildDefinition": {"externalParameters": {}}}}`, + `{"predicate": {"buildDefinition": {"externalParameters": {"key": "value"}}}}`, + ) +} + +func TestV1BuildDefinitionInternalParameters(t *testing.T) { + checkV1(t, + `{"predicate": {"buildDefinition": {"internalParameters": null}}}`, + `{"predicate": {"buildDefinition": {"internalParameters": 1}}}`, + `{"predicate": {"buildDefinition": {"internalParameters": {}}}}`, + `{"predicate": {"buildDefinition": {"internalParameters": {"key": "value"}}}}`, + ) +} + +func TestV1BuildDefinitionResolvedDependencies(t *testing.T) { + checkV1(t, + `{"predicate": {"buildDefinition": {"resolvedDependencies": null}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": 1}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": {}}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": []}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{}, {}]}}}`, + ) +} + +func TestV1BuildDefinitionResolvedDependenciesUri(t *testing.T) { + checkV1(t, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"uri": null}]}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"uri": ""}]}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"uri": "not_uri"}]}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"uri": "scheme:authority"}]}}}`, + ) +} + +func TestV1BuildDefinitionResolvedDependenciesDigest(t *testing.T) { + checkV1(t, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"digest": null}]}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"digest": {"foo": "abcdef0123456789"}}]}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"digest": {"sha256": ""}}]}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"digest": {"sha256": "g%-A"}}]}}}`, + `{"predicate": {"buildDefinition": {"resolvedDependencies": [{"digest": {"sha256": "abcdef0123456789"}}]}}}`, + ) +} + +func TestV1RunDetailsBuilder(t *testing.T) { + checkV1(t, + `{"predicate": {"runDetails": {"builder": null}}}`, + `{"predicate": {"runDetails": {"builder": {}}}}`, + `{"predicate": {"runDetails": {"builder": {"id": null}}}}`, + `{"predicate": {"runDetails": {"builder": {"id": ""}}}}`, + `{"predicate": {"runDetails": {"builder": {"id": "not_uri"}}}}`, + `{"predicate": {"runDetails": {"builder": {"id": "scheme:authority"}}}}`, + ) +} + +func TestV1RunDetailsMetadata(t *testing.T) { + checkV1(t, + `{"predicate": {"runDetails": {"metadata": null}}}`, + `{"predicate": {"runDetails": {"metadata": 1}}}`, + `{"predicate": {"runDetails": {"metadata": {}}}}`, + `{"predicate": {"runDetails": {"metadata": {"invocationId": "abc"}}}}`, + ) +} + +func TestV1RunDetailsMetadataInvocationId(t *testing.T) { + checkV1(t, + `{"predicate": {"runDetails": {"metadata": {"invocationId": null}}}}`, + `{"predicate": {"runDetails": {"metadata": {"invocationId": ""}}}}`, + `{"predicate": {"runDetails": {"metadata": {"invocationId": 1}}}}`, + `{"predicate": {"runDetails": {"metadata": {"invocationId": "abc"}}}}`, + ) +} + +func TestV1RunDetailsMetadataStartedOn(t *testing.T) { + checkV1(t, + `{"predicate": {"runDetails": {"metadata": {"startedOn": null}}}}`, + `{"predicate": {"runDetails": {"metadata": {"startedOn": ""}}}}`, + `{"predicate": {"runDetails": {"metadata": {"startedOn": 1}}}}`, + `{"predicate": {"runDetails": {"metadata": {"startedOn": "1937-01-01T12:00:27.87+00:20"}}}}`, + `{"predicate": {"runDetails": {"metadata": {"startedOn": "1985-04-12T23:20:50.52Z"}}}}`, + ) +} + +func TestV1RunDetailsMetadataFinishedOn(t *testing.T) { + checkV1(t, + `{"predicate": {"runDetails": {"metadata": {"finishedOn": null}}}}`, + `{"predicate": {"runDetails": {"metadata": {"finishedOn": ""}}}}`, + `{"predicate": {"runDetails": {"metadata": {"finishedOn": 1}}}}`, + `{"predicate": {"runDetails": {"metadata": {"finishedOn": "1937-01-01T12:00:27.87+00:20"}}}}`, + `{"predicate": {"runDetails": {"metadata": {"finishedOn": "1985-04-12T23:20:50.52Z"}}}}`, + ) +} + +func TestV1RunDetailsByproducts(t *testing.T) { + checkV1(t, + `{"predicate": {"runDetails": {"byproducts": null}}}`, + `{"predicate": {"runDetails": {"byproducts": 1}}}`, + `{"predicate": {"runDetails": {"byproducts": {}}}}`, + `{"predicate": {"runDetails": {"byproducts": []}}}`, + `{"predicate": {"runDetails": {"byproducts": [{}, {}]}}}`, + ) +} + +func TestV1RunDetailsByproductsUri(t *testing.T) { + checkV1(t, + `{"predicate": {"runDetails": {"byproducts": [{"uri": null}]}}}`, + `{"predicate": {"runDetails": {"byproducts": [{"uri": ""}]}}}`, + `{"predicate": {"runDetails": {"byproducts": [{"uri": "not_uri"}]}}}`, + `{"predicate": {"runDetails": {"byproducts": [{"uri": "scheme:authority"}]}}}`, + ) +} + +func TestV1RunDetailsByproductsDigest(t *testing.T) { + checkV1(t, + `{"predicate": {"runDetails": {"byproducts": [{"digest": null}]}}}`, + `{"predicate": {"runDetails": {"byproducts": [{"digest": {"foo": "abcdef0123456789"}}]}}}`, + `{"predicate": {"runDetails": {"byproducts": [{"digest": {"sha256": ""}}]}}}`, + `{"predicate": {"runDetails": {"byproducts": [{"digest": {"sha256": "g%-A"}}]}}}`, + `{"predicate": {"runDetails": {"byproducts": [{"digest": {"sha256": "abcdef0123456789"}}]}}}`, + ) +}