diff --git a/attestation/example_attestation_manifest_test.go b/attestation/example_attestation_manifest_test.go index 1c21697..1612f93 100644 --- a/attestation/example_attestation_manifest_test.go +++ b/attestation/example_attestation_manifest_test.go @@ -26,7 +26,7 @@ func ExampleManifest() { // configure signing options opts := &attestation.SigningOptions{ - SkipTL: true, // skip trust logging to a transparency log + TransparencyLog: nil, // set this to log to a transparency log } ref := "docker/image-signer-verifier:latest" diff --git a/attestation/referrers_test.go b/attestation/referrers_test.go index 60c2f7c..4432b55 100644 --- a/attestation/referrers_test.go +++ b/attestation/referrers_test.go @@ -88,9 +88,7 @@ func TestAttestationReferenceTypes(t *testing.T) { u, err := url.Parse(s.URL) require.NoError(t, err) - opts := &attestation.SigningOptions{ - SkipTL: true, - } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(test.UnsignedTestImage("..")) require.NoError(t, err) @@ -210,9 +208,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { refServerURL, err := url.Parse(refServer.URL) require.NoError(t, err) - opts := &attestation.SigningOptions{ - SkipTL: true, - } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(test.UnsignedTestImage("..")) require.NoError(t, err) @@ -236,9 +232,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { refServerURL, err := url.Parse(refServer.URL) require.NoError(t, err) - opts := &attestation.SigningOptions{ - SkipTL: true, - } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(test.UnsignedTestImage("..")) require.NoError(t, err) @@ -291,9 +285,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) { repoName := "repo" - opts := &attestation.SigningOptions{ - SkipTL: true, - } + opts := &attestation.SigningOptions{} attIdx, err := oci.IndexFromPath(test.UnsignedTestImage("..")) require.NoError(t, err) diff --git a/attestation/sign.go b/attestation/sign.go index adee225..55eea11 100644 --- a/attestation/sign.go +++ b/attestation/sign.go @@ -37,10 +37,10 @@ func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, o KeyID: keyID, Sig: base64Encoding.EncodeToString(sig), } - if !opts.SkipTL { - ext, err := logSignature(ctx, tlog.GetTL(ctx), &sig, &encPayload, signer) + if opts.TransparencyLog != nil { + ext, err := logSignature(ctx, opts.TransparencyLog, sig, encPayload, signer) if err != nil { - return nil, fmt.Errorf("failed to log to rekor: %w", err) + return nil, fmt.Errorf("failed to log signature: %w", err) } dsseSig.Extension = ext } @@ -51,27 +51,21 @@ func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, o } // returns a new envelope with the transparency log entry added to the signature extension. -func logSignature(ctx context.Context, t tlog.TL, sig *[]byte, encPayload *[]byte, signer dsse.SignerVerifier) (*Extension, error) { +func logSignature(ctx context.Context, t tlog.TransparencyLog, sig []byte, encPayload []byte, signer dsse.SignerVerifier) (*Extension, error) { // get Key ID from signer keyID, err := signer.KeyID() if err != nil { return nil, fmt.Errorf("error getting public key ID: %w", err) } - entry, err := t.UploadLogEntry(ctx, keyID, *encPayload, *sig, signer) + entry, err := t.UploadEntry(ctx, keyID, encPayload, sig, signer) if err != nil { return nil, fmt.Errorf("error uploading TL entry: %w", err) } - entryObj, err := t.UnmarshalEntry(entry) - if err != nil { - return nil, fmt.Errorf("error unmarshaling tl entry: %w", err) - } + return &Extension{ Kind: DockerDSSEExtKind, Ext: &DockerDSSEExtension{ - TL: &DockerTLExtension{ - Kind: RekorTLExtKind, - Data: entryObj, // transparency log entry metadata - }, + TL: entry, }, }, nil } diff --git a/attestation/sign_test.go b/attestation/sign_test.go index 110c596..90a686a 100644 --- a/attestation/sign_test.go +++ b/attestation/sign_test.go @@ -1,6 +1,7 @@ package attestation_test import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -14,6 +15,7 @@ import ( "github.com/docker/attest/internal/test" "github.com/docker/attest/oci" "github.com/docker/attest/signerverifier" + "github.com/docker/attest/tlog" "github.com/google/go-containerregistry/pkg/registry" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/static" @@ -35,7 +37,10 @@ func TestSignVerifyAttestation(t *testing.T) { payload, err := json.Marshal(stmt) require.NoError(t, err) - opts := &attestation.SigningOptions{} + tl := tlog.GetMockTL() + opts := &attestation.SigningOptions{ + TransparencyLog: tl, + } env, err := attestation.SignDSSE(ctx, payload, signer, opts) require.NoError(t, err) @@ -146,8 +151,17 @@ func TestSignVerifyAttestation(t *testing.T) { opts := &attestation.VerifyOptions{ Keys: attestation.Keys{keyMeta}, } - _, err = attestation.VerifyDSSE(ctx, deserializedEnv, opts) + getTL := func(_ context.Context, opts *attestation.VerifyOptions) (tlog.TransparencyLog, error) { + if opts.SkipTL { + return nil, nil + } + return tl, nil + } + verifier, err := attestation.NewVerfier(attestation.WithLogVerifierFactory(getTL)) + require.NoError(t, err) + _, err = attestation.VerifyDSSE(ctx, verifier, deserializedEnv, opts) if tc.expectedError != "" { + require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedError) } else { assert.NoError(t, err) @@ -222,7 +236,6 @@ func TestSimpleStatementSigning(t *testing.T) { {"replaced", true}, {"not replaced", false}, } - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { opts := &attestation.SigningOptions{} diff --git a/attestation/types.go b/attestation/types.go index 7d1b229..a912ca5 100644 --- a/attestation/types.go +++ b/attestation/types.go @@ -1,9 +1,12 @@ package attestation import ( + "crypto" "encoding/base64" "fmt" + "time" + "github.com/docker/attest/tlog" v1 "github.com/google/go-containerregistry/pkg/v1" intoto "github.com/in-toto/in-toto-golang/in_toto" v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" @@ -17,7 +20,6 @@ const ( InTotoPredicateType = "in-toto.io/predicate-type" DockerReferenceDigest = "vnd.docker.reference.digest" DockerDSSEExtKind = "application/vnd.docker.attestation-verification.v1+json" - RekorTLExtKind = "Rekor" OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse" InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage" LifecycleStageExperimental = "experimental" @@ -72,22 +74,40 @@ type AnnotatedStatement struct { } type DockerDSSEExtension struct { - TL *DockerTLExtension `json:"tl"` + TL *tlog.DockerTLExtension `json:"tl"` } -type DockerTLExtension struct { - Kind string `json:"kind"` - Data any `json:"data"` -} +type TransparencyLogKind string + +const ( + RekorTransparencyLogKind = "rekor" +) type VerifyOptions struct { - Keys []*KeyMetadata `json:"keys"` - SkipTL bool `json:"skip_tl"` + Keys []*KeyMetadata `json:"keys"` + SkipTL bool `json:"skip_tl"` + TransparencyLog TransparencyLogKind `json:"tl"` } +type KeyMetadata struct { + ID string `json:"id"` + PEM string `json:"key"` + From time.Time `json:"from"` + To *time.Time `json:"to"` + Status string `json:"status"` + SigningFormat string `json:"signing-format"` + Distrust bool `json:"distrust,omitempty"` + publicKey crypto.PublicKey +} + +type ( + Keys []*KeyMetadata + KeysMap map[string]*KeyMetadata +) + type SigningOptions struct { - // don't log to the configured transparency log - SkipTL bool + // set this in order to log to a transparency log + TransparencyLog tlog.TransparencyLog } type Options struct { diff --git a/attestation/verifier.go b/attestation/verifier.go new file mode 100644 index 0000000..482aefa --- /dev/null +++ b/attestation/verifier.go @@ -0,0 +1,143 @@ +package attestation + +import ( + "context" + "crypto" + "crypto/x509" + "fmt" + + "github.com/docker/attest/signerverifier" + "github.com/docker/attest/tlog" + "github.com/docker/attest/tuf" + "github.com/secure-systems-lab/go-securesystemslib/dsse" +) + +func WithTUFDownloader(tufDownloader tuf.Downloader) func(*verifier) { + return func(r *verifier) { + r.tufDownloader = tufDownloader + } +} + +type SignatureVerifierFactory func(ctx context.Context, publicKey crypto.PublicKey, opts *VerifyOptions) (dsse.Verifier, error) + +func WithSignatureVerifierFactory(factory SignatureVerifierFactory) func(*verifier) { + return func(r *verifier) { + r.signatureVerifierFactory = factory + } +} + +func WithLogVerifierFactory(factory LogVerifierFactory) func(*verifier) { + return func(r *verifier) { + r.logVerifierFactory = factory + } +} + +type LogVerifierFactory func(ctx context.Context, opts *VerifyOptions) (tlog.TransparencyLog, error) + +func NewVerfier(options ...func(*verifier)) (Verifier, error) { + verifier := &verifier{} + for _, opt := range options { + opt(verifier) + } + return verifier, nil +} + +type Verifier interface { + GetSignatureVerifier(ctx context.Context, publicKey crypto.PublicKey, opts *VerifyOptions) (dsse.Verifier, error) + GetLogVerifier(ctx context.Context, opts *VerifyOptions) (tlog.TransparencyLog, error) + VerifySignature(ctx context.Context, publicKey crypto.PublicKey, data []byte, signature []byte, opts *VerifyOptions) error + VerifyLog(ctx context.Context, keyMeta *KeyMetadata, data []byte, sig *Signature, opts *VerifyOptions) error +} + +// ensure it has all the necessary methods. +var _ Verifier = (*verifier)(nil) + +type verifier struct { + tufDownloader tuf.Downloader + signatureVerifierFactory SignatureVerifierFactory + logVerifierFactory LogVerifierFactory +} + +// GetLogVerifier implements Verifier. +func (v *verifier) GetLogVerifier(ctx context.Context, opts *VerifyOptions) (tlog.TransparencyLog, error) { + if v.logVerifierFactory != nil { + return v.logVerifierFactory(ctx, opts) + } + if opts.SkipTL { + return nil, nil + } + // TODO support other transparency logs + var transparencyLog tlog.TransparencyLog + switch opts.TransparencyLog { + case "", RekorTransparencyLogKind: + var err error + transparencyLog, err = tlog.NewRekorLog(tlog.WithTUFDownloader(v.tufDownloader)) + if err != nil { + return nil, fmt.Errorf("error failed to create rekor verifier: %w", err) + } + default: + return nil, fmt.Errorf("unsupported transparency log: %s", opts.TransparencyLog) + } + return transparencyLog, nil +} + +// GetSignatureVerifier implements Verifier. +func (v *verifier) GetSignatureVerifier(ctx context.Context, publicKey crypto.PublicKey, opts *VerifyOptions) (dsse.Verifier, error) { + if v.signatureVerifierFactory != nil { + return v.signatureVerifierFactory(ctx, publicKey, opts) + } + // TODO: use details from opts to decide which algorithm to use here + ecdsaVerifier, err := signerverifier.NewECDSAVerifier(publicKey) + if err != nil { + return nil, fmt.Errorf("error failed to create ecdsa verifier: %w", err) + } + return ecdsaVerifier, nil +} + +func (v *verifier) VerifySignature(ctx context.Context, publicKey crypto.PublicKey, data []byte, signature []byte, opts *VerifyOptions) error { + sigVerifier, err := v.GetSignatureVerifier(ctx, publicKey, opts) + if err != nil { + return fmt.Errorf("error failed to get verifier: %w", err) + } + return sigVerifier.Verify(ctx, data, signature) +} + +func (v *verifier) VerifyLog(ctx context.Context, keyMeta *KeyMetadata, encPayload []byte, sig *Signature, opts *VerifyOptions) error { + if opts.SkipTL { + return nil + } + if sig.Extension == nil || sig.Extension.Kind == "" { + return fmt.Errorf("error missing signature extension") + } + if sig.Extension.Kind != DockerDSSEExtKind { + return fmt.Errorf("error unsupported signature extension kind: %s", sig.Extension.Kind) + } + transparencyLog, err := v.GetLogVerifier(ctx, opts) + if err != nil { + return fmt.Errorf("error failed to get transparency log verifier: %w", err) + } + if transparencyLog == nil { + return fmt.Errorf("error missing transparency log verifier") + } + + // verify TL entry payload + publicKey, err := keyMeta.ParsedKey() + if err != nil { + return fmt.Errorf("error failed to parse public key: %w", err) + } + encodedPub, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return fmt.Errorf("error failed to marshal public key: %w", err) + } + integratedTime, err := transparencyLog.VerifyEntry(ctx, sig.Extension.Ext.TL, encPayload, encodedPub) + if err != nil { + return fmt.Errorf("TL entry failed verification: %w", err) + } + if integratedTime.Before(keyMeta.From) { + return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From) + } + if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) { + return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To) + } + return nil +} diff --git a/attestation/verifier_test.go b/attestation/verifier_test.go new file mode 100644 index 0000000..d2b6d1e --- /dev/null +++ b/attestation/verifier_test.go @@ -0,0 +1,55 @@ +package attestation + +import ( + "context" + "reflect" + "testing" + + "github.com/docker/attest/tlog" + "github.com/docker/attest/tuf" + "github.com/stretchr/testify/require" +) + +func Test_verifier_GetLogVerifier(t *testing.T) { + type fields struct { + tufDownloader tuf.Downloader + signatureVerifierFactory SignatureVerifierFactory + logVerifierFactory LogVerifierFactory + } + type args struct { + ctx context.Context + opts *VerifyOptions + } + rekor, err := tlog.NewRekorLog() + require.NoError(t, err) + tests := []struct { + name string + fields fields + args args + want tlog.TransparencyLog + wantErr bool + }{ + {name: "skip_tl true", fields: fields{}, args: args{ctx: context.Background(), opts: &VerifyOptions{SkipTL: true}}}, + {name: "skip_tl false", fields: fields{}, args: args{ctx: context.Background(), opts: &VerifyOptions{SkipTL: false}}, want: rekor}, + {name: "tl: rekor", fields: fields{logVerifierFactory: func(_ context.Context, _ *VerifyOptions) (tlog.TransparencyLog, error) { + return &tlog.Rekor{}, nil + }}, args: args{ctx: context.Background(), opts: &VerifyOptions{}}, want: &tlog.Rekor{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &verifier{ + tufDownloader: tt.fields.tufDownloader, + signatureVerifierFactory: tt.fields.signatureVerifierFactory, + logVerifierFactory: tt.fields.logVerifierFactory, + } + got, err := v.GetLogVerifier(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("verifier.GetLogVerifier() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("verifier.GetLogVerifier() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/attestation/verify.go b/attestation/verify.go index 31e3309..fbd47d6 100644 --- a/attestation/verify.go +++ b/attestation/verify.go @@ -2,37 +2,17 @@ package attestation import ( "context" - "crypto/ecdsa" - "crypto/x509" + "crypto" "encoding/base64" - "encoding/json" "fmt" - "time" - "github.com/docker/attest/internal/util" "github.com/docker/attest/signerverifier" - "github.com/docker/attest/tlog" intoto "github.com/in-toto/in-toto-golang/in_toto" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/secure-systems-lab/go-securesystemslib/dsse" ) -type KeyMetadata struct { - ID string `json:"id"` - PEM string `json:"key"` - From time.Time `json:"from"` - To *time.Time `json:"to"` - Status string `json:"status"` - SigningFormat string `json:"signing-format"` - Distrust bool `json:"distrust,omitempty"` -} - -type ( - Keys []*KeyMetadata - KeysMap map[string]*KeyMetadata -) - -func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte, error) { +func VerifyDSSE(ctx context.Context, verifier Verifier, env *Envelope, opts *VerifyOptions) ([]byte, error) { // enforce payload type if !ValidPayloadType(env.PayloadType) { return nil, fmt.Errorf("unsupported payload type %s", env.PayloadType) @@ -42,97 +22,62 @@ func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte return nil, fmt.Errorf("no signatures found") } + keys := make(map[string]*KeyMetadata, len(opts.Keys)) + for _, key := range opts.Keys { + keys[key.ID] = key + } + payload, err := base64Encoding.DecodeString(env.Payload) if err != nil { return nil, fmt.Errorf("error failed to decode payload: %w", err) } encPayload := dsse.PAE(env.PayloadType, payload) - // verify signatures and transparency log entry for _, sig := range env.Signatures { - err := verifySignature(ctx, sig, encPayload, opts) + // resolve public key used to sign + keyMeta, ok := keys[sig.KeyID] + if !ok { + return nil, fmt.Errorf("error key not found: %s", sig.KeyID) + } + + if keyMeta.Distrust { + return nil, fmt.Errorf("key %s is distrusted", keyMeta.ID) + } + publicKey, err := keyMeta.ParsedKey() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse public key: %w", err) + } + // decode signature + signature, err := base64.StdEncoding.Strict().DecodeString(sig.Sig) + if err != nil { + return nil, fmt.Errorf("error failed to decode signature: %w", err) + } + + err = verifier.VerifySignature(ctx, publicKey, encPayload, signature, opts) + if err != nil { + return nil, fmt.Errorf("error failed to verify signature: %w", err) + } + if err := verifier.VerifyLog(ctx, keyMeta, encPayload, sig, opts); err != nil { + return nil, fmt.Errorf("error failed to verify transparency log entry: %w", err) } } return payload, nil } -func verifySignature(ctx context.Context, sig *Signature, payload []byte, opts *VerifyOptions) error { - keys := make(map[string]*KeyMetadata, len(opts.Keys)) - for _, key := range opts.Keys { - keys[key.ID] = key - } - keyMeta, ok := keys[sig.KeyID] - if !ok { - return fmt.Errorf("error key not found: %s", sig.KeyID) - } - - if keyMeta.Distrust { - return fmt.Errorf("key %s is distrusted", keyMeta.ID) - } - // TODO: this is unmarshalling with MarshalPKIXPublicKey only for us to marshal it again - publicKey, err := signerverifier.ParsePublicKey([]byte(keyMeta.PEM)) - if err != nil { - return fmt.Errorf("failed to parse public key: %w", err) - } - - if !opts.SkipTL { - t := tlog.GetTL(ctx) - if sig.Extension == nil || sig.Extension.Kind == "" { - return fmt.Errorf("error missing signature extension") - } - if sig.Extension.Kind != DockerDSSEExtKind { - return fmt.Errorf("error unsupported signature extension kind: %s", sig.Extension.Kind) - } - - // verify TL entry - if sig.Extension.Ext.TL.Kind != RekorTLExtKind { - return fmt.Errorf("error unsupported TL extension kind: %s", sig.Extension.Ext.TL.Kind) - } - entry := sig.Extension.Ext.TL.Data - entryBytes, err := json.Marshal(entry) - if err != nil { - return fmt.Errorf("failed to marshal TL entry: %w", err) - } - - integratedTime, err := t.VerifyLogEntry(ctx, entryBytes) - if err != nil { - return fmt.Errorf("TL entry failed verification: %w", err) - } - if integratedTime.Before(keyMeta.From) { - return fmt.Errorf("key %s was not yet valid at TL log time %s (key valid from %s)", keyMeta.ID, integratedTime, keyMeta.From) - } - if keyMeta.To != nil && !integratedTime.Before(*keyMeta.To) { - return fmt.Errorf("key %s was already %s at TL log time %s (key %s at %s)", keyMeta.ID, keyMeta.Status, integratedTime, keyMeta.Status, *keyMeta.To) - } - // verify TL entry payload - encodedPub, err := x509.MarshalPKIXPublicKey(publicKey) - if err != nil { - return fmt.Errorf("error failed to marshal public key: %w", err) - } - err = t.VerifyEntryPayload(entryBytes, payload, encodedPub) - if err != nil { - return fmt.Errorf("TL entry failed payload verification: %w", err) - } - } - - // decode signature - signature, err := base64.StdEncoding.Strict().DecodeString(sig.Sig) - if err != nil { - return fmt.Errorf("error failed to decode signature: %w", err) - } - // verify payload ecdsa signature - ok = ecdsa.VerifyASN1(publicKey, util.SHA256(payload), signature) - if !ok { - return fmt.Errorf("payload signature is not valid") - } - - return nil -} - func ValidPayloadType(payloadType string) bool { return payloadType == intoto.PayloadType || payloadType == ociv1.MediaTypeDescriptor } + +func (km *KeyMetadata) ParsedKey() (crypto.PublicKey, error) { + if km.publicKey != nil { + return km.publicKey, nil + } + publicKey, err := signerverifier.ParsePublicKey([]byte(km.PEM)) + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %w", err) + } + km.publicKey = publicKey + return publicKey, nil +} diff --git a/attestation/verify_test.go b/attestation/verify_test.go index 1e13b24..0e3f0fc 100644 --- a/attestation/verify_test.go +++ b/attestation/verify_test.go @@ -42,8 +42,7 @@ func TestVerifyUnsignedAttestation(t *testing.T) { opts := &attestation.VerifyOptions{ Keys: attestation.Keys{}, } - - _, err := attestation.VerifyDSSE(ctx, env, opts) + _, err := attestation.VerifyDSSE(ctx, nil, env, opts) assert.Error(t, err) assert.Contains(t, err.Error(), "no signatures") } diff --git a/example_sign_test.go b/example_sign_test.go index e89b0e0..97ab9a7 100644 --- a/example_sign_test.go +++ b/example_sign_test.go @@ -7,6 +7,7 @@ import ( "github.com/docker/attest/attestation" "github.com/docker/attest/oci" "github.com/docker/attest/signerverifier" + "github.com/docker/attest/tlog" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" @@ -25,8 +26,14 @@ func ExampleSignStatements_remote() { // signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region) // configure signing options + + // use rekor transparency log wit static rekor public key (see options to use dynamic rekor public key) + rekor, err := tlog.NewRekorLog() + if err != nil { + panic(err) + } opts := &attestation.SigningOptions{ - SkipTL: true, // skip trust logging to a transparency log + TransparencyLog: rekor, // unset this to disable signature transparency logging } // load image index with unsigned attestation-manifests diff --git a/go.mod b/go.mod index 64152ca..1723224 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/secure-systems-lab/go-securesystemslib v0.8.0 github.com/sigstore/cosign/v2 v2.4.0 github.com/sigstore/rekor v1.3.6 + github.com/sigstore/sigstore v1.8.8 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.9 github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.9 github.com/stretchr/testify v1.9.0 @@ -126,7 +127,6 @@ require ( github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.3.2 // indirect - github.com/sigstore/sigstore v1.8.8 // indirect github.com/sigstore/timestamp-authority v1.2.2 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/internal/test/test.go b/internal/test/test.go index d762582..7ab6186 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -7,24 +7,22 @@ import ( _ "embed" "encoding/pem" "fmt" + "io" + "log" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" - "time" - "github.com/docker/attest/attestation" "github.com/docker/attest/signerverifier" - "github.com/docker/attest/tlog" "github.com/docker/attest/useragent" "github.com/google/go-containerregistry/pkg/registry" "github.com/secure-systems-lab/go-securesystemslib/dsse" ) const ( - UseMockTL = true UseMockKMS = true AWSRegion = "us-east-1" @@ -60,15 +58,7 @@ func GetMockSigner(_ context.Context) (dsse.SignerVerifier, error) { } func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) { - var tl tlog.TL - if UseMockTL { - tl = tlog.GetMockTL() - } else { - tl = &tlog.RekorTL{} - } - - ctx := tlog.WithTL(context.Background(), tl) - + ctx := context.Background() var signer dsse.SignerVerifier var err error if UseMockKMS { @@ -87,6 +77,7 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) { } func NewLocalRegistry(ctx context.Context, options ...registry.Option) *httptest.Server { + options = append(options, registry.Logger(log.New(io.Discard, "", log.LstdFlags))) regHandler := registry.New(options...) return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check the user agent @@ -99,7 +90,7 @@ func NewLocalRegistry(ctx context.Context, options ...registry.Option) *httptest })) } -func publicKeyToPEM(pubKey crypto.PublicKey) (string, error) { +func PublicKeyToPEM(pubKey crypto.PublicKey) (string, error) { derBytes, err := x509.MarshalPKIXPublicKey(pubKey) if err != nil { return "", err @@ -112,24 +103,3 @@ func publicKeyToPEM(pubKey crypto.PublicKey) (string, error) { return string(pem.EncodeToMemory(pemBlock)), nil } - -// LoadKeyMetadata loads the key metadata for the given signer verifier. -func GenKeyMetadata(sv dsse.SignerVerifier) (*attestation.KeyMetadata, error) { - pub := sv.Public() - pem, err := publicKeyToPEM(pub) - if err != nil { - return nil, fmt.Errorf("failed to convert public key to PEM: %w", err) - } - id, err := sv.KeyID() - if err != nil { - return nil, err - } - - return &attestation.KeyMetadata{ - ID: id, - Status: "active", - SigningFormat: "dssev1", - From: time.Now(), - PEM: pem, - }, nil -} diff --git a/policy/policy_test.go b/policy/policy_test.go index d209f09..a29e58f 100644 --- a/policy/policy_test.go +++ b/policy/policy_test.go @@ -39,8 +39,9 @@ func TestRegoEvaluator_Evaluate(t *testing.T) { TestDataPath := filepath.Join("..", "test", "testdata") ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json") - re := policy.NewRegoEvaluator(true) - + verifier, err := attestation.NewVerfier() + require.NoError(t, err) + re := policy.NewRegoEvaluator(true, verifier) defaultResolver := attestation.MockResolver{ Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)}, } diff --git a/policy/rego.go b/policy/rego.go index c87ea36..37e606e 100644 --- a/policy/rego.go +++ b/policy/rego.go @@ -21,7 +21,8 @@ import ( ) type regoEvaluator struct { - debug bool + debug bool + attestationVerifier attestation.Verifier } const ( @@ -29,9 +30,10 @@ const ( resultBinding = "result" ) -func NewRegoEvaluator(debug bool) Evaluator { +func NewRegoEvaluator(debug bool, attestationVerifier attestation.Verifier) Evaluator { return ®oEvaluator{ - debug: debug, + debug: debug, + attestationVerifier: attestationVerifier, } } @@ -86,7 +88,11 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Reso rego.Store(store), rego.GenerateJSON(jsonGenerator[Result]()), ) - for _, custom := range RegoFunctions(resolver) { + regoFnOpts := ®oFnOpts{ + attestationResolver: resolver, + attestationVerifier: re.attestationVerifier, + } + for _, custom := range RegoFunctions(regoFnOpts) { regoOpts = append(regoOpts, custom.Func) } @@ -169,7 +175,7 @@ func handleErrors2(f func(rCtx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, } } -func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin { +func RegoFunctions(regoOpts *regoFnOpts) []*tester.Builtin { return []*tester.Builtin{ { Decl: verifyDecl, @@ -180,7 +186,7 @@ func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin { Memoize: true, Nondeterministic: verifyDecl.Nondeterministic, }, - handleErrors2(verifyInTotoEnvelope(resolver))), + handleErrors2(verifyInTotoEnvelope(regoOpts))), }, { Decl: attestDecl, @@ -191,12 +197,12 @@ func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin { Memoize: true, Nondeterministic: attestDecl.Nondeterministic, }, - handleErrors1(fetchInTotoAttestations(resolver))), + handleErrors1(fetchInTotoAttestations(regoOpts))), }, } } -func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 { +func fetchInTotoAttestations(regoOpts *regoFnOpts) rego.Builtin1 { return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) { predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String) if !ok { @@ -204,7 +210,7 @@ func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 { } predicateType := string(predicateTypeStr) - envelopes, err := resolver.Attestations(rCtx.Context, predicateType) + envelopes, err := regoOpts.attestationResolver.Attestations(rCtx.Context, predicateType) if err != nil { return nil, err } @@ -226,7 +232,12 @@ func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 { } } -func verifyInTotoEnvelope(resolver attestation.Resolver) rego.Builtin2 { +type regoFnOpts struct { + attestationResolver attestation.Resolver + attestationVerifier attestation.Verifier +} + +func verifyInTotoEnvelope(regoOpts *regoFnOpts) rego.Builtin2 { return func(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) { env := new(attestation.Envelope) opts := new(attestation.VerifyOptions) @@ -238,8 +249,7 @@ func verifyInTotoEnvelope(resolver attestation.Resolver) rego.Builtin2 { if err != nil { return nil, fmt.Errorf("failed to cast verifier options: %w", err) } - - payload, err := attestation.VerifyDSSE(rCtx.Context, env, opts) + payload, err := attestation.VerifyDSSE(rCtx.Context, regoOpts.attestationVerifier, env, opts) if err != nil { return nil, fmt.Errorf("failed to verify envelope: %w", err) } @@ -257,7 +267,7 @@ func verifyInTotoEnvelope(resolver attestation.Resolver) rego.Builtin2 { return nil, fmt.Errorf("unsupported payload type: %s", env.PayloadType) } - err = VerifySubject(rCtx.Context, statement.Subject, resolver) + err = VerifySubject(rCtx.Context, statement.Subject, regoOpts.attestationResolver) if err != nil { return nil, fmt.Errorf("failed to verify subject: %w", err) } diff --git a/policy/types.go b/policy/types.go index 0828232..5a5f40c 100644 --- a/policy/types.go +++ b/policy/types.go @@ -1,6 +1,7 @@ package policy import ( + "github.com/docker/attest/attestation" "github.com/docker/attest/config" "github.com/docker/attest/tuf" intoto "github.com/in-toto/in-toto-golang/in_toto" @@ -27,14 +28,15 @@ type Result struct { } type Options struct { - TUFClientOptions *tuf.ClientOptions - DisableTUF bool - LocalTargetsDir string - LocalPolicyDir string - PolicyID string - ReferrersRepo string - AttestationStyle config.AttestationStyle - Debug bool + TUFClientOptions *tuf.ClientOptions + DisableTUF bool + LocalTargetsDir string + LocalPolicyDir string + PolicyID string + ReferrersRepo string + AttestationStyle config.AttestationStyle + Debug bool + AttestationVerifier attestation.Verifier } type Policy struct { diff --git a/signerverifier/aws.go b/signerverifier/aws.go index 8f817bb..a61568c 100644 --- a/signerverifier/aws.go +++ b/signerverifier/aws.go @@ -20,8 +20,5 @@ func GetAWSSigner(ctx context.Context, keyARN string, region string) (dsse.Signe if err != nil { return nil, fmt.Errorf("error getting aws crypto signer: %w", err) } - signer := &ECDSA256SignerVerifier{ - Signer: cs, - } - return signer, nil + return NewECDSASignerVerifier(cs) } diff --git a/signerverifier/common.go b/signerverifier/common.go index 87124fd..0747460 100644 --- a/signerverifier/common.go +++ b/signerverifier/common.go @@ -9,52 +9,17 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "io" - "github.com/docker/attest/internal/util" "github.com/secure-systems-lab/go-securesystemslib/dsse" ) -type ECDSA256SignerVerifier struct { - crypto.Signer -} - -// implement keyid function. -func (s *ECDSA256SignerVerifier) KeyID() (string, error) { - keyid, err := KeyID(s.Signer.Public()) - if err != nil { - return "", fmt.Errorf("error getting keyid: %w", err) - } - return keyid, nil -} - -func (s *ECDSA256SignerVerifier) Public() crypto.PublicKey { - return s.Signer.Public() -} - -func (s *ECDSA256SignerVerifier) Sign(_ context.Context, data []byte) ([]byte, error) { - return s.Signer.Sign(rand.Reader, data, crypto.SHA256) -} - -func (s *ECDSA256SignerVerifier) Verify(_ context.Context, data []byte, sig []byte) error { - pub, ok := s.Signer.Public().(*ecdsa.PublicKey) - if !ok { - return fmt.Errorf("public key is not ecdsa") - } - ok = ecdsa.VerifyASN1(pub, util.SHA256(data), sig) - if !ok { - return fmt.Errorf("payload signature is not valid") - } - return nil -} - func LoadKeyPair(priv []byte) (dsse.SignerVerifier, error) { privateKey, err := parsePriv(priv) if err != nil { return nil, err } - return &ECDSA256SignerVerifier{ - Signer: privateKey, - }, nil + return NewECDSASignerVerifier(privateKey) } func parsePriv(privkeyBytes []byte) (*ecdsa.PrivateKey, error) { @@ -78,7 +43,26 @@ func GenKeyPair() (dsse.SignerVerifier, error) { if err != nil { return nil, err } - return &ECDSA256SignerVerifier{ - Signer: signer, - }, nil + return NewECDSASignerVerifier(signer) +} + +// ensure it implements crypto.Signer. +var _ crypto.Signer = (*cryptoSignerWrapper)(nil) + +type cryptoSignerWrapper struct { + sv dsse.SignerVerifier +} + +// Public implements crypto.Signer. +func (c *cryptoSignerWrapper) Public() crypto.PublicKey { + return c.sv.Public() +} + +// Sign implements crypto.Signer. +func (c *cryptoSignerWrapper) Sign(_ io.Reader, digest []byte, _ crypto.SignerOpts) (signature []byte, err error) { + return c.sv.Sign(context.Background(), digest) +} + +func AsCryptoSigner(signer dsse.SignerVerifier) (crypto.Signer, error) { + return &cryptoSignerWrapper{sv: signer}, nil } diff --git a/signerverifier/ecdsa.go b/signerverifier/ecdsa.go new file mode 100644 index 0000000..18bf9c7 --- /dev/null +++ b/signerverifier/ecdsa.go @@ -0,0 +1,80 @@ +package signerverifier + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/rand" + "fmt" + + "github.com/docker/attest/internal/util" + "github.com/secure-systems-lab/go-securesystemslib/dsse" +) + +type ecdsaVerifier struct { + publicKey *ecdsa.PublicKey + keyID string +} + +// ensure ECDSAVerifier implements dsse.Verifier. +var _ dsse.Verifier = (*ecdsaVerifier)(nil) + +func NewECDSAVerifier(publicKey crypto.PublicKey) (dsse.Verifier, error) { + ecdsaPublicKey, ok := (publicKey).(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("public key is not an ECDSA public key") + } + return &ecdsaVerifier{ + publicKey: ecdsaPublicKey, + }, nil +} + +func (v *ecdsaVerifier) Verify(_ context.Context, data, signature []byte) error { + // verify payload ecdsa signature + ok := ecdsa.VerifyASN1(v.publicKey, util.SHA256(data), signature) + if !ok { + return fmt.Errorf("payload signature is not valid") + } + + return nil +} + +func (v *ecdsaVerifier) Public() crypto.PublicKey { + return v.publicKey +} + +func (v *ecdsaVerifier) KeyID() (string, error) { + if v.keyID != "" { + return v.keyID, nil + } + keyID, err := KeyID(v.publicKey) + if err != nil { + return "", fmt.Errorf("failed to get key ID: %w", err) + } + v.keyID = keyID + return v.keyID, nil +} + +// must implement dsse.SignerVerifier interface. +var _ dsse.SignerVerifier = (*ecdsa256SignerVerifier)(nil) + +type ecdsa256SignerVerifier struct { + signer crypto.Signer + dsse.Verifier +} + +func NewECDSASignerVerifier(signer crypto.Signer) (dsse.SignerVerifier, error) { + verifier, err := NewECDSAVerifier(signer.Public()) + if err != nil { + return nil, fmt.Errorf("failed to create verifier: %w", err) + } + sv := &ecdsa256SignerVerifier{ + signer: signer, + Verifier: verifier, + } + return sv, nil +} + +func (s *ecdsa256SignerVerifier) Sign(_ context.Context, data []byte) ([]byte, error) { + return s.signer.Sign(rand.Reader, data, crypto.SHA256) +} diff --git a/signerverifier/gcp.go b/signerverifier/gcp.go index e8b81a4..0768451 100644 --- a/signerverifier/gcp.go +++ b/signerverifier/gcp.go @@ -21,8 +21,5 @@ func GetGCPSigner(ctx context.Context, reference string, opts ...option.ClientOp if err != nil { return nil, fmt.Errorf("error getting gcp crypto signer: %w", err) } - signer := &ECDSA256SignerVerifier{ - Signer: cs, - } - return signer, nil + return NewECDSASignerVerifier(cs) } diff --git a/signerverifier/gcp_test.go b/signerverifier/gcp_test.go index 90f4ac5..e603ab9 100644 --- a/signerverifier/gcp_test.go +++ b/signerverifier/gcp_test.go @@ -40,6 +40,14 @@ func TestGCPKMS_Signer(t *testing.T) { publicKey, err := ParsePublicKey([]byte(publicKeyPEM)) require.NoError(t, err) // verify payload ecdsa signature - ok := ecdsa.VerifyASN1(publicKey, hash, sig) + + ecdsaPublicKey, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + t.Fatal("Failed to convert publicKey to *ecdsa.PublicKey") + } + ok = ecdsa.VerifyASN1(ecdsaPublicKey, hash, sig) assert.True(t, ok) + + err = signer.Verify(ctx, msg, sig) + require.NoError(t, err) } diff --git a/signerverifier/parse.go b/signerverifier/parse.go index c4be243..93e042e 100644 --- a/signerverifier/parse.go +++ b/signerverifier/parse.go @@ -1,6 +1,7 @@ package signerverifier import ( + "crypto" "crypto/ecdsa" "crypto/x509" "encoding/pem" @@ -9,7 +10,7 @@ import ( const pemType = "PUBLIC KEY" -func ParsePublicKey(pubkeyBytes []byte) (*ecdsa.PublicKey, error) { +func ParsePublicKey(pubkeyBytes []byte) (crypto.PublicKey, error) { p, _ := pem.Decode(pubkeyBytes) if p == nil { return nil, fmt.Errorf("pubkey file does not contain any PEM data") @@ -17,12 +18,15 @@ func ParsePublicKey(pubkeyBytes []byte) (*ecdsa.PublicKey, error) { if p.Type != pemType { return nil, fmt.Errorf("pubkey file does not contain a public key") } - pubKey, err := x509.ParsePKIXPublicKey(p.Bytes) - if err != nil { - return nil, fmt.Errorf("error failed to parse public key: %w", err) - } + return x509.ParsePKIXPublicKey(p.Bytes) +} - ecdsaPubKey, ok := pubKey.(*ecdsa.PublicKey) +func ParseECDSAPublicKey(pubkeyBytes []byte) (*ecdsa.PublicKey, error) { + pk, err := ParsePublicKey(pubkeyBytes) + if err != nil { + return nil, err + } + ecdsaPubKey, ok := pk.(*ecdsa.PublicKey) if !ok { return nil, fmt.Errorf("error public key is not an ecdsa key: %w", err) } @@ -34,6 +38,5 @@ func ConvertToPEM(ecdsaPubKey *ecdsa.PublicKey) ([]byte, error) { if err != nil { return nil, fmt.Errorf("error failed to marshal public key: %w", err) } - return pem.EncodeToMemory(&pem.Block{Type: pemType, Bytes: pubKeyBytes}), nil } diff --git a/test/testdata/local-policy-fail/policy.rego b/test/testdata/local-policy-fail/policy.rego index 7f58cbb..1440d3c 100644 --- a/test/testdata/local-policy-fail/policy.rego +++ b/test/testdata/local-policy-fail/policy.rego @@ -23,7 +23,7 @@ atts := union({ provs("https://spdx.dev/Document"), }) -opts := {"keys": keys} +opts := {"keys": keys, "skip_tl": true} statements contains s if { some att in atts diff --git a/test/testdata/local-policy-pass/policy.rego b/test/testdata/local-policy-pass/policy.rego index ff9f04b..71a3b82 100644 --- a/test/testdata/local-policy-pass/policy.rego +++ b/test/testdata/local-policy-pass/policy.rego @@ -23,7 +23,7 @@ atts := union({ provs("https://spdx.dev/Document"), }) -opts := {"keys": keys} +opts := {"keys": keys, "skip_tl": true} statements contains s if { some att in atts diff --git a/test/testdata/local-policy/doi/policy.rego b/test/testdata/local-policy/doi/policy.rego index aadb8cd..9666ba1 100644 --- a/test/testdata/local-policy/doi/policy.rego +++ b/test/testdata/local-policy/doi/policy.rego @@ -22,7 +22,7 @@ atts := union({ provs("https://spdx.dev/Document"), }) -opts := {"keys": keys} +opts := {"keys": keys, "skip_tl": false} statements contains s if { some att in atts diff --git a/tlog/keys/c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d.pem b/tlog/keys/c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d.pem new file mode 100644 index 0000000..050ef60 --- /dev/null +++ b/tlog/keys/c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr +kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw== +-----END PUBLIC KEY----- diff --git a/tlog/mock.go b/tlog/mock.go index 33bf80b..9126718 100644 --- a/tlog/mock.go +++ b/tlog/mock.go @@ -2,6 +2,7 @@ package tlog import ( "context" + "encoding/json" "fmt" "time" @@ -15,36 +16,48 @@ const ( TestEntry = `{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI5Zjg2ZDA4MTg4NGM3ZDY1OWEyZmVhYTBjNTVhZDAxNWEzYmY0ZjFiMmIwYjgyMmNkMTVkNmMxNWIwZjAwYTA4In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQUlyVUZGUzBIYmNzZjc5L08yajVXdHl2R2Vvd1NVSXpZcDlBM2IwWnREVUFpQVQxZU42ZjFyVmVWa011REFlN3dxWkJ2bE5LY2VsajNVVDNmaWhyQjZSY2c9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVSlZla05DSzJGQlJFRm5SVU5CWjBWQ1RVRnZSME5EY1VkVFRUUTVRa0ZOUTAxQk9IaEVWRUZNUW1kT1ZrSkJUVlJDU0ZKc1l6TlJkMGhvWTA0S1RXcE5lRTFxU1ROTlZHdDVUWHBWTlZkb1kwNU5hbEY0VFdwSk1rMVVhM2xOZWxVMVYycEJVRTFSTUhkRGQxbEVWbEZSUkVWM1VqQmFXRTR3VFVacmR3cEZkMWxJUzI5YVNYcHFNRU5CVVZsSlMyOWFTWHBxTUVSQlVXTkVVV2RCUlVRMFZpdFNSV2g0SzJGeFYwZzNlV3hOVFVSSVlXaE9UVzVOVEZOUFNsQXZDamxyUVcwNWJIQXJNMjF4V1ZSQmFGVlNjbUUyVDBRMVVYZzRXbUprSzJWMVVIbFFhemw1SzNjdloxZEhSRUk1ZW00dlNXd3hTMDVIVFVWUmQwUm5XVVFLVmxJd1VFRlJTQzlDUVZGRVFXZGxRVTFDVFVkQk1WVmtTbEZSVFUxQmIwZERRM05IUVZGVlJrSjNUVVJOUVhkSFFURlZaRVYzUlVJdmQxRkRUVUZCZHdwRWQxbEVWbEl3VWtKQlozZENiMGxGWkVkV2VtUkVRVXRDWjJkeGFHdHFUMUJSVVVSQlowNUtRVVJDUjBGcFJVRTNOMjFFTDFSbVJtRlJVemxrWlhRMENqbFhaRk41YURKT1VTOUZiMVJtYVVGdFFtaHVWblpEVTNSUVowTkpVVU1yZDNSdllpOU9iMUp4T0c5cU4wZDNibTVKYUZKVGRDOVJNbmtyVXpoUkwzSUthRkpVYW5GaE9HZExRVDA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn19fX0=","integratedTime":1703705039,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d","logIndex":59674396,"verification":{"inclusionProof":{"checkpoint":"rekor.sigstore.dev - 2605736670972794746\n55510966\nJCi1O53Xmdi9lXnui4Q5SQ+MJSMnWr1Bxn+Q2Qf22tU=\nTimestamp: 1703705040158839214\n\n— rekor.sigstore.dev wNI9ajBFAiAXgtjFDVqCSgiSP04TQzELrz4+EyBwyYVL2EEULTCy0AIhAI9peLU76ZUD1tvU8qvzBJBo77IYD1rc+A1MPc35AeVK\n","hashes":["fb77ee213b48f4b18dc81c6e634c570abf99b257713561f174f2e0f4c039af67","6cb113bbefadecbbb8b89b1c08232438a6125071790b6a062cff8c1ccfdcb91e","6fbe1424e264e4590ca502d671b7a036c87f7a90d1f57534b98eb781144160bf","077b606720a6478200f6c3ed08a68e9b01b1cae192cb120888ddcc95521601bd","b6f8e8bc21ae0cde82b92422a4b4f37b28a43185821e468a4e65b6c79ed8f5b7","89332533fac54e9bc68c7353c42f6ebb9fe38039f67910332ff95082072068d4","0814d6f707a75fb3334bab14ab5466bd8b9a64ae7be7cd4d53a428c64932bc66","e883e826f10329c63a4a2ed21156037a050df43b9d74079296beac6968ed4150","d79230703257b7e4a8a61b032b6980d1a0bdbc7ae96ca838b525b3751785fe48","2f4a77e5288462cd3b75084d37f1502dcbe0943d18dd95cb247fc1ebbabc0aad","38562c253d3536d0d00e3547c880b6b0251a25ac69605b50c9eaa1a27186cc7a","9dea192350ff8b3c0f5ccda38261cb38ebd61869281c3928912332d1144e0a04","2c4d25ba59aa573ab2c79c2d3cd9e1d74789b10632432724d63112ce50b44874","98c486feb5d87092a78a46c4b5be04868654900affc2e86ffb20074dc73a883a","6969c49bd73f19bf28a5eaeabd331ddd60502defb2cd3d96e17b741c80adec6c"],"logIndex":55510965,"rootHash":"2428b53b9dd799d8bd9579ee8b8439490f8c2523275abd41c67f90d907f6dad5","treeSize":55510966},"signedEntryTimestamp":"MEUCIQCG9PRI8PcvtJyE9pbcculZipze6NEWR1Nk8EYocto3BwIgYu5gqgjW80HMjSjUxUNJLp0wlVTesnJCeByUBySc59w="}}` ) -func GetMockTL() TL { - unmarshalEntry := func(entry []byte) (*models.LogEntryAnon, error) { - le := new(models.LogEntryAnon) - err := le.UnmarshalBinary(entry) - if err != nil { - return nil, fmt.Errorf("error failed to unmarshal TL entry: %w", err) - } - return le, nil - } - - return &MockTL{ - UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) ([]byte, error) { - return []byte(TestEntry), nil +func GetMockTL() TransparencyLog { + return &MockTransparencyLog{ + UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) (*DockerTLExtension, error) { + return &DockerTLExtension{ + Kind: "Mock", + Data: json.RawMessage(TestEntry), + }, nil }, - VerifyLogEntryFunc: func(_ context.Context, entryBytes []byte) (time.Time, error) { + VerifyLogEntryFunc: func(_ context.Context, ext *DockerTLExtension, _, _ []byte) (time.Time, error) { // return the integrated time in the log entry without any checking - le, err := unmarshalEntry(entryBytes) + entry := new(models.LogEntryAnon) + entryBytes, err := json.Marshal(ext.Data) if err != nil { - return time.Time{}, err + return time.Time{}, fmt.Errorf("error failed to marshal TL entry: %w", err) } - if le.IntegratedTime == nil { + err = entry.UnmarshalBinary(entryBytes) + if err != nil { + return time.Time{}, fmt.Errorf("error failed to unmarshal TL entry: %w", err) + } + if entry.IntegratedTime == nil { return time.Time{}, fmt.Errorf("error missing integrated time in TL entry") } - return time.Unix(*le.IntegratedTime, 0), nil - }, - VerifyEntryPayloadFunc: func(_, _, _ []byte) error { - return nil - }, - UnmarshalEntryFunc: func(entry []byte) (any, error) { - return unmarshalEntry(entry) + return time.Unix(*entry.IntegratedTime, 0), nil }, } } + +type MockTransparencyLog struct { + UploadLogEntryFunc func(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) (*DockerTLExtension, error) + VerifyLogEntryFunc func(ctx context.Context, ext *DockerTLExtension, payload, publicKey []byte) (time.Time, error) +} + +func (tl *MockTransparencyLog) UploadEntry(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) (*DockerTLExtension, error) { + if tl.UploadLogEntryFunc != nil { + return tl.UploadLogEntryFunc(ctx, subject, payload, signature, signer) + } + return nil, nil +} + +func (tl *MockTransparencyLog) VerifyEntry(ctx context.Context, ext *DockerTLExtension, payload, publicKey []byte) (time.Time, error) { + if tl.VerifyLogEntryFunc != nil { + return tl.VerifyLogEntryFunc(ctx, ext, payload, publicKey) + } + return time.Time{}, nil +} diff --git a/tlog/rekor.go b/tlog/rekor.go new file mode 100644 index 0000000..2ae536b --- /dev/null +++ b/tlog/rekor.go @@ -0,0 +1,229 @@ +package tlog + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "path/filepath" + "strings" + "time" + + "github.com/docker/attest/internal/util" + "github.com/docker/attest/signerverifier" + "github.com/docker/attest/tuf" + "github.com/docker/attest/useragent" + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/v2/pkg/cosign" + rclient "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" + hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" + + stuf "github.com/sigstore/sigstore/pkg/tuf" + + _ "embed" +) + +const RekorTLExtKind = "Rekor" + +// ensure it has all the necessary methods. +var _ TransparencyLog = (*Rekor)(nil) + +const defaultPublicKeysDir = "rekor" + +type Rekor struct { + publicKeys *cosign.TrustedTransparencyLogPubKeys + tufDownloader tuf.Downloader + publicKeysDir string +} + +//go:embed keys/c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d.pem +var rekorPublicKey []byte + +func WithTUFDownloader(tufDownloader tuf.Downloader) func(*Rekor) { + return func(r *Rekor) { + r.tufDownloader = tufDownloader + } +} + +func WithTUFPublicKeysDir(dir string) func(*Rekor) { + return func(r *Rekor) { + r.publicKeysDir = dir + } +} + +func NewRekorLog(options ...func(*Rekor)) (*Rekor, error) { + pk, err := signerverifier.ParsePublicKey(rekorPublicKey) + if err != nil { + return nil, fmt.Errorf("error parsing rekor public key: %w", err) + } + kid, err := signerverifier.KeyID(pk) + if err != nil { + return nil, fmt.Errorf("error getting keyid: %w", err) + } + keys := map[string]cosign.TransparencyLogPubKey{ + kid: { + PubKey: pk, + Status: stuf.Active, + }, + } + rekor := &Rekor{ + publicKeys: &cosign.TrustedTransparencyLogPubKeys{ + Keys: keys, + }, + publicKeysDir: defaultPublicKeysDir, + } + for _, opt := range options { + opt(rekor) + } + return rekor, nil +} + +// UploadEntry submits a PK token signature to the transparency log. +func (tl *Rekor) UploadEntry(ctx context.Context, subject string, encPayload, signature []byte, signer dsse.SignerVerifier) (*DockerTLExtension, error) { + // generate self-signed x509 cert + pubCert, err := CreateX509Cert(subject, signer) + if err != nil { + return nil, fmt.Errorf("Error creating x509 cert: %w", err) + } + + // generate hash of payload + hasher := sha256.New() + hasher.Write(encPayload) + + // upload entry + rekorClient, err := rclient.GetRekorClient(DefaultRekorURL, rclient.WithUserAgent(useragent.Get(ctx))) + if err != nil { + return nil, fmt.Errorf("Error creating rekor client: %w", err) + } + entry, err := cosign.TLogUpload(ctx, rekorClient, signature, hasher, pubCert) + if err != nil { + return nil, fmt.Errorf("Error uploading tlog: %w", err) + } + + return &DockerTLExtension{ + Kind: RekorTLExtKind, + Data: entry, // transparency log entry metadata + }, nil +} + +// VerifyEntry verifies a transparency log entry. +func (tl *Rekor) VerifyEntry(ctx context.Context, ext *DockerTLExtension, encPayload, publicKey []byte) (time.Time, error) { + zeroTime := time.Time{} + // because the Data field has been unmarsalled into a map[string]interface{} we need to marshal it back to bytes + // for the unmarshaler to work correctly + entryBytes, err := json.Marshal(ext.Data) + if err != nil { + return time.Time{}, fmt.Errorf("error failed to marshal TL entry: %w", err) + } + + entry, err := tl.UnmarshalEntry(entryBytes) + if err != nil { + return zeroTime, fmt.Errorf("error unmarshaling TL entry: %w", err) + } + + err = entry.Validate(strfmt.Default) + if err != nil { + return zeroTime, fmt.Errorf("TL entry failed validation: %w", err) + } + // check if tl.publicKeys containers le.LogId + _, ok := tl.publicKeys.Keys[*entry.LogID] + if !ok { + // otherwise check TUF + pkTarget, err := tl.tufDownloader.DownloadTarget(filepath.Join(tl.publicKeysDir, fmt.Sprintf("%s.pem", *entry.LogID)), "") + if err != nil { + return zeroTime, fmt.Errorf("error downloading rekor public key %s: %w", *entry.LogID, err) + } + pk, err := signerverifier.ParsePublicKey(pkTarget.Data) + if err != nil { + return zeroTime, fmt.Errorf("error parsing public key: %w", err) + } + tl.publicKeys.Keys[*entry.LogID] = cosign.TransparencyLogPubKey{ + PubKey: pk, + Status: stuf.Active, + } + } + err = cosign.VerifyTLogEntryOffline(ctx, entry, tl.publicKeys) + if err != nil { + return zeroTime, fmt.Errorf("TL entry failed verification: %w", err) + } + + integratedTime := time.Unix(*entry.IntegratedTime, 0) + + err = tl.VerifyEntryPayload(entry, encPayload, publicKey) + if err != nil { + return zeroTime, fmt.Errorf("error verifying TL entry payload: %w", err) + } + return integratedTime, nil +} + +// VerifyEntryPayload checks that the TL entry payload matches envelope payload. +func (tl *Rekor) VerifyEntryPayload(entry *models.LogEntryAnon, payload, publicKey []byte) error { + tlBody, ok := entry.Body.(string) + if !ok { + return fmt.Errorf("expected tl body to be of type string, got %T", entry) + } + rekord, err := extractHashedRekord(tlBody) + if err != nil { + return fmt.Errorf("error extract HashedRekord from TL entry: %w", err) + } + + // compare payload hashes + payloadHash := util.SHA256Hex(payload) + if rekord.Hash != payloadHash { + return fmt.Errorf("error payload and tl entry hash mismatch") + } + + // compare public keys + cert, err := base64.StdEncoding.Strict().DecodeString(rekord.PublicKey) + if err != nil { + return fmt.Errorf("failed to decode public key: %w", err) + } + p, _ := pem.Decode(cert) + result, err := x509.ParseCertificate(p.Bytes) + if err != nil { + return fmt.Errorf("failed to parse certificate: %w", err) + } + if !bytes.Equal(result.RawSubjectPublicKeyInfo, publicKey) { + return fmt.Errorf("error payload and tl entry public key mismatch") + } + return nil +} + +func (tl *Rekor) UnmarshalEntry(entry []byte) (*models.LogEntryAnon, error) { + le := new(models.LogEntryAnon) + err := le.UnmarshalBinary(entry) + if err != nil { + return nil, fmt.Errorf("error failed to unmarshal Rekor entry: %w", err) + } + return le, nil +} + +func extractHashedRekord(body string) (*Payload, error) { + sig := new(Payload) + pe, err := models.UnmarshalProposedEntry(base64.NewDecoder(base64.StdEncoding, strings.NewReader(body)), runtime.JSONConsumer()) + if err != nil { + return nil, err + } + impl, err := types.UnmarshalEntry(pe) + if err != nil { + return nil, err + } + switch entry := impl.(type) { + case *hashedrekord_v001.V001Entry: + sig.Algorithm = *entry.HashedRekordObj.Data.Hash.Algorithm + sig.Hash = *entry.HashedRekordObj.Data.Hash.Value + sig.Signature = entry.HashedRekordObj.Signature.Content.String() + sig.PublicKey = entry.HashedRekordObj.Signature.PublicKey.Content.String() + return sig, nil + default: + return nil, fmt.Errorf("failed to extract haskedrekord, unsupported type: %T", entry) + } +} diff --git a/tlog/rekor_test.go b/tlog/rekor_test.go new file mode 100644 index 0000000..deeb964 --- /dev/null +++ b/tlog/rekor_test.go @@ -0,0 +1,64 @@ +//go:build e2e + +package tlog + +import ( + "context" + "crypto/x509" + _ "embed" + "testing" + "time" + + "github.com/docker/attest/internal/test" + "github.com/docker/attest/internal/util" + "github.com/docker/attest/signerverifier" + "github.com/docker/attest/tuf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// NOTE: these are only run on CI to protect Rekor, but work just fine locally. +func TestRekor(t *testing.T) { + // message digest + payload := []byte("test") + hash := util.SHA256(payload) + // generate ephemeral keys to sign message digest + signer, err := signerverifier.GenKeyPair() + assert.NoError(t, err) + sig, err := signer.Sign(context.Background(), hash) + assert.NoError(t, err) + tests := []struct { + name string + tufDownloader tuf.Downloader + pubKeysDir string + }{ + {name: "TestRekor (no tuf)"}, + {name: "TestRekor (with tuf)", tufDownloader: tuf.NewMockTufClient("."), pubKeysDir: "keys"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pk := signer.Public() + publicKey, err := x509.MarshalPKIXPublicKey(pk) + if tt.tufDownloader != nil { + // set to incorrect public key to test TUF flow + keyStr, err := test.PublicKeyToPEM(pk) + require.NoError(t, err) + rekorPublicKey = []byte(keyStr) + } + + rekor, err := NewRekorLog(WithTUFDownloader(tt.tufDownloader), WithTUFPublicKeysDir(tt.pubKeysDir)) + + require.NoError(t, err) + require.NotNil(t, rekor) + ext, err := rekor.UploadEntry(context.Background(), "test", payload, sig, signer) + require.NoError(t, err) + require.NotNil(t, ext) + assert.Equal(t, RekorTLExtKind, ext.Kind) + assert.NotEmpty(t, ext.Data) + + when, err := rekor.VerifyEntry(context.Background(), ext, payload, publicKey) + require.NoError(t, err) + assert.WithinDuration(t, time.Now(), when, 5*time.Second) + }) + } +} diff --git a/tlog/tl.go b/tlog/tl.go index 17f7e50..46a2dc7 100644 --- a/tlog/tl.go +++ b/tlog/tl.go @@ -1,162 +1,38 @@ package tlog import ( - "bytes" "context" "crypto/rand" - "crypto/sha256" "crypto/x509" "crypto/x509/pkix" - "encoding/base64" "encoding/pem" "fmt" "math/big" - "strings" "time" - "github.com/docker/attest/internal/util" "github.com/docker/attest/signerverifier" - "github.com/docker/attest/useragent" - "github.com/go-openapi/runtime" - "github.com/go-openapi/strfmt" "github.com/secure-systems-lab/go-securesystemslib/dsse" - "github.com/sigstore/cosign/v2/pkg/cosign" - rclient "github.com/sigstore/rekor/pkg/client" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/rekor/pkg/types" - hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" ) const ( DefaultRekorURL = "https://rekor.sigstore.dev" ) -type tlCtxKeyType struct{} - -var TLCtxKey tlCtxKeyType - -// sets TL in context. -func WithTL(ctx context.Context, tl TL) context.Context { - return context.WithValue(ctx, TLCtxKey, tl) +type TransparencyLog interface { + UploadEntry(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) (*DockerTLExtension, error) + VerifyEntry(ctx context.Context, entry *DockerTLExtension, payload, publicKey []byte) (time.Time, error) } -// gets TL from context, defaults to Rekor TL if not set. -func GetTL(ctx context.Context) TL { - t, ok := ctx.Value(TLCtxKey).(TL) - if !ok { - t = &RekorTL{} - } - return t -} - -type TLPayload struct { +type Payload struct { Algorithm string Hash string Signature string PublicKey string } -type TL interface { - UploadLogEntry(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) ([]byte, error) - VerifyLogEntry(ctx context.Context, entryBytes []byte) (time.Time, error) - VerifyEntryPayload(entryBytes, payload, publicKey []byte) error - UnmarshalEntry(entryBytes []byte) (any, error) -} - -type MockTL struct { - UploadLogEntryFunc func(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) ([]byte, error) - VerifyLogEntryFunc func(ctx context.Context, entryBytes []byte) (time.Time, error) - VerifyEntryPayloadFunc func(entryBytes, payload, publicKey []byte) error - UnmarshalEntryFunc func(entryBytes []byte) (any, error) -} - -func (tl *MockTL) UploadLogEntry(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) ([]byte, error) { - if tl.UploadLogEntryFunc != nil { - return tl.UploadLogEntryFunc(ctx, subject, payload, signature, signer) - } - return nil, nil -} - -func (tl *MockTL) VerifyLogEntry(ctx context.Context, entryBytes []byte) (time.Time, error) { - if tl.VerifyLogEntryFunc != nil { - return tl.VerifyLogEntryFunc(ctx, entryBytes) - } - return time.Time{}, nil -} - -func (tl *MockTL) VerifyEntryPayload(entryBytes, payload, publicKey []byte) error { - if tl.VerifyEntryPayloadFunc != nil { - return tl.VerifyEntryPayloadFunc(entryBytes, payload, publicKey) - } - return nil -} - -func (tl *MockTL) UnmarshalEntry(entryBytes []byte) (any, error) { - if tl.UnmarshalEntryFunc != nil { - return tl.UnmarshalEntryFunc(entryBytes) - } - return nil, nil -} - -type RekorTL struct{} - -// UploadLogEntry submits a PK token signature to the transparency log. -func (tl *RekorTL) UploadLogEntry(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) ([]byte, error) { - // generate self-signed x509 cert - pubCert, err := CreateX509Cert(subject, signer) - if err != nil { - return nil, fmt.Errorf("Error creating x509 cert: %w", err) - } - - // generate hash of payload - hasher := sha256.New() - hasher.Write(payload) - - // upload entry - rekorClient, err := rclient.GetRekorClient(DefaultRekorURL, rclient.WithUserAgent(useragent.Get(ctx))) - if err != nil { - return nil, fmt.Errorf("Error creating rekor client: %w", err) - } - entry, err := cosign.TLogUpload(ctx, rekorClient, signature, hasher, pubCert) - if err != nil { - return nil, fmt.Errorf("Error uploading tlog: %w", err) - } - entryBytes, err := entry.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("error marshaling TL entry: %w", err) - } - return entryBytes, nil -} - -// VerifyLogEntry verifies a transparency log entry. -func (tl *RekorTL) VerifyLogEntry(ctx context.Context, entryBytes []byte) (time.Time, error) { - zeroTime := time.Time{} - entry, err := tl.UnmarshalEntry(entryBytes) - if err != nil { - return zeroTime, fmt.Errorf("error failed to unmarshal TL entry: %w", err) - } - le, ok := entry.(*models.LogEntryAnon) - if !ok { - return zeroTime, fmt.Errorf("expected entry to be of type *models.LogEntryAnon, got %T", entry) - } - err = le.Validate(strfmt.Default) - if err != nil { - return zeroTime, fmt.Errorf("TL entry failed validation: %w", err) - } - - // TODO: get rekor public keys from TUF (ours or theirs?), and/or embed the public key in the binary - rekorPubKeys, err := cosign.GetRekorPubs(ctx) - if err != nil { - return zeroTime, fmt.Errorf("error failed to get rekor public keys: %w", err) - } - err = cosign.VerifyTLogEntryOffline(ctx, le, rekorPubKeys) - if err != nil { - return zeroTime, fmt.Errorf("TL entry failed verification: %w", err) - } - - integratedTime := time.Unix(*le.IntegratedTime, 0) - - return integratedTime, nil +type DockerTLExtension struct { + Kind string `json:"kind"` + Data any `json:"data"` } // CreateX509Cert generates a self-signed x509 cert for TL submission. @@ -182,87 +58,15 @@ func CreateX509Cert(subject string, signer dsse.SignerVerifier) ([]byte, error) // dsse.SignerVerifier doesn't implement cypto.Signer exactly - csigner, ok := signer.(*signerverifier.ECDSA256SignerVerifier) - if !ok { - return nil, fmt.Errorf("expected signer to be of type *signerverifier.ECDSA_SignerVerifier, got %T", signer) + csigner, err := signerverifier.AsCryptoSigner(signer) + if err != nil { + return nil, fmt.Errorf("error converting signer to crypto.Signer: %w", err) } // create a self-signed X.509 certificate - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, signer.Public(), csigner.Signer) + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, signer.Public(), csigner) if err != nil { return nil, fmt.Errorf("error creating X.509 certificate: %w", err) } certBlock := &pem.Block{Type: "CERTIFICATE", Bytes: certDER} return pem.EncodeToMemory(certBlock), nil } - -// VerifyEntryPayload checks that the TL entry payload matches envelope payload. -func (tl *RekorTL) VerifyEntryPayload(entryBytes, payload, publicKey []byte) error { - entry, err := tl.UnmarshalEntry(entryBytes) - if err != nil { - return fmt.Errorf("error failed to unmarshal TL entry: %w", err) - } - le, ok := entry.(*models.LogEntryAnon) - if !ok { - return fmt.Errorf("expected tl entry to be of type *models.LogEntryAnon, got %T", entry) - } - tlBody, ok := le.Body.(string) - if !ok { - return fmt.Errorf("expected tl body to be of type string, got %T", entry) - } - rekord, err := extractHashedRekord(tlBody) - if err != nil { - return fmt.Errorf("error extract HashedRekord from TL entry: %w", err) - } - - // compare payload hashes - payloadHash := util.SHA256Hex(payload) - if rekord.Hash != payloadHash { - return fmt.Errorf("error payload and tl entry hash mismatch") - } - - // compare public keys - cert, err := base64.StdEncoding.Strict().DecodeString(rekord.PublicKey) - if err != nil { - return fmt.Errorf("failed to decode public key: %w", err) - } - p, _ := pem.Decode(cert) - result, err := x509.ParseCertificate(p.Bytes) - if err != nil { - return fmt.Errorf("failed to parse certificate: %w", err) - } - if !bytes.Equal(result.RawSubjectPublicKeyInfo, publicKey) { - return fmt.Errorf("error payload and tl entry public key mismatch") - } - return nil -} - -func (tl *RekorTL) UnmarshalEntry(entry []byte) (any, error) { - le := new(models.LogEntryAnon) - err := le.UnmarshalBinary(entry) - if err != nil { - return nil, fmt.Errorf("error failed to unmarshal TL entry: %w", err) - } - return le, nil -} - -func extractHashedRekord(body string) (*TLPayload, error) { - sig := new(TLPayload) - pe, err := models.UnmarshalProposedEntry(base64.NewDecoder(base64.StdEncoding, strings.NewReader(body)), runtime.JSONConsumer()) - if err != nil { - return nil, err - } - impl, err := types.UnmarshalEntry(pe) - if err != nil { - return nil, err - } - switch entry := impl.(type) { - case *hashedrekord_v001.V001Entry: - sig.Algorithm = *entry.HashedRekordObj.Data.Hash.Algorithm - sig.Hash = *entry.HashedRekordObj.Data.Hash.Value - sig.Signature = entry.HashedRekordObj.Signature.Content.String() - sig.PublicKey = entry.HashedRekordObj.Signature.PublicKey.Content.String() - return sig, nil - default: - return nil, fmt.Errorf("failed to extract haskedrekord, unsupported type: %T", entry) - } -} diff --git a/tlog/tl_test.go b/tlog/tl_test.go index 0ec7813..15f9412 100644 --- a/tlog/tl_test.go +++ b/tlog/tl_test.go @@ -52,42 +52,31 @@ func TestUploadAndVerifyLogEntry(t *testing.T) { sig, err := signer.Sign(context.Background(), hash) assert.NoError(t, err) - var tl TL + var tl TransparencyLog if UseMockTL { - tl = &MockTL{ - UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) ([]byte, error) { - return []byte(TestEntry), nil + tl = &MockTransparencyLog{ + UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) (*DockerTLExtension, error) { + return &DockerTLExtension{ + Kind: RekorTLExtKind, + Data: []byte(TestEntry), + }, nil }, - VerifyLogEntryFunc: func(_ context.Context, _ []byte) (time.Time, error) { + VerifyLogEntryFunc: func(_ context.Context, _ *DockerTLExtension, _, _ []byte) (time.Time, error) { return time.Time{}, nil }, - VerifyEntryPayloadFunc: func(_, _, _ []byte) error { - return nil - }, } } else { - tl = &RekorTL{} + assert.NoError(t, err) } // test upload log entry - ctx := WithTL(context.Background(), tl) - entry, err := tl.UploadLogEntry(ctx, "test", payload, sig, signer) + ctx := context.Background() + entry, err := tl.UploadEntry(ctx, "test", payload, sig, signer) assert.NoError(t, err) - // test verify log entry - _, err = tl.VerifyLogEntry(ctx, entry) - assert.NoError(t, err) - - // verify TL entry payload + // verify TL entry ecPub, err := x509.MarshalPKIXPublicKey(signer.Public()) assert.NoError(t, err) - err = tl.VerifyEntryPayload(entry, payload, ecPub) - assert.NoError(t, err) -} - -func TestVerifyEntryPayload(t *testing.T) { - tl := &RekorTL{} - p, _ := pem.Decode([]byte(TestPublicKey)) - err := tl.VerifyEntryPayload([]byte(TestEntry), []byte(TestPayload), p.Bytes) + _, err = tl.VerifyEntry(ctx, entry, payload, ecPub) assert.NoError(t, err) } diff --git a/verify.go b/verify.go index 873953c..1c6b745 100644 --- a/verify.go +++ b/verify.go @@ -17,16 +17,13 @@ import ( intoto "github.com/in-toto/in-toto-golang/in_toto" ) -type Verifier interface { - Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) +type ImageVerifier struct { + opts *policy.Options + tufClient tuf.Downloader + attestationVerifier attestation.Verifier } -type tufVerifier struct { - opts *policy.Options - tufClient tuf.Downloader -} - -func NewVerifier(ctx context.Context, opts *policy.Options) (Verifier, error) { +func NewImageVerifier(ctx context.Context, opts *policy.Options) (*ImageVerifier, error) { err := populateDefaultOptions(opts) if err != nil { return nil, err @@ -38,13 +35,21 @@ func NewVerifier(ctx context.Context, opts *policy.Options) (Verifier, error) { return nil, fmt.Errorf("failed to create TUF client: %w", err) } } - return &tufVerifier{ - opts: opts, - tufClient: tufClient, + attestationVerifier := opts.AttestationVerifier + if attestationVerifier == nil { + attestationVerifier, err = attestation.NewVerfier(attestation.WithTUFDownloader(tufClient)) + if err != nil { + return nil, fmt.Errorf("failed to create attestation verifier: %w", err) + } + } + return &ImageVerifier{ + opts: opts, + tufClient: tufClient, + attestationVerifier: attestationVerifier, }, nil } -func (verifier *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) { +func (verifier *ImageVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) { // so that we can resolve mapping from the image name earlier detailsResolver, err := policy.CreateImageDetailsResolver(src) if err != nil { @@ -82,7 +87,7 @@ func (verifier *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (re if err != nil { return nil, fmt.Errorf("failed to create attestation resolver: %w", err) } - evaluator := policy.NewRegoEvaluator(verifier.opts.Debug) + evaluator := policy.NewRegoEvaluator(verifier.opts.Debug, verifier.attestationVerifier) result, err = VerifyAttestations(ctx, resolver, evaluator, resolvedPolicy) if err != nil { return nil, fmt.Errorf("failed to evaluate policy: %w", err) @@ -91,7 +96,7 @@ func (verifier *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (re } func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.Options) (result *VerificationResult, err error) { - verifier, err := NewVerifier(ctx, opts) + verifier, err := NewImageVerifier(ctx, opts) if err != nil { return nil, err } diff --git a/verify_test.go b/verify_test.go index a13d61b..d9dd880 100644 --- a/verify_test.go +++ b/verify_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/distribution/reference" "github.com/docker/attest/attestation" @@ -14,8 +15,10 @@ import ( "github.com/docker/attest/internal/test" "github.com/docker/attest/oci" "github.com/docker/attest/policy" + "github.com/docker/attest/tlog" "github.com/docker/attest/tuf" intoto "github.com/in-toto/in-toto-golang/in_toto" + "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" @@ -74,7 +77,9 @@ func TestVSA(t *testing.T) { // setup an image with signed attestations outputLayout := test.CreateTempDir(t, "", TestTempDir) - opts := &attestation.SigningOptions{} + opts := &attestation.SigningOptions{ + TransparencyLog: tlog.GetMockTL(), + } attIdx, err := oci.IndexFromPath(test.UnsignedTestImage()) assert.NoError(t, err) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) @@ -118,7 +123,8 @@ func TestVSA(t *testing.T) { assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels) assert.Equal(t, PassPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation) assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI) - assert.Equal(t, map[string]string{"sha256": "d71d6b8f49fcba1295b16f5394dd5863a14e4277eb663d66d8c48e392509afe0"}, attestationPredicate.Policy.Digest) + // this is the digest of the policy file + assert.Equal(t, map[string]string{"sha256": "ae71defe3b9ecebdf4f939a396b68884d0cba3c2c9d78ce5e64146d9487b0ade"}, attestationPredicate.Policy.Digest) } func TestVerificationFailure(t *testing.T) { @@ -126,7 +132,9 @@ func TestVerificationFailure(t *testing.T) { // setup an image with signed attestations outputLayout := test.CreateTempDir(t, "", TestTempDir) - opts := &attestation.SigningOptions{} + opts := &attestation.SigningOptions{ + TransparencyLog: tlog.GetMockTL(), + } attIdx, err := oci.IndexFromPath(test.UnsignedTestImage()) assert.NoError(t, err) signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) @@ -170,7 +178,7 @@ func TestVerificationFailure(t *testing.T) { assert.Equal(t, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels) assert.Equal(t, FailPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation) assert.Equal(t, "https://docker.com/official/policy/v0.1", attestationPredicate.Policy.URI) - assert.Equal(t, map[string]string{"sha256": "ad045e1bd7cd602d90196acf68f2c57d7b51565d59e6e30e30d94ae86aa16201"}, attestationPredicate.Policy.Digest) + assert.Equal(t, map[string]string{"sha256": "4345a4f5db3ce02664bd83f8e4aad03bd9a26d4edb334338c762d9648e16bed1"}, attestationPredicate.Policy.Digest) } func TestSignVerify(t *testing.T) { @@ -178,7 +186,7 @@ func TestSignVerify(t *testing.T) { // setup an image with signed attestations outputLayout := test.CreateTempDir(t, "", TestTempDir) - keys, err := test.GenKeyMetadata(signer) + keys, err := GenKeyMetadata(signer) require.NoError(t, err) config := struct { Keys []*attestation.KeyMetadata `json:"keys"` @@ -210,11 +218,11 @@ func TestSignVerify(t *testing.T) { attIdx, err := oci.IndexFromPath(test.UnsignedTestImage()) assert.NoError(t, err) - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - opts := &attestation.SigningOptions{ - SkipTL: !tc.signTL, + opts := &attestation.SigningOptions{} + if tc.signTL { + opts.TransparencyLog = tlog.GetMockTL() } signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) @@ -329,3 +337,24 @@ func TestDefaultOptions(t *testing.T) { }) } } + +// LoadKeyMetadata loads the key metadata for the given signer verifier. +func GenKeyMetadata(sv dsse.SignerVerifier) (*attestation.KeyMetadata, error) { + pub := sv.Public() + pem, err := test.PublicKeyToPEM(pub) + if err != nil { + return nil, fmt.Errorf("failed to convert public key to PEM: %w", err) + } + id, err := sv.KeyID() + if err != nil { + return nil, err + } + + return &attestation.KeyMetadata{ + ID: id, + Status: "active", + SigningFormat: "dssev1", + From: time.Now(), + PEM: pem, + }, nil +}