Skip to content

Commit fd0368a

Browse files
authored
Conformance testing for cosign (#3806)
* Adding conformance helper and Action Also add e2e test and some helpful error messages about what flags go together Signed-off-by: Zach Steindler <[email protected]> * Allow conformance driver to call cosign with user-supplied args Signed-off-by: Zach Steindler <[email protected]> * fix e2e test Signed-off-by: Zach Steindler <[email protected]> * Detail TODO comments; remove unneeded trusted root in e2e tests Signed-off-by: Zach Steindler <[email protected]> --------- Signed-off-by: Zach Steindler <[email protected]>
1 parent 2387b50 commit fd0368a

File tree

7 files changed

+365
-1
lines changed

7 files changed

+365
-1
lines changed

.github/workflows/conformance.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2024 The Sigstore Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: Conformance Tests
16+
17+
on:
18+
push:
19+
branches:
20+
- main
21+
pull_request:
22+
branches:
23+
- main
24+
25+
permissions:
26+
contents: read
27+
28+
jobs:
29+
conformance:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
33+
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
34+
with:
35+
go-version: '1.22'
36+
check-latest: true
37+
38+
- run: make cosign conformance
39+
40+
- uses: sigstore/sigstore-conformance@ee4de0e602873beed74cf9e49d5332529fe69bf6 # v0.0.11
41+
with:
42+
entrypoint: ${{ github.workspace }}/conformance

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export KO_DOCKER_REPO=$(KO_PREFIX)
6565
GHCR_PREFIX ?= ghcr.io/sigstore/cosign
6666
LATEST_TAG ?=
6767

68-
.PHONY: all lint test clean cosign cross
68+
.PHONY: all lint test clean cosign conformance cross
6969
all: cosign
7070

7171
log-%:
@@ -106,6 +106,9 @@ lint: golangci-lint ## Run golangci-lint linter
106106
test:
107107
$(GOEXE) test $(shell $(GOEXE) list ./... | grep -v third_party/)
108108

109+
conformance:
110+
$(GOEXE) build -trimpath -ldflags "$(LDFLAGS)" -o $@ ./cmd/conformance
111+
109112
clean:
110113
rm -rf cosign
111114
rm -rf dist/

cmd/conformance/main.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Copyright 2024 The Sigstore Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"crypto/sha256"
19+
"encoding/base64"
20+
"encoding/pem"
21+
"fmt"
22+
"log"
23+
"os"
24+
"os/exec"
25+
"path/filepath"
26+
"strings"
27+
28+
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
29+
protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
30+
"github.com/sigstore/sigstore-go/pkg/bundle"
31+
"google.golang.org/protobuf/encoding/protojson"
32+
)
33+
34+
var bundlePath *string
35+
var certPath *string
36+
var certOIDC *string
37+
var certSAN *string
38+
var identityToken *string
39+
var signaturePath *string
40+
var trustedRootPath *string
41+
42+
func usage() {
43+
fmt.Println("Usage:")
44+
fmt.Printf("\t%s sign --identity-token TOKEN --signature FILE --certificate FILE FILE\n", os.Args[0])
45+
fmt.Printf("\t%s sign-bundle --identity-token TOKEN --bundle FILE FILE\n", os.Args[0])
46+
fmt.Printf("\t%s verify --signature FILE --certificate FILE --certificate-identity IDENTITY --certificate-oidc-issuer URL [--trusted-root FILE] FILE\n", os.Args[0])
47+
fmt.Printf("\t%s verify-bundle --bundle FILE --certificate-identity IDENTITY --certificate-oidc-issuer URL [--trusted-root FILE] FILE\n", os.Args[0])
48+
}
49+
50+
func parseArgs() {
51+
for i := 2; i < len(os.Args); {
52+
switch os.Args[i] {
53+
// TODO: support staging (see https://github.com/sigstore/cosign/issues/2434)
54+
//
55+
// Today cosign signing does not yet use sigstore-go, and so we would
56+
// need to make some clever invocation of `cosign initialize` to
57+
// support staging. Instead it might make sense to wait for cosign
58+
// signing to use sigstore-go.
59+
case "--bundle":
60+
bundlePath = &os.Args[i+1]
61+
i += 2
62+
case "--certificate":
63+
certPath = &os.Args[i+1]
64+
i += 2
65+
case "--certificate-oidc-issuer":
66+
certOIDC = &os.Args[i+1]
67+
i += 2
68+
case "--certificate-identity":
69+
certSAN = &os.Args[i+1]
70+
i += 2
71+
case "--identity-token":
72+
identityToken = &os.Args[i+1]
73+
i += 2
74+
case "--signature":
75+
signaturePath = &os.Args[i+1]
76+
i += 2
77+
case "--trusted-root":
78+
trustedRootPath = &os.Args[i+1]
79+
i += 2
80+
default:
81+
i++
82+
}
83+
}
84+
}
85+
86+
func main() {
87+
if len(os.Args) < 2 {
88+
usage()
89+
os.Exit(1)
90+
}
91+
92+
parseArgs()
93+
94+
args := []string{}
95+
96+
switch os.Args[1] {
97+
case "sign":
98+
args = append(args, "sign-blob")
99+
if signaturePath != nil {
100+
args = append(args, "--output-signature", *signaturePath)
101+
}
102+
if certPath != nil {
103+
args = append(args, "--output-certificate", *certPath)
104+
}
105+
args = append(args, "-y")
106+
107+
case "sign-bundle":
108+
args = append(args, "sign-blob")
109+
args = append(args, "-y")
110+
111+
case "verify":
112+
args = append(args, "verify-blob")
113+
114+
// TODO: for now, we handle `verify` by constructing a bundle
115+
// (see https://github.com/sigstore/cosign/issues/3700)
116+
//
117+
// Today cosign only supports `--trusted-root` with the new bundle
118+
// format. When cosign supports `--trusted-root` with detached signed
119+
// material, we can supply this content with `--certificate`
120+
// and `--signature` instead.
121+
fileBytes, err := os.ReadFile(os.Args[len(os.Args)-1])
122+
if err != nil {
123+
log.Fatal(err)
124+
}
125+
126+
fileDigest := sha256.Sum256(fileBytes)
127+
128+
pb := protobundle.Bundle{
129+
MediaType: "application/vnd.dev.sigstore.bundle+json;version=0.1",
130+
}
131+
132+
if signaturePath != nil {
133+
sig, err := os.ReadFile(*signaturePath)
134+
if err != nil {
135+
log.Fatal(err)
136+
}
137+
138+
sigBytes, err := base64.StdEncoding.DecodeString(string(sig))
139+
if err != nil {
140+
log.Fatal(err)
141+
}
142+
143+
pb.Content = &protobundle.Bundle_MessageSignature{
144+
MessageSignature: &protocommon.MessageSignature{
145+
MessageDigest: &protocommon.HashOutput{
146+
Algorithm: protocommon.HashAlgorithm_SHA2_256,
147+
Digest: fileDigest[:],
148+
},
149+
Signature: sigBytes,
150+
},
151+
}
152+
}
153+
if certPath != nil {
154+
cert, err := os.ReadFile(*certPath)
155+
if err != nil {
156+
log.Fatal(err)
157+
}
158+
159+
pemCert, _ := pem.Decode(cert)
160+
if pemCert == nil {
161+
log.Fatalf("unable to load cerficate from %s", *certPath)
162+
}
163+
164+
signingCert := protocommon.X509Certificate{
165+
RawBytes: pemCert.Bytes,
166+
}
167+
168+
pb.VerificationMaterial = &protobundle.VerificationMaterial{
169+
Content: &protobundle.VerificationMaterial_X509CertificateChain{
170+
X509CertificateChain: &protocommon.X509CertificateChain{
171+
Certificates: []*protocommon.X509Certificate{&signingCert},
172+
},
173+
},
174+
}
175+
}
176+
177+
bundleFile, err := os.CreateTemp(os.TempDir(), "bundle.sigstore.json")
178+
if err != nil {
179+
log.Fatal(err)
180+
}
181+
bundleFileName := bundleFile.Name()
182+
pbBytes, err := protojson.Marshal(&pb)
183+
if err != nil {
184+
log.Fatal(err)
185+
}
186+
if err := os.WriteFile(bundleFileName, pbBytes, 0600); err != nil {
187+
log.Fatal(err)
188+
}
189+
bundlePath = &bundleFileName
190+
args = append(args, "--insecure-ignore-tlog")
191+
192+
case "verify-bundle":
193+
args = append(args, "verify-blob")
194+
195+
// How do we know if we should expect signed timestamps or not?
196+
// Let's crack open the bundle
197+
if bundlePath != nil {
198+
b, err := bundle.LoadJSONFromPath(*bundlePath)
199+
if err != nil {
200+
log.Fatal(err)
201+
}
202+
ts, err := b.Timestamps()
203+
if err != nil {
204+
log.Fatal(err)
205+
}
206+
if len(ts) > 0 {
207+
args = append(args, "--use-signed-timestamps")
208+
}
209+
}
210+
211+
default:
212+
log.Fatalf("Unsupported command %s", os.Args[1])
213+
}
214+
215+
if bundlePath != nil {
216+
args = append(args, "--bundle", *bundlePath)
217+
args = append(args, "--new-bundle-format")
218+
}
219+
if identityToken != nil {
220+
args = append(args, "--identity-token", *identityToken)
221+
}
222+
if certSAN != nil {
223+
args = append(args, "--certificate-identity", *certSAN)
224+
}
225+
if certOIDC != nil {
226+
args = append(args, "--certificate-oidc-issuer", *certOIDC)
227+
}
228+
if trustedRootPath != nil {
229+
args = append(args, "--trusted-root", *trustedRootPath)
230+
}
231+
args = append(args, os.Args[len(os.Args)-1])
232+
233+
dir := filepath.Dir(os.Args[0])
234+
cmd := exec.Command(filepath.Join(dir, "cosign"), args...) // #nosec G204
235+
var out strings.Builder
236+
cmd.Stdout = &out
237+
cmd.Stderr = &out
238+
err := cmd.Run()
239+
240+
fmt.Println(out.String())
241+
242+
if err != nil {
243+
log.Fatal(err)
244+
}
245+
246+
if os.Args[1] == "sign" && certPath != nil {
247+
// We want the signature to be base64 encoded, but not the certificate
248+
// So base64 decode the certificate
249+
cert, err := os.ReadFile(*certPath)
250+
if err != nil {
251+
log.Fatal(err)
252+
}
253+
certB64Decode, err := base64.StdEncoding.DecodeString(string(cert))
254+
if err != nil {
255+
log.Fatal(err)
256+
}
257+
if err := os.WriteFile(*certPath, certB64Decode, 0600); err != nil {
258+
log.Fatal(err)
259+
}
260+
}
261+
}

cmd/cosign/cli/verify/verify_blob.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,16 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
9393
}
9494

9595
if c.KeyOpts.NewBundleFormat {
96+
if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.RekorURL, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SigRef, c.SCTRef) > 1 {
97+
return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root")
98+
}
9699
err := verifyNewBundle(ctx, c.BundlePath, c.TrustedRootPath, c.KeyRef, c.Slot, c.CertVerifyOptions.CertOidcIssuer, c.CertVerifyOptions.CertOidcIssuerRegexp, c.CertVerifyOptions.CertIdentity, c.CertVerifyOptions.CertIdentityRegexp, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef, blobRef, c.Sk, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT)
97100
if err == nil {
98101
ui.Infof(ctx, "Verified OK")
99102
}
100103
return err
104+
} else if c.TrustedRootPath != "" {
105+
return fmt.Errorf("--trusted-root only supported with --new-bundle-format")
101106
}
102107

103108
var cert *x509.Certificate

cmd/cosign/cli/verify/verify_blob_attestation.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,16 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st
9393
}
9494

9595
if c.KeyOpts.NewBundleFormat {
96+
if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.RekorURL, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SCTRef) > 1 {
97+
return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root")
98+
}
9699
err = verifyNewBundle(ctx, c.BundlePath, c.TrustedRootPath, c.KeyRef, c.Slot, c.CertVerifyOptions.CertOidcIssuer, c.CertVerifyOptions.CertOidcIssuerRegexp, c.CertVerifyOptions.CertIdentity, c.CertVerifyOptions.CertIdentityRegexp, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef, artifactPath, c.Sk, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT)
97100
if err == nil {
98101
fmt.Fprintln(os.Stderr, "Verified OK")
99102
}
100103
return err
104+
} else if c.TrustedRootPath != "" {
105+
return fmt.Errorf("--trusted-root only supported with --new-bundle-format")
101106
}
102107

103108
var identities []cosign.Identity

cmd/cosign/cli/verify/verify_blob_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,10 @@ func TestVerifyBlob(t *testing.T) {
619619
}
620620
if tt.newBundle {
621621
cmd.TrustedRootPath = writeTrustedRootFile(t, td, "{\"mediaType\":\"application/vnd.dev.sigstore.trustedroot+json;version=0.1\"}")
622+
cmd.KeyOpts.RekorURL = ""
623+
cmd.KeyOpts.RFC3161TimestampPath = ""
624+
cmd.KeyOpts.TSACertChainPath = ""
625+
cmd.CertChain = ""
622626
}
623627

624628
err := cmd.Exec(context.Background(), blobPath)

0 commit comments

Comments
 (0)