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:
James Carnegie
2024-09-18 13:34:10 +01:00
committed by GitHub
parent 5335a56da1
commit 05caa959c4
32 changed files with 892 additions and 535 deletions

View File

@@ -26,7 +26,7 @@ func ExampleManifest() {
// configure signing options // configure signing options
opts := &attestation.SigningOptions{ 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" ref := "docker/image-signer-verifier:latest"

View File

@@ -88,9 +88,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
u, err := url.Parse(s.URL) u, err := url.Parse(s.URL)
require.NoError(t, err) require.NoError(t, err)
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{}
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage("..")) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err) require.NoError(t, err)
@@ -210,9 +208,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
refServerURL, err := url.Parse(refServer.URL) refServerURL, err := url.Parse(refServer.URL)
require.NoError(t, err) require.NoError(t, err)
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{}
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage("..")) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err) require.NoError(t, err)
@@ -236,9 +232,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
refServerURL, err := url.Parse(refServer.URL) refServerURL, err := url.Parse(refServer.URL)
require.NoError(t, err) require.NoError(t, err)
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{}
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage("..")) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err) require.NoError(t, err)
@@ -291,9 +285,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
repoName := "repo" repoName := "repo"
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{}
SkipTL: true,
}
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage("..")) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
require.NoError(t, err) require.NoError(t, err)

View File

@@ -37,10 +37,10 @@ func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, o
KeyID: keyID, KeyID: keyID,
Sig: base64Encoding.EncodeToString(sig), Sig: base64Encoding.EncodeToString(sig),
} }
if !opts.SkipTL { if opts.TransparencyLog != nil {
ext, err := logSignature(ctx, tlog.GetTL(ctx), &sig, &encPayload, signer) ext, err := logSignature(ctx, opts.TransparencyLog, sig, encPayload, signer)
if err != nil { 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 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. // 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 // get Key ID from signer
keyID, err := signer.KeyID() keyID, err := signer.KeyID()
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting public key ID: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("error uploading TL entry: %w", err) 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{ return &Extension{
Kind: DockerDSSEExtKind, Kind: DockerDSSEExtKind,
Ext: &DockerDSSEExtension{ Ext: &DockerDSSEExtension{
TL: &DockerTLExtension{ TL: entry,
Kind: RekorTLExtKind,
Data: entryObj, // transparency log entry metadata
},
}, },
}, nil }, nil
} }

View File

@@ -1,6 +1,7 @@
package attestation_test package attestation_test
import ( import (
"context"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
@@ -14,6 +15,7 @@ import (
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"github.com/docker/attest/oci" "github.com/docker/attest/oci"
"github.com/docker/attest/signerverifier" "github.com/docker/attest/signerverifier"
"github.com/docker/attest/tlog"
"github.com/google/go-containerregistry/pkg/registry" "github.com/google/go-containerregistry/pkg/registry"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/static" "github.com/google/go-containerregistry/pkg/v1/static"
@@ -35,7 +37,10 @@ func TestSignVerifyAttestation(t *testing.T) {
payload, err := json.Marshal(stmt) payload, err := json.Marshal(stmt)
require.NoError(t, err) require.NoError(t, err)
opts := &attestation.SigningOptions{} tl := tlog.GetMockTL()
opts := &attestation.SigningOptions{
TransparencyLog: tl,
}
env, err := attestation.SignDSSE(ctx, payload, signer, opts) env, err := attestation.SignDSSE(ctx, payload, signer, opts)
require.NoError(t, err) require.NoError(t, err)
@@ -146,8 +151,17 @@ func TestSignVerifyAttestation(t *testing.T) {
opts := &attestation.VerifyOptions{ opts := &attestation.VerifyOptions{
Keys: attestation.Keys{keyMeta}, 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 != "" { if tc.expectedError != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError) assert.Contains(t, err.Error(), tc.expectedError)
} else { } else {
assert.NoError(t, err) assert.NoError(t, err)
@@ -222,7 +236,6 @@ func TestSimpleStatementSigning(t *testing.T) {
{"replaced", true}, {"replaced", true},
{"not replaced", false}, {"not replaced", false},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
opts := &attestation.SigningOptions{} opts := &attestation.SigningOptions{}

View File

@@ -1,9 +1,12 @@
package attestation package attestation
import ( import (
"crypto"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"time"
"github.com/docker/attest/tlog"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" 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" InTotoPredicateType = "in-toto.io/predicate-type"
DockerReferenceDigest = "vnd.docker.reference.digest" DockerReferenceDigest = "vnd.docker.reference.digest"
DockerDSSEExtKind = "application/vnd.docker.attestation-verification.v1+json" DockerDSSEExtKind = "application/vnd.docker.attestation-verification.v1+json"
RekorTLExtKind = "Rekor"
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse" OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage" InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage"
LifecycleStageExperimental = "experimental" LifecycleStageExperimental = "experimental"
@@ -72,22 +74,40 @@ type AnnotatedStatement struct {
} }
type DockerDSSEExtension struct { type DockerDSSEExtension struct {
TL *DockerTLExtension `json:"tl"` TL *tlog.DockerTLExtension `json:"tl"`
} }
type DockerTLExtension struct { type TransparencyLogKind string
Kind string `json:"kind"`
Data any `json:"data"` const (
} RekorTransparencyLogKind = "rekor"
)
type VerifyOptions struct { type VerifyOptions struct {
Keys []*KeyMetadata `json:"keys"` Keys []*KeyMetadata `json:"keys"`
SkipTL bool `json:"skip_tl"` 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 { type SigningOptions struct {
// don't log to the configured transparency log // set this in order to log to a transparency log
SkipTL bool TransparencyLog tlog.TransparencyLog
} }
type Options struct { type Options struct {

143
attestation/verifier.go Normal file
View 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
}

View 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)
}
})
}
}

View File

@@ -2,37 +2,17 @@ package attestation
import ( import (
"context" "context"
"crypto/ecdsa" "crypto"
"crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"time"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/signerverifier" "github.com/docker/attest/signerverifier"
"github.com/docker/attest/tlog"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/secure-systems-lab/go-securesystemslib/dsse"
) )
type KeyMetadata struct { func VerifyDSSE(ctx context.Context, verifier Verifier, env *Envelope, opts *VerifyOptions) ([]byte, error) {
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) {
// enforce payload type // enforce payload type
if !ValidPayloadType(env.PayloadType) { if !ValidPayloadType(env.PayloadType) {
return nil, fmt.Errorf("unsupported payload type %s", 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") 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) payload, err := base64Encoding.DecodeString(env.Payload)
if err != nil { if err != nil {
return nil, fmt.Errorf("error failed to decode payload: %w", err) return nil, fmt.Errorf("error failed to decode payload: %w", err)
} }
encPayload := dsse.PAE(env.PayloadType, payload) encPayload := dsse.PAE(env.PayloadType, payload)
// verify signatures and transparency log entry // verify signatures and transparency log entry
for _, sig := range env.Signatures { 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 { 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 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 { func ValidPayloadType(payloadType string) bool {
return payloadType == intoto.PayloadType || payloadType == ociv1.MediaTypeDescriptor 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
}

View File

@@ -42,8 +42,7 @@ func TestVerifyUnsignedAttestation(t *testing.T) {
opts := &attestation.VerifyOptions{ opts := &attestation.VerifyOptions{
Keys: attestation.Keys{}, Keys: attestation.Keys{},
} }
_, err := attestation.VerifyDSSE(ctx, nil, env, opts)
_, err := attestation.VerifyDSSE(ctx, env, opts)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "no signatures") assert.Contains(t, err.Error(), "no signatures")
} }

View File

@@ -7,6 +7,7 @@ import (
"github.com/docker/attest/attestation" "github.com/docker/attest/attestation"
"github.com/docker/attest/oci" "github.com/docker/attest/oci"
"github.com/docker/attest/signerverifier" "github.com/docker/attest/signerverifier"
"github.com/docker/attest/tlog"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate" "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) // signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region)
// configure signing options // 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{ 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 // load image index with unsigned attestation-manifests

2
go.mod
View File

@@ -18,6 +18,7 @@ require (
github.com/secure-systems-lab/go-securesystemslib v0.8.0 github.com/secure-systems-lab/go-securesystemslib v0.8.0
github.com/sigstore/cosign/v2 v2.4.0 github.com/sigstore/cosign/v2 v2.4.0
github.com/sigstore/rekor v1.3.6 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/aws v1.8.9
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.9 github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.9
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
@@ -126,7 +127,6 @@ require (
github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sigstore/protobuf-specs v0.3.2 // 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/sigstore/timestamp-authority v1.2.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect

View File

@@ -7,24 +7,22 @@ import (
_ "embed" _ "embed"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io"
"log"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"github.com/docker/attest/attestation"
"github.com/docker/attest/signerverifier" "github.com/docker/attest/signerverifier"
"github.com/docker/attest/tlog"
"github.com/docker/attest/useragent" "github.com/docker/attest/useragent"
"github.com/google/go-containerregistry/pkg/registry" "github.com/google/go-containerregistry/pkg/registry"
"github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/secure-systems-lab/go-securesystemslib/dsse"
) )
const ( const (
UseMockTL = true
UseMockKMS = true UseMockKMS = true
AWSRegion = "us-east-1" AWSRegion = "us-east-1"
@@ -60,15 +58,7 @@ func GetMockSigner(_ context.Context) (dsse.SignerVerifier, error) {
} }
func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) { func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
var tl tlog.TL ctx := context.Background()
if UseMockTL {
tl = tlog.GetMockTL()
} else {
tl = &tlog.RekorTL{}
}
ctx := tlog.WithTL(context.Background(), tl)
var signer dsse.SignerVerifier var signer dsse.SignerVerifier
var err error var err error
if UseMockKMS { 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 { 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...) regHandler := registry.New(options...)
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check the user agent // 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) derBytes, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil { if err != nil {
return "", err return "", err
@@ -112,24 +103,3 @@ func publicKeyToPEM(pubKey crypto.PublicKey) (string, error) {
return string(pem.EncodeToMemory(pemBlock)), nil 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
}

View File

@@ -39,8 +39,9 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
TestDataPath := filepath.Join("..", "test", "testdata") TestDataPath := filepath.Join("..", "test", "testdata")
ExampleAttestation := filepath.Join(TestDataPath, "example_attestation.json") 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{ defaultResolver := attestation.MockResolver{
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)}, Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
} }

View File

@@ -21,7 +21,8 @@ import (
) )
type regoEvaluator struct { type regoEvaluator struct {
debug bool debug bool
attestationVerifier attestation.Verifier
} }
const ( const (
@@ -29,9 +30,10 @@ const (
resultBinding = "result" resultBinding = "result"
) )
func NewRegoEvaluator(debug bool) Evaluator { func NewRegoEvaluator(debug bool, attestationVerifier attestation.Verifier) Evaluator {
return &regoEvaluator{ return &regoEvaluator{
debug: debug, debug: debug,
attestationVerifier: attestationVerifier,
} }
} }
@@ -86,7 +88,11 @@ func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Reso
rego.Store(store), rego.Store(store),
rego.GenerateJSON(jsonGenerator[Result]()), rego.GenerateJSON(jsonGenerator[Result]()),
) )
for _, custom := range RegoFunctions(resolver) { regoFnOpts := &regoFnOpts{
attestationResolver: resolver,
attestationVerifier: re.attestationVerifier,
}
for _, custom := range RegoFunctions(regoFnOpts) {
regoOpts = append(regoOpts, custom.Func) 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{ return []*tester.Builtin{
{ {
Decl: verifyDecl, Decl: verifyDecl,
@@ -180,7 +186,7 @@ func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin {
Memoize: true, Memoize: true,
Nondeterministic: verifyDecl.Nondeterministic, Nondeterministic: verifyDecl.Nondeterministic,
}, },
handleErrors2(verifyInTotoEnvelope(resolver))), handleErrors2(verifyInTotoEnvelope(regoOpts))),
}, },
{ {
Decl: attestDecl, Decl: attestDecl,
@@ -191,12 +197,12 @@ func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin {
Memoize: true, Memoize: true,
Nondeterministic: attestDecl.Nondeterministic, 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) { return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String) predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
if !ok { if !ok {
@@ -204,7 +210,7 @@ func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 {
} }
predicateType := string(predicateTypeStr) predicateType := string(predicateTypeStr)
envelopes, err := resolver.Attestations(rCtx.Context, predicateType) envelopes, err := regoOpts.attestationResolver.Attestations(rCtx.Context, predicateType)
if err != nil { if err != nil {
return nil, err 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) { return func(rCtx rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
env := new(attestation.Envelope) env := new(attestation.Envelope)
opts := new(attestation.VerifyOptions) opts := new(attestation.VerifyOptions)
@@ -238,8 +249,7 @@ func verifyInTotoEnvelope(resolver attestation.Resolver) rego.Builtin2 {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to cast verifier options: %w", err) return nil, fmt.Errorf("failed to cast verifier options: %w", err)
} }
payload, err := attestation.VerifyDSSE(rCtx.Context, regoOpts.attestationVerifier, env, opts)
payload, err := attestation.VerifyDSSE(rCtx.Context, env, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to verify envelope: %w", err) 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) 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 { if err != nil {
return nil, fmt.Errorf("failed to verify subject: %w", err) return nil, fmt.Errorf("failed to verify subject: %w", err)
} }

View File

@@ -1,6 +1,7 @@
package policy package policy
import ( import (
"github.com/docker/attest/attestation"
"github.com/docker/attest/config" "github.com/docker/attest/config"
"github.com/docker/attest/tuf" "github.com/docker/attest/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
@@ -27,14 +28,15 @@ type Result struct {
} }
type Options struct { type Options struct {
TUFClientOptions *tuf.ClientOptions TUFClientOptions *tuf.ClientOptions
DisableTUF bool DisableTUF bool
LocalTargetsDir string LocalTargetsDir string
LocalPolicyDir string LocalPolicyDir string
PolicyID string PolicyID string
ReferrersRepo string ReferrersRepo string
AttestationStyle config.AttestationStyle AttestationStyle config.AttestationStyle
Debug bool Debug bool
AttestationVerifier attestation.Verifier
} }
type Policy struct { type Policy struct {

View File

@@ -20,8 +20,5 @@ func GetAWSSigner(ctx context.Context, keyARN string, region string) (dsse.Signe
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting aws crypto signer: %w", err) return nil, fmt.Errorf("error getting aws crypto signer: %w", err)
} }
signer := &ECDSA256SignerVerifier{ return NewECDSASignerVerifier(cs)
Signer: cs,
}
return signer, nil
} }

View File

@@ -9,52 +9,17 @@ import (
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io"
"github.com/docker/attest/internal/util"
"github.com/secure-systems-lab/go-securesystemslib/dsse" "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) { func LoadKeyPair(priv []byte) (dsse.SignerVerifier, error) {
privateKey, err := parsePriv(priv) privateKey, err := parsePriv(priv)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ECDSA256SignerVerifier{ return NewECDSASignerVerifier(privateKey)
Signer: privateKey,
}, nil
} }
func parsePriv(privkeyBytes []byte) (*ecdsa.PrivateKey, error) { func parsePriv(privkeyBytes []byte) (*ecdsa.PrivateKey, error) {
@@ -78,7 +43,26 @@ func GenKeyPair() (dsse.SignerVerifier, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ECDSA256SignerVerifier{ return NewECDSASignerVerifier(signer)
Signer: signer, }
}, nil
// 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
} }

80
signerverifier/ecdsa.go Normal file
View File

@@ -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)
}

View File

@@ -21,8 +21,5 @@ func GetGCPSigner(ctx context.Context, reference string, opts ...option.ClientOp
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting gcp crypto signer: %w", err) return nil, fmt.Errorf("error getting gcp crypto signer: %w", err)
} }
signer := &ECDSA256SignerVerifier{ return NewECDSASignerVerifier(cs)
Signer: cs,
}
return signer, nil
} }

View File

@@ -40,6 +40,14 @@ func TestGCPKMS_Signer(t *testing.T) {
publicKey, err := ParsePublicKey([]byte(publicKeyPEM)) publicKey, err := ParsePublicKey([]byte(publicKeyPEM))
require.NoError(t, err) require.NoError(t, err)
// verify payload ecdsa signature // 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) assert.True(t, ok)
err = signer.Verify(ctx, msg, sig)
require.NoError(t, err)
} }

View File

@@ -1,6 +1,7 @@
package signerverifier package signerverifier
import ( import (
"crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
@@ -9,7 +10,7 @@ import (
const pemType = "PUBLIC KEY" const pemType = "PUBLIC KEY"
func ParsePublicKey(pubkeyBytes []byte) (*ecdsa.PublicKey, error) { func ParsePublicKey(pubkeyBytes []byte) (crypto.PublicKey, error) {
p, _ := pem.Decode(pubkeyBytes) p, _ := pem.Decode(pubkeyBytes)
if p == nil { if p == nil {
return nil, fmt.Errorf("pubkey file does not contain any PEM data") 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 { if p.Type != pemType {
return nil, fmt.Errorf("pubkey file does not contain a public key") return nil, fmt.Errorf("pubkey file does not contain a public key")
} }
pubKey, err := x509.ParsePKIXPublicKey(p.Bytes) return x509.ParsePKIXPublicKey(p.Bytes)
if err != nil { }
return nil, fmt.Errorf("error failed to parse public key: %w", err)
}
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 { if !ok {
return nil, fmt.Errorf("error public key is not an ecdsa key: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("error failed to marshal public key: %w", err) return nil, fmt.Errorf("error failed to marshal public key: %w", err)
} }
return pem.EncodeToMemory(&pem.Block{Type: pemType, Bytes: pubKeyBytes}), nil return pem.EncodeToMemory(&pem.Block{Type: pemType, Bytes: pubKeyBytes}), nil
} }

View File

@@ -23,7 +23,7 @@ atts := union({
provs("https://spdx.dev/Document"), provs("https://spdx.dev/Document"),
}) })
opts := {"keys": keys} opts := {"keys": keys, "skip_tl": true}
statements contains s if { statements contains s if {
some att in atts some att in atts

View File

@@ -23,7 +23,7 @@ atts := union({
provs("https://spdx.dev/Document"), provs("https://spdx.dev/Document"),
}) })
opts := {"keys": keys} opts := {"keys": keys, "skip_tl": true}
statements contains s if { statements contains s if {
some att in atts some att in atts

View File

@@ -22,7 +22,7 @@ atts := union({
provs("https://spdx.dev/Document"), provs("https://spdx.dev/Document"),
}) })
opts := {"keys": keys} opts := {"keys": keys, "skip_tl": false}
statements contains s if { statements contains s if {
some att in atts some att in atts

View File

@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr
kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==
-----END PUBLIC KEY-----

View File

@@ -2,6 +2,7 @@ package tlog
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"time" "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="}}` 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 { func GetMockTL() TransparencyLog {
unmarshalEntry := func(entry []byte) (*models.LogEntryAnon, error) { return &MockTransparencyLog{
le := new(models.LogEntryAnon) UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) (*DockerTLExtension, error) {
err := le.UnmarshalBinary(entry) return &DockerTLExtension{
if err != nil { Kind: "Mock",
return nil, fmt.Errorf("error failed to unmarshal TL entry: %w", err) Data: json.RawMessage(TestEntry),
} }, nil
return le, nil
}
return &MockTL{
UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) ([]byte, error) {
return []byte(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 // 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 { 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.Time{}, fmt.Errorf("error missing integrated time in TL entry")
} }
return time.Unix(*le.IntegratedTime, 0), nil return time.Unix(*entry.IntegratedTime, 0), nil
},
VerifyEntryPayloadFunc: func(_, _, _ []byte) error {
return nil
},
UnmarshalEntryFunc: func(entry []byte) (any, error) {
return unmarshalEntry(entry)
}, },
} }
} }
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
}

229
tlog/rekor.go Normal file
View File

@@ -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)
}
}

64
tlog/rekor_test.go Normal file
View File

@@ -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)
})
}
}

View File

@@ -1,162 +1,38 @@
package tlog package tlog
import ( import (
"bytes"
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/base64"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"math/big" "math/big"
"strings"
"time" "time"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/signerverifier" "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/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 ( const (
DefaultRekorURL = "https://rekor.sigstore.dev" DefaultRekorURL = "https://rekor.sigstore.dev"
) )
type tlCtxKeyType struct{} type TransparencyLog interface {
UploadEntry(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) (*DockerTLExtension, error)
var TLCtxKey tlCtxKeyType VerifyEntry(ctx context.Context, entry *DockerTLExtension, payload, publicKey []byte) (time.Time, error)
// sets TL in context.
func WithTL(ctx context.Context, tl TL) context.Context {
return context.WithValue(ctx, TLCtxKey, tl)
} }
// gets TL from context, defaults to Rekor TL if not set. type Payload struct {
func GetTL(ctx context.Context) TL {
t, ok := ctx.Value(TLCtxKey).(TL)
if !ok {
t = &RekorTL{}
}
return t
}
type TLPayload struct {
Algorithm string Algorithm string
Hash string Hash string
Signature string Signature string
PublicKey string PublicKey string
} }
type TL interface { type DockerTLExtension struct {
UploadLogEntry(ctx context.Context, subject string, payload, signature []byte, signer dsse.SignerVerifier) ([]byte, error) Kind string `json:"kind"`
VerifyLogEntry(ctx context.Context, entryBytes []byte) (time.Time, error) Data any `json:"data"`
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
} }
// CreateX509Cert generates a self-signed x509 cert for TL submission. // 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 // dsse.SignerVerifier doesn't implement cypto.Signer exactly
csigner, ok := signer.(*signerverifier.ECDSA256SignerVerifier) csigner, err := signerverifier.AsCryptoSigner(signer)
if !ok { if err != nil {
return nil, fmt.Errorf("expected signer to be of type *signerverifier.ECDSA_SignerVerifier, got %T", signer) return nil, fmt.Errorf("error converting signer to crypto.Signer: %w", err)
} }
// create a self-signed X.509 certificate // 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 { if err != nil {
return nil, fmt.Errorf("error creating X.509 certificate: %w", err) return nil, fmt.Errorf("error creating X.509 certificate: %w", err)
} }
certBlock := &pem.Block{Type: "CERTIFICATE", Bytes: certDER} certBlock := &pem.Block{Type: "CERTIFICATE", Bytes: certDER}
return pem.EncodeToMemory(certBlock), nil 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)
}
}

View File

@@ -52,42 +52,31 @@ func TestUploadAndVerifyLogEntry(t *testing.T) {
sig, err := signer.Sign(context.Background(), hash) sig, err := signer.Sign(context.Background(), hash)
assert.NoError(t, err) assert.NoError(t, err)
var tl TL var tl TransparencyLog
if UseMockTL { if UseMockTL {
tl = &MockTL{ tl = &MockTransparencyLog{
UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) ([]byte, error) { UploadLogEntryFunc: func(_ context.Context, _ string, _ []byte, _ []byte, _ dsse.SignerVerifier) (*DockerTLExtension, error) {
return []byte(TestEntry), nil 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 return time.Time{}, nil
}, },
VerifyEntryPayloadFunc: func(_, _, _ []byte) error {
return nil
},
} }
} else { } else {
tl = &RekorTL{} assert.NoError(t, err)
} }
// test upload log entry // test upload log entry
ctx := WithTL(context.Background(), tl) ctx := context.Background()
entry, err := tl.UploadLogEntry(ctx, "test", payload, sig, signer) entry, err := tl.UploadEntry(ctx, "test", payload, sig, signer)
assert.NoError(t, err) assert.NoError(t, err)
// test verify log entry // verify TL entry
_, err = tl.VerifyLogEntry(ctx, entry)
assert.NoError(t, err)
// verify TL entry payload
ecPub, err := x509.MarshalPKIXPublicKey(signer.Public()) ecPub, err := x509.MarshalPKIXPublicKey(signer.Public())
assert.NoError(t, err) assert.NoError(t, err)
err = tl.VerifyEntryPayload(entry, payload, ecPub) _, err = tl.VerifyEntry(ctx, 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)
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@@ -17,16 +17,13 @@ import (
intoto "github.com/in-toto/in-toto-golang/in_toto" intoto "github.com/in-toto/in-toto-golang/in_toto"
) )
type Verifier interface { type ImageVerifier struct {
Verify(ctx context.Context, src *oci.ImageSpec) (result *VerificationResult, err error) opts *policy.Options
tufClient tuf.Downloader
attestationVerifier attestation.Verifier
} }
type tufVerifier struct { func NewImageVerifier(ctx context.Context, opts *policy.Options) (*ImageVerifier, error) {
opts *policy.Options
tufClient tuf.Downloader
}
func NewVerifier(ctx context.Context, opts *policy.Options) (Verifier, error) {
err := populateDefaultOptions(opts) err := populateDefaultOptions(opts)
if err != nil { if err != nil {
return nil, err 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 nil, fmt.Errorf("failed to create TUF client: %w", err)
} }
} }
return &tufVerifier{ attestationVerifier := opts.AttestationVerifier
opts: opts, if attestationVerifier == nil {
tufClient: tufClient, 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 }, 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 // so that we can resolve mapping from the image name earlier
detailsResolver, err := policy.CreateImageDetailsResolver(src) detailsResolver, err := policy.CreateImageDetailsResolver(src)
if err != nil { if err != nil {
@@ -82,7 +87,7 @@ func (verifier *tufVerifier) Verify(ctx context.Context, src *oci.ImageSpec) (re
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create attestation resolver: %w", err) 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) result, err = VerifyAttestations(ctx, resolver, evaluator, resolvedPolicy)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to evaluate policy: %w", err) 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -7,6 +7,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/attest/attestation" "github.com/docker/attest/attestation"
@@ -14,8 +15,10 @@ import (
"github.com/docker/attest/internal/test" "github.com/docker/attest/internal/test"
"github.com/docker/attest/oci" "github.com/docker/attest/oci"
"github.com/docker/attest/policy" "github.com/docker/attest/policy"
"github.com/docker/attest/tlog"
"github.com/docker/attest/tuf" "github.com/docker/attest/tuf"
intoto "github.com/in-toto/in-toto-golang/in_toto" 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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
@@ -74,7 +77,9 @@ func TestVSA(t *testing.T) {
// setup an image with signed attestations // setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir) outputLayout := test.CreateTempDir(t, "", TestTempDir)
opts := &attestation.SigningOptions{} opts := &attestation.SigningOptions{
TransparencyLog: tlog.GetMockTL(),
}
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage()) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
assert.NoError(t, err) assert.NoError(t, err)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) 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, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
assert.Equal(t, PassPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation) 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, "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) { func TestVerificationFailure(t *testing.T) {
@@ -126,7 +132,9 @@ func TestVerificationFailure(t *testing.T) {
// setup an image with signed attestations // setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir) outputLayout := test.CreateTempDir(t, "", TestTempDir)
opts := &attestation.SigningOptions{} opts := &attestation.SigningOptions{
TransparencyLog: tlog.GetMockTL(),
}
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage()) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
assert.NoError(t, err) assert.NoError(t, err)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) 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, []string{"SLSA_BUILD_LEVEL_3"}, attestationPredicate.VerifiedLevels)
assert.Equal(t, FailPolicyDir+"/policy.rego", attestationPredicate.Policy.DownloadLocation) 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, "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) { func TestSignVerify(t *testing.T) {
@@ -178,7 +186,7 @@ func TestSignVerify(t *testing.T) {
// setup an image with signed attestations // setup an image with signed attestations
outputLayout := test.CreateTempDir(t, "", TestTempDir) outputLayout := test.CreateTempDir(t, "", TestTempDir)
keys, err := test.GenKeyMetadata(signer) keys, err := GenKeyMetadata(signer)
require.NoError(t, err) require.NoError(t, err)
config := struct { config := struct {
Keys []*attestation.KeyMetadata `json:"keys"` Keys []*attestation.KeyMetadata `json:"keys"`
@@ -210,11 +218,11 @@ func TestSignVerify(t *testing.T) {
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage()) attIdx, err := oci.IndexFromPath(test.UnsignedTestImage())
assert.NoError(t, err) assert.NoError(t, err)
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
opts := &attestation.SigningOptions{ opts := &attestation.SigningOptions{}
SkipTL: !tc.signTL, if tc.signTL {
opts.TransparencyLog = tlog.GetMockTL()
} }
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts) 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
}