Use a Factory to create signature verifiers at policy evaluation time (#165)
* Make verifiers composable * fix: remove unused code and improve signature verification logic * fix: simplify abstractions and renamed some things * fix: improve tl interface. * fix: sort out signer/verifier
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
143
attestation/verifier.go
Normal file
143
attestation/verifier.go
Normal file
@@ -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
|
||||
}
|
||||
55
attestation/verifier_test.go
Normal file
55
attestation/verifier_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user