Handle errors from Go in Rego. Support for skipping TL (#47)
* Make TL logging/verification optional * Return errors from go-lang fns * Update pkg/policy/rego.go Co-authored-by: Jonny Stoten <jonny@jonnystoten.com> * Update pkg/attestation/sign.go Co-authored-by: Joel Kamp <joel.kamp@docker.com> * Move public key marshelling until later * Simplify logSignature and pass down opts --------- Co-authored-by: Jonny Stoten <jonny@jonnystoten.com> Co-authored-by: Joel Kamp <joel.kamp@docker.com>
This commit is contained in:
@@ -6,13 +6,16 @@ import (
|
||||
|
||||
"github.com/docker/attest/internal/util"
|
||||
"github.com/docker/attest/pkg/tlog"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
// SignDSSE signs a payload with a given signer and uploads the signature to the transparency log
|
||||
func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer dsse.SignerVerifier) (*Envelope, error) {
|
||||
t := tlog.GetTL(ctx)
|
||||
|
||||
func SignDSSE(ctx context.Context, payload []byte, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
|
||||
payloadType := opts.PayloadType
|
||||
if payloadType == "" {
|
||||
payloadType = intoto.PayloadType
|
||||
}
|
||||
env := new(Envelope)
|
||||
env.Payload = base64Encoding.EncodeToString(payload)
|
||||
env.PayloadType = payloadType
|
||||
@@ -33,8 +36,31 @@ func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer ds
|
||||
return nil, fmt.Errorf("error getting public key ID: %w", err)
|
||||
}
|
||||
|
||||
// upload to TL
|
||||
entry, err := t.UploadLogEntry(ctx, keyId, encPayload, sig, signer)
|
||||
dsseSig := Signature{
|
||||
KeyID: keyId,
|
||||
Sig: base64Encoding.EncodeToString(sig),
|
||||
}
|
||||
if !opts.SkipTL {
|
||||
ext, err := logSignature(ctx, tlog.GetTL(ctx), &sig, &encPayload, signer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to log to rekor: %w", err)
|
||||
}
|
||||
dsseSig.Extension = *ext
|
||||
}
|
||||
// add signature to dsse envelope
|
||||
env.Signatures = []Signature{dsseSig}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error uploading TL entry: %w", err)
|
||||
}
|
||||
@@ -42,21 +68,13 @@ func SignDSSE(ctx context.Context, payload []byte, payloadType string, signer ds
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling tl entry: %w", err)
|
||||
}
|
||||
|
||||
// add signature w/ tl extension to dsse envelope
|
||||
env.Signatures = append(env.Signatures, Signature{
|
||||
KeyID: keyId,
|
||||
Sig: base64Encoding.EncodeToString(sig),
|
||||
Extension: Extension{
|
||||
Kind: DockerDsseExtKind,
|
||||
Ext: DockerDsseExtension{
|
||||
Tl: DockerTlExtension{
|
||||
Kind: RekorTlExtKind,
|
||||
Data: entryObj, // transparency log entry metadata
|
||||
},
|
||||
return &Extension{
|
||||
Kind: DockerDsseExtKind,
|
||||
Ext: DockerDsseExtension{
|
||||
Tl: DockerTlExtension{
|
||||
Kind: RekorTlExtKind,
|
||||
Data: entryObj, // transparency log entry metadata
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return env, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSignVerifyAttestation(t *testing.T) {
|
||||
@@ -27,17 +28,17 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(stmt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
opts := &attestation.SigningOptions{}
|
||||
env, err := attestation.SignDSSE(ctx, payload, signer, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// marshal envelope to json to test for bugs when marshaling envelope data
|
||||
serializedEnv, err := json.Marshal(env)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
deserializedEnv := new(attestation.Envelope)
|
||||
err = json.Unmarshal(serializedEnv, deserializedEnv)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// signer.Public() calls AWS API when using AWS signer, use attestation.GetPublicVerificationKey() to get key from TUF repo
|
||||
// signer.Public() used here for test purposes
|
||||
@@ -49,10 +50,10 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
badKeyPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
badKey := &badKeyPriv.PublicKey
|
||||
badPEM, err := signerverifier.ToPEM(badKey)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -136,7 +137,10 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
To: tc.to,
|
||||
Status: tc.status,
|
||||
}
|
||||
_, err = attestation.VerifyDSSE(ctx, deserializedEnv, attestation.KeysMap{tc.keyId: keyMeta})
|
||||
opts := &attestation.VerifyOptions{
|
||||
Keys: attestation.Keys{keyMeta},
|
||||
}
|
||||
_, err = attestation.VerifyDSSE(ctx, deserializedEnv, opts)
|
||||
if tc.expectedError != "" {
|
||||
assert.Contains(t, err.Error(), tc.expectedError)
|
||||
} else {
|
||||
|
||||
@@ -66,6 +66,17 @@ type DockerTlExtension struct {
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type VerifyOptions struct {
|
||||
Keys []KeyMetadata `json:"keys"`
|
||||
SkipTL bool `json:"skip_tl"`
|
||||
}
|
||||
|
||||
type SigningOptions struct {
|
||||
Replace bool
|
||||
SkipTL bool
|
||||
PayloadType string
|
||||
}
|
||||
|
||||
func DSSEMediaType(predicateType string) (string, error) {
|
||||
var predicateName string
|
||||
switch predicateType {
|
||||
|
||||
@@ -30,7 +30,7 @@ type KeyMetadata struct {
|
||||
type Keys []KeyMetadata
|
||||
type KeysMap map[string]KeyMetadata
|
||||
|
||||
func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error) {
|
||||
func VerifyDSSE(ctx context.Context, env *Envelope, opts *VerifyOptions) ([]byte, error) {
|
||||
// enforce payload type
|
||||
if !ValidPayloadType(env.PayloadType) {
|
||||
return nil, fmt.Errorf("unsupported payload type %s", env.PayloadType)
|
||||
@@ -49,7 +49,7 @@ func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error
|
||||
|
||||
// verify signatures and transparency log entry
|
||||
for _, sig := range env.Signatures {
|
||||
err := verifySignature(ctx, sig, encPayload, keys)
|
||||
err := verifySignature(ctx, sig, encPayload, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -58,31 +58,11 @@ func VerifyDSSE(ctx context.Context, env *Envelope, keys KeysMap) ([]byte, error
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func verifySignature(ctx context.Context, sig Signature, payload []byte, keys KeysMap) error {
|
||||
t := tlog.GetTL(ctx)
|
||||
|
||||
if sig.Extension.Kind == "" {
|
||||
return fmt.Errorf("error missing signature extension kind")
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
keyMeta, ok := keys[sig.KeyID]
|
||||
if !ok {
|
||||
return fmt.Errorf("error key not found: %s", sig.KeyID)
|
||||
@@ -91,30 +71,53 @@ func verifySignature(ctx context.Context, sig Signature, payload []byte, keys Ke
|
||||
if keyMeta.Distrust {
|
||||
return fmt.Errorf("key %s is distrusted", keyMeta.ID)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// TODO: this is unmarshalling with MarshalPKIXPublicKey only for us to marshal it again
|
||||
publicKey, err := signerverifier.Parse([]byte(keyMeta.PEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse public key: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !opts.SkipTL {
|
||||
t := tlog.GetTL(ctx)
|
||||
|
||||
if sig.Extension.Kind == "" {
|
||||
return fmt.Errorf("error missing signature extension kind")
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -39,8 +39,11 @@ func TestVerifyUnsignedAttestation(t *testing.T) {
|
||||
Payload: base64.StdEncoding.EncodeToString(payload),
|
||||
PayloadType: intoto.PayloadType,
|
||||
}
|
||||
opts := &attestation.VerifyOptions{
|
||||
Keys: attestation.Keys{},
|
||||
}
|
||||
|
||||
_, err := attestation.VerifyDSSE(ctx, env, attestation.KeysMap{})
|
||||
_, err := attestation.VerifyDSSE(ctx, env, opts)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no signatures")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user