From 549ea0d7fbec504ead7806608c05c412846ccbab Mon Sep 17 00:00:00 2001 From: Adam Hughes <9903835+tri-adam@users.noreply.github.com> Date: Wed, 26 Oct 2022 22:46:52 +0000 Subject: [PATCH] feat: add DSSE encoder/decoder --- go.mod | 3 + go.sum | 10 +- pkg/integrity/dsse.go | 212 +++++++++++ pkg/integrity/dsse_test.go | 342 ++++++++++++++++++ pkg/integrity/main_test.go | 19 +- pkg/integrity/result.go | 15 +- .../Test_dsseEncoder_signMessage/ECDSA.golden | 1 + .../ED25519.golden | 1 + .../Test_dsseEncoder_signMessage/Multi.golden | 1 + .../Test_dsseEncoder_signMessage/RSA.golden | 1 + .../SHA256.golden | 1 + .../SHA384.golden | 1 + .../SHA512.golden | 1 + 13 files changed, 605 insertions(+), 3 deletions(-) create mode 100644 pkg/integrity/dsse.go create mode 100644 pkg/integrity/dsse_test.go create mode 100644 pkg/integrity/testdata/Test_dsseEncoder_signMessage/ECDSA.golden create mode 100644 pkg/integrity/testdata/Test_dsseEncoder_signMessage/ED25519.golden create mode 100644 pkg/integrity/testdata/Test_dsseEncoder_signMessage/Multi.golden create mode 100644 pkg/integrity/testdata/Test_dsseEncoder_signMessage/RSA.golden create mode 100644 pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA256.golden create mode 100644 pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA384.golden create mode 100644 pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA512.golden diff --git a/go.mod b/go.mod index 73656451..486d00f9 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 github.com/google/uuid v1.3.0 github.com/sebdah/goldie/v2 v2.5.3 + github.com/secure-systems-lab/go-securesystemslib v0.4.0 github.com/sigstore/sigstore v1.4.5 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 @@ -14,8 +15,10 @@ require ( require ( github.com/cloudflare/circl v1.1.0 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-containerregistry v0.12.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/letsencrypt/boulder v0.0.0-20220929215747-76583552c2be // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 // indirect diff --git a/go.sum b/go.sum index 7a887ad6..b66c952f 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -12,11 +13,14 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-containerregistry v0.12.0 h1:nidOEtFYlgPCRqxCKj/4c/js940HVWplCWc5ftdfdUA= +github.com/google/go-containerregistry v0.12.0/go.mod h1:sdIK+oHQO7B93xI8UweYdl887YhuIwg9vz8BSLH3+8k= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/honeycombio/beeline-go v1.10.0 h1:cUDe555oqvw8oD76BQJ8alk7FP0JZ/M/zXpNvOEDLDc= @@ -24,7 +28,7 @@ github.com/honeycombio/libhoney-go v1.16.0 h1:kPpqoz6vbOzgp7jC6SR7SkNj7rua7rgxvz github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4= -github.com/klauspost/compress v1.15.7 h1:7cgTQxJCU/vy+oP/E3B9RGbQTgbiVzIJWIKOLoAsPok= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -33,6 +37,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/letsencrypt/boulder v0.0.0-20220929215747-76583552c2be h1:Cx2bsfM27RBF/45zP1xhFN9FHDxo40LdYdE5L+GWVTw= github.com/letsencrypt/boulder v0.0.0-20220929215747-76583552c2be/go.mod h1:j/WMsOEcTSfy6VR1PkiIo20qH1V9iRRzb7ishoKkN0g= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -43,6 +49,8 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= +github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= diff --git a/pkg/integrity/dsse.go b/pkg/integrity/dsse.go new file mode 100644 index 00000000..c4125579 --- /dev/null +++ b/pkg/integrity/dsse.go @@ -0,0 +1,212 @@ +// Copyright (c) 2022, Sylabs Inc. All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file +// distributed with the sources of this project regarding your rights to use or distribute this +// software. + +package integrity + +import ( + "bytes" + "crypto" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" +) + +var metadataMediaType = "application/vnd.sylabs.sif-metadata+json" + +type dsseEncoder struct { + es *dsse.EnvelopeSigner + h crypto.Hash + payloadType string +} + +// newDSSEEncoder returns an encoder that signs messages in DSSE format according to opts, with key +// material from ss. SHA256 is used as the hash algorithm, unless overridden by opts. +func newDSSEEncoder(ss []signature.Signer, opts ...signature.SignOption) (*dsseEncoder, error) { + var so crypto.SignerOpts + for _, opt := range opts { + opt.ApplyCryptoSignerOpts(&so) + } + + // If SignerOpts not explicitly supplied, set default hash algorithm. + if so == nil { + so = crypto.SHA256 + opts = append(opts, options.WithCryptoSignerOpts(so)) + } + + dss := make([]dsse.SignVerifier, 0, len(ss)) + for _, s := range ss { + ds, err := newDSSESigner(s, opts...) + if err != nil { + return nil, err + } + + dss = append(dss, ds) + } + + es, err := dsse.NewEnvelopeSigner(dss...) + if err != nil { + return nil, err + } + + return &dsseEncoder{ + es: es, + h: so.HashFunc(), + payloadType: metadataMediaType, + }, nil +} + +// signMessage signs the message from r in DSSE format, and writes the result to w. On success, the +// hash function is returned. +func (en *dsseEncoder) signMessage(w io.Writer, r io.Reader) (crypto.Hash, error) { + body, err := io.ReadAll(r) + if err != nil { + return 0, err + } + + e, err := en.es.SignPayload(en.payloadType, body) + if err != nil { + return 0, err + } + + return en.h, json.NewEncoder(w).Encode(e) +} + +type dsseDecoder struct { + vs []signature.Verifier + threshold int + payloadType string +} + +// newDSSEDecoder returns a decoder that verifies messages in DSSE format using key material from +// vs. +func newDSSEDecoder(vs ...signature.Verifier) *dsseDecoder { + return &dsseDecoder{ + vs: vs, + threshold: 1, // Envelope considered verified if at least one verifier succeeds. + payloadType: metadataMediaType, + } +} + +var ( + errDSSEVerifyEnvelopeFailed = errors.New("dsse: verify envelope failed") + errDSSEUnexpectedPayloadType = errors.New("unexpected DSSE payload type") +) + +// verifyMessage reads a message from r, verifies its signature(s), and returns the message +// contents. On success, the accepted public keys are set in vr. +func (de *dsseDecoder) verifyMessage(r io.Reader, h crypto.Hash, vr *VerifyResult) ([]byte, error) { + vs := make([]dsse.Verifier, 0, len(de.vs)) + for _, v := range de.vs { + dv, err := newDSSEVerifier(v, options.WithCryptoSignerOpts(h)) + if err != nil { + return nil, err + } + + vs = append(vs, dv) + } + + v, err := dsse.NewMultiEnvelopeVerifier(de.threshold, vs...) + if err != nil { + return nil, err + } + + var e dsse.Envelope + if err := json.NewDecoder(r).Decode(&e); err != nil { + return nil, err + } + + vr.aks, err = v.Verify(&e) + if err != nil { + return nil, fmt.Errorf("%w: %v", errDSSEVerifyEnvelopeFailed, err) + } + + if e.PayloadType != de.payloadType { + return nil, fmt.Errorf("%w: %v", errDSSEUnexpectedPayloadType, e.PayloadType) + } + + return e.DecodeB64Payload() +} + +type dsseSigner struct { + s signature.Signer + opts []signature.SignOption + pub crypto.PublicKey +} + +// newDSSESigner returns a dsse.SignVerifier that uses s to sign according to opts. +func newDSSESigner(s signature.Signer, opts ...signature.SignOption) (*dsseSigner, error) { + pub, err := s.PublicKey() + if err != nil { + return nil, err + } + + return &dsseSigner{ + s: s, + opts: opts, + pub: pub, + }, nil +} + +// Sign signs the supplied data. +func (s *dsseSigner) Sign(data []byte) ([]byte, error) { + return s.s.SignMessage(bytes.NewReader(data), s.opts...) +} + +var errSignNotImplemented = errors.New("sign not implemented") + +// Verify is not implemented, but required for the dsse.SignVerifier interface. +func (s *dsseSigner) Verify(data, sig []byte) error { + return errSignNotImplemented +} + +// Public returns the public key associated with s. +func (s *dsseSigner) Public() crypto.PublicKey { + return s.pub +} + +// KeyID returns the key ID associated with s. +func (s dsseSigner) KeyID() (string, error) { + return dsse.SHA256KeyID(s.pub) +} + +type dsseVerifier struct { + v signature.Verifier + opts []signature.VerifyOption + pub crypto.PublicKey +} + +// newDSSEVerifier returns a dsse.Verifier that uses v to verify according to opts. +func newDSSEVerifier(v signature.Verifier, opts ...signature.VerifyOption) (*dsseVerifier, error) { + pub, err := v.PublicKey() + if err != nil { + return nil, err + } + + return &dsseVerifier{ + v: v, + opts: opts, + pub: pub, + }, nil +} + +// Verify verifies that sig is a valid signature of data. +func (v *dsseVerifier) Verify(data, sig []byte) error { + return v.v.VerifySignature(bytes.NewReader(sig), bytes.NewReader(data), v.opts...) +} + +// Public returns the public key associated with v. +func (v *dsseVerifier) Public() crypto.PublicKey { + return v.pub +} + +// KeyID returns the key ID associated with v. +func (v *dsseVerifier) KeyID() (string, error) { + return dsse.SHA256KeyID(v.pub) +} diff --git a/pkg/integrity/dsse_test.go b/pkg/integrity/dsse_test.go new file mode 100644 index 00000000..d1585a07 --- /dev/null +++ b/pkg/integrity/dsse_test.go @@ -0,0 +1,342 @@ +// Copyright (c) 2022, Sylabs Inc. All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file +// distributed with the sources of this project regarding your rights to use or distribute this +// software. + +package integrity + +import ( + "bytes" + "crypto" + "encoding/base64" + "encoding/json" + "errors" + "reflect" + "strings" + "testing" + + "github.com/sebdah/goldie/v2" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" +) + +func Test_dsseEncoder_signMessage(t *testing.T) { + ecdsa := getTestSignerVerifier(t, "ecdsa.pem") + ed25519 := getTestSignerVerifier(t, "ed25519.pem") + rsa := getTestSignerVerifier(t, "rsa.pem") + + fakeRand := make([]byte, 1024) + + tests := []struct { + name string + signers []signature.Signer + signOpts []signature.SignOption + wantErr bool + wantHash crypto.Hash + }{ + { + name: "Multi", + signers: []signature.Signer{ecdsa, ed25519, rsa}, + signOpts: []signature.SignOption{ + options.WithRand(bytes.NewReader(fakeRand)), // For deterministic ECDSA signature. + }, + wantHash: crypto.SHA256, + }, + { + name: "ECDSA", + signers: []signature.Signer{ecdsa}, + signOpts: []signature.SignOption{ + options.WithRand(bytes.NewReader(fakeRand)), // For deterministic ECDSA signature. + }, + wantHash: crypto.SHA256, + }, + { + name: "ED25519", + signers: []signature.Signer{ed25519}, + wantHash: crypto.SHA256, + }, + { + name: "RSA", + signers: []signature.Signer{rsa}, + wantHash: crypto.SHA256, + }, + { + name: "SHA256", + signers: []signature.Signer{rsa}, + signOpts: []signature.SignOption{ + options.WithCryptoSignerOpts(crypto.SHA256), + }, + wantHash: crypto.SHA256, + }, + { + name: "SHA384", + signers: []signature.Signer{rsa}, + signOpts: []signature.SignOption{ + options.WithCryptoSignerOpts(crypto.SHA384), + }, + wantHash: crypto.SHA384, + }, + { + name: "SHA512", + signers: []signature.Signer{rsa}, + signOpts: []signature.SignOption{ + options.WithCryptoSignerOpts(crypto.SHA512), + }, + wantHash: crypto.SHA512, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + b := bytes.Buffer{} + + en, err := newDSSEEncoder(tt.signers, tt.signOpts...) + if err != nil { + t.Fatal(err) + } + + ht, err := en.signMessage(&b, strings.NewReader(testMessage)) + if got, want := err, tt.wantErr; (got != nil) != want { + t.Fatalf("got error %v, wantErr %v", got, want) + } + + if err == nil { + if got, want := ht, tt.wantHash; got != want { + t.Errorf("got hash %v, want %v", got, want) + } + + g := goldie.New(t, goldie.WithTestNameForDir(true)) + g.Assert(t, tt.name, b.Bytes()) + } + }) + } +} + +// corruptPayloadType corrupts the payload type of e and re-signs the envelope. The result is a +// cryptographically valid envelope with an unexpected payload types. +func corruptPayloadType(t *testing.T, en *dsseEncoder, e *dsse.Envelope) { + body, err := e.DecodeB64Payload() + if err != nil { + t.Fatal(err) + } + + bad, err := en.es.SignPayload("bad", body) + if err != nil { + t.Fatal(err) + } + + *e = *bad +} + +// corruptPayload corrupts the payload in e. The result is that the signature(s) in e do not match +// the payload. +func corruptPayload(t *testing.T, _ *dsseEncoder, e *dsse.Envelope) { + body, err := e.DecodeB64Payload() + if err != nil { + t.Fatal(err) + } + + e.Payload = base64.StdEncoding.EncodeToString(body[:len(body)-1]) +} + +// corruptSignatures corrupts the signature(s) in e. The result is that the signature(s) in e do +// not match the payload. +func corruptSignatures(t *testing.T, _ *dsseEncoder, e *dsse.Envelope) { + for i, sig := range e.Signatures { + b, err := base64.StdEncoding.DecodeString(sig.Sig) + if err != nil { + t.Fatal(err) + } + + sig.Sig = base64.StdEncoding.EncodeToString(b[:len(b)-1]) + + e.Signatures[i] = sig + } +} + +func Test_dsseDecoder_verifyMessage(t *testing.T) { + ecdsa := getTestSignerVerifier(t, "ecdsa.pem") + ed25519 := getTestSignerVerifier(t, "ed25519.pem") + rsa := getTestSignerVerifier(t, "rsa.pem") + + ecdsaPub, err := ecdsa.PublicKey() + if err != nil { + t.Fatal(err) + } + + ed25519Pub, err := ed25519.PublicKey() + if err != nil { + t.Fatal(err) + } + + rsaPub, err := rsa.PublicKey() + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + signers []signature.Signer + signOpts []signature.SignOption + corrupter func(*testing.T, *dsseEncoder, *dsse.Envelope) + de *dsseDecoder + wantErr error + wantMessage string + wantKeys []crypto.PublicKey + }{ + { + name: "CorruptPayloadType", + signers: []signature.Signer{rsa}, + corrupter: corruptPayloadType, + de: newDSSEDecoder(rsa), + wantErr: errDSSEUnexpectedPayloadType, + wantKeys: []crypto.PublicKey{rsaPub}, + }, + { + name: "CorruptPayload", + signers: []signature.Signer{rsa}, + corrupter: corruptPayload, + de: newDSSEDecoder(rsa), + wantErr: errDSSEVerifyEnvelopeFailed, + wantKeys: []crypto.PublicKey{}, + }, + { + name: "CorruptSignatures", + signers: []signature.Signer{rsa}, + corrupter: corruptSignatures, + de: newDSSEDecoder(rsa), + wantErr: errDSSEVerifyEnvelopeFailed, + wantKeys: []crypto.PublicKey{}, + }, + { + name: "VerifyMulti", + signers: []signature.Signer{ecdsa, ed25519, rsa}, + de: newDSSEDecoder(ecdsa, ed25519, rsa), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{ecdsaPub, ed25519Pub, rsaPub}, + }, + { + name: "ECDSAVerifyMulti", + signers: []signature.Signer{ecdsa, ed25519, rsa}, + de: newDSSEDecoder(ecdsa), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{ecdsaPub}, + }, + { + name: "ED25519VerifyMulti", + signers: []signature.Signer{ecdsa, ed25519, rsa}, + de: newDSSEDecoder(ed25519), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{ed25519Pub}, + }, + { + name: "RSAVerifyMulti", + signers: []signature.Signer{ecdsa, ed25519, rsa}, + de: newDSSEDecoder(rsa), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{rsaPub}, + }, + { + name: "ECDSA", + signers: []signature.Signer{ecdsa}, + de: newDSSEDecoder(ecdsa), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{ecdsaPub}, + }, + { + name: "ED25519", + signers: []signature.Signer{ed25519}, + de: newDSSEDecoder(ed25519), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{ed25519Pub}, + }, + { + name: "RSA", + signers: []signature.Signer{rsa}, + de: newDSSEDecoder(rsa), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{rsaPub}, + }, + { + name: "SHA256", + signers: []signature.Signer{rsa}, + signOpts: []signature.SignOption{ + options.WithCryptoSignerOpts(crypto.SHA256), + }, + de: newDSSEDecoder(rsa), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{rsaPub}, + }, + { + name: "SHA384", + signers: []signature.Signer{rsa}, + signOpts: []signature.SignOption{ + options.WithCryptoSignerOpts(crypto.SHA384), + }, + de: newDSSEDecoder(rsa), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{rsaPub}, + }, + { + name: "SHA512", + signers: []signature.Signer{rsa}, + signOpts: []signature.SignOption{ + options.WithCryptoSignerOpts(crypto.SHA512), + }, + de: newDSSEDecoder(rsa), + wantMessage: testMessage, + wantKeys: []crypto.PublicKey{rsaPub}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + b := bytes.Buffer{} + + en, err := newDSSEEncoder(tt.signers, tt.signOpts...) + if err != nil { + t.Fatal(err) + } + + // Sign and encode message. + h, err := en.signMessage(&b, strings.NewReader(testMessage)) + if err != nil { + t.Fatal(err) + } + + // Introduce corruption, if applicable. + if tt.corrupter != nil { + var e dsse.Envelope + if err := json.Unmarshal(b.Bytes(), &e); err != nil { + t.Fatal(err) + } + + tt.corrupter(t, en, &e) + + b.Reset() + if err := json.NewEncoder(&b).Encode(e); err != nil { + t.Fatal(err) + } + } + + // Decode and verify message. + var vr VerifyResult + message, err := tt.de.verifyMessage(bytes.NewReader(b.Bytes()), h, &vr) + + if got, want := err, tt.wantErr; !errors.Is(got, want) { + t.Errorf("got error %v, want %v", got, want) + } + + if got, want := string(message), tt.wantMessage; got != want { + t.Errorf("got message %v, want %v", got, want) + } + + if got, want := vr.keys(), tt.wantKeys; !reflect.DeepEqual(got, want) { + t.Errorf("got keys %#v, want %#v", got, want) + } + }) + } +} diff --git a/pkg/integrity/main_test.go b/pkg/integrity/main_test.go index 16c80366..16757a34 100644 --- a/pkg/integrity/main_test.go +++ b/pkg/integrity/main_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved. +// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file // distributed with the sources of this project regarding your rights to use or distribute this // software. @@ -6,12 +6,15 @@ package integrity import ( + "crypto" "os" "path/filepath" "testing" "time" "github.com/ProtonMail/go-crypto/openpgp" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" "github.com/sylabs/sif/v2/pkg/sif" ) @@ -39,6 +42,20 @@ func loadContainer(t *testing.T, path string) *sif.FileImage { return f } +// getTestSignerVerifier returns a SignerVerifier read from the PEM file at path. +func getTestSignerVerifier(t *testing.T, name string) signature.SignerVerifier { //nolint:ireturn + t.Helper() + + path := filepath.Join("..", "..", "test", "keys", name) + + sv, err := signature.LoadSignerVerifierFromPEMFile(path, crypto.SHA256, cryptoutils.SkipPassword) + if err != nil { + t.Fatal(err) + } + + return sv +} + // getTestEntity returns a fixed test PGP entity. func getTestEntity(t *testing.T) *openpgp.Entity { t.Helper() diff --git a/pkg/integrity/result.go b/pkg/integrity/result.go index 650fa237..fa8fef85 100644 --- a/pkg/integrity/result.go +++ b/pkg/integrity/result.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021, Sylabs Inc. All rights reserved. +// Copyright (c) 2020-2022, Sylabs Inc. All rights reserved. // This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file // distributed with the sources of this project regarding your rights to use or distribute this // software. @@ -6,7 +6,10 @@ package integrity import ( + "crypto" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sylabs/sif/v2/pkg/sif" ) @@ -14,6 +17,7 @@ import ( type VerifyResult struct { sig sif.Descriptor verified []sif.Descriptor + aks []dsse.AcceptedKey e *openpgp.Entity err error } @@ -28,6 +32,15 @@ func (r VerifyResult) Verified() []sif.Descriptor { return r.verified } +// keys returns the public key(s) used to verify the signature. +func (r VerifyResult) keys() []crypto.PublicKey { + keys := make([]crypto.PublicKey, 0, len(r.aks)) + for _, ak := range r.aks { + keys = append(keys, ak.Public) + } + return keys +} + // Entity returns the signing entity, or nil if the signing entity could not be determined. func (r VerifyResult) Entity() *openpgp.Entity { return r.e diff --git a/pkg/integrity/testdata/Test_dsseEncoder_signMessage/ECDSA.golden b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/ECDSA.golden new file mode 100644 index 00000000..bb683961 --- /dev/null +++ b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/ECDSA.golden @@ -0,0 +1 @@ +{"payloadType":"application/vnd.sylabs.sif-metadata+json","payload":"eyJPbmUiOjEsIlR3byI6Mn0K","signatures":[{"keyid":"SHA256:13cNV0A11AxgIQYCvlDmJlRi0CjRtjnYF47RzrMhlaU","sig":"MEQCIFo8YCnASBqoUCa0T2H2L/crRYxZ9vFkSpbplBYo4IR6AiAUNulVHknRpFlrRWayZLY6vG9WIW5mx/ywLyI90KchQA=="}]} diff --git a/pkg/integrity/testdata/Test_dsseEncoder_signMessage/ED25519.golden b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/ED25519.golden new file mode 100644 index 00000000..2e545c67 --- /dev/null +++ b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/ED25519.golden @@ -0,0 +1 @@ +{"payloadType":"application/vnd.sylabs.sif-metadata+json","payload":"eyJPbmUiOjEsIlR3byI6Mn0K","signatures":[{"keyid":"SHA256:x6l8ZblpSSXGaPMCzySedWg88BwIFcz8jlPb6el0mFs","sig":"SNnYRFIhDwWjk0pxoreaNiLea6L2WAFUm4boxnv7jiBNGmvMnbCxdsHYsTRBLXvMJHwEfKGvHFJmi9VvMe4JCQ=="}]} diff --git a/pkg/integrity/testdata/Test_dsseEncoder_signMessage/Multi.golden b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/Multi.golden new file mode 100644 index 00000000..ead06c85 --- /dev/null +++ b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/Multi.golden @@ -0,0 +1 @@ +{"payloadType":"application/vnd.sylabs.sif-metadata+json","payload":"eyJPbmUiOjEsIlR3byI6Mn0K","signatures":[{"keyid":"SHA256:13cNV0A11AxgIQYCvlDmJlRi0CjRtjnYF47RzrMhlaU","sig":"MEQCIFo8YCnASBqoUCa0T2H2L/crRYxZ9vFkSpbplBYo4IR6AiAUNulVHknRpFlrRWayZLY6vG9WIW5mx/ywLyI90KchQA=="},{"keyid":"SHA256:x6l8ZblpSSXGaPMCzySedWg88BwIFcz8jlPb6el0mFs","sig":"SNnYRFIhDwWjk0pxoreaNiLea6L2WAFUm4boxnv7jiBNGmvMnbCxdsHYsTRBLXvMJHwEfKGvHFJmi9VvMe4JCQ=="},{"keyid":"SHA256:BhCwr7qZulYcOMSl2Jt2DuYHxHNnN6th4NdMqR/PGa4","sig":"qtxC0N3TWRUOOF4nAFwf8izZMVhpGca/s0STBi2h/OU/lND9M4uPG70LMGJ+n2GhCOyKKLR5BpgtlUkBpwhsxiPDqyyXFE2/Rvu/MsNicNIal7A1E64X3iOrMmaXK7qHDY6TpwC0KlxTOsh2XHJSM/cItgebkiRn5ZaZl48/10IzMsq/nOr0k9fGdAdgeApnRAQzBuHzcSAMpz8k9ovbyecfwuNLxXk6PO3isetpFx2j1d11gNfmwE54lCQ9ZGC3hiTJVt9WLBP+xC5AGoiX9f5FQpRzQrg9xGjyfwZDF4PSE9UFfUAC4fGPdultxUPXp8afWocJwbDgZBOkUKgE2L16LtMYSPFMdmAy615Ah6AOyudDTY+6iUr8D7YFdXgkjuQOGxtk7Wh2AIwk1lTOF4nrpycNjOJawBW5AFxdjEJ0LvG/XEJgSC88RoAkQ0YdN7j5N8nNf4+bZJ+CmTXPWU0MdFVDgI59bJKUJU/lt1WM/ZEIzujCgtqYKwCc8LNl5Fruh+2nHmtsAS3bxxPv51Nbw5d8T316SBp0bhjY+R7OncQDaP2FQ+nwpUXuDX3Tr9pqMJxDgErbIATOdSaRQ3KB1iC5gzTwIikuwPIxAuB2Gb5wWGxhqqfx7iA38TpnP5x8YXsjGCseUxFjrKoj5uL1p6ayGXOPJy/D9FsQVtA="}]} diff --git a/pkg/integrity/testdata/Test_dsseEncoder_signMessage/RSA.golden b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/RSA.golden new file mode 100644 index 00000000..0bc3e8d6 --- /dev/null +++ b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/RSA.golden @@ -0,0 +1 @@ +{"payloadType":"application/vnd.sylabs.sif-metadata+json","payload":"eyJPbmUiOjEsIlR3byI6Mn0K","signatures":[{"keyid":"SHA256:BhCwr7qZulYcOMSl2Jt2DuYHxHNnN6th4NdMqR/PGa4","sig":"qtxC0N3TWRUOOF4nAFwf8izZMVhpGca/s0STBi2h/OU/lND9M4uPG70LMGJ+n2GhCOyKKLR5BpgtlUkBpwhsxiPDqyyXFE2/Rvu/MsNicNIal7A1E64X3iOrMmaXK7qHDY6TpwC0KlxTOsh2XHJSM/cItgebkiRn5ZaZl48/10IzMsq/nOr0k9fGdAdgeApnRAQzBuHzcSAMpz8k9ovbyecfwuNLxXk6PO3isetpFx2j1d11gNfmwE54lCQ9ZGC3hiTJVt9WLBP+xC5AGoiX9f5FQpRzQrg9xGjyfwZDF4PSE9UFfUAC4fGPdultxUPXp8afWocJwbDgZBOkUKgE2L16LtMYSPFMdmAy615Ah6AOyudDTY+6iUr8D7YFdXgkjuQOGxtk7Wh2AIwk1lTOF4nrpycNjOJawBW5AFxdjEJ0LvG/XEJgSC88RoAkQ0YdN7j5N8nNf4+bZJ+CmTXPWU0MdFVDgI59bJKUJU/lt1WM/ZEIzujCgtqYKwCc8LNl5Fruh+2nHmtsAS3bxxPv51Nbw5d8T316SBp0bhjY+R7OncQDaP2FQ+nwpUXuDX3Tr9pqMJxDgErbIATOdSaRQ3KB1iC5gzTwIikuwPIxAuB2Gb5wWGxhqqfx7iA38TpnP5x8YXsjGCseUxFjrKoj5uL1p6ayGXOPJy/D9FsQVtA="}]} diff --git a/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA256.golden b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA256.golden new file mode 100644 index 00000000..0bc3e8d6 --- /dev/null +++ b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA256.golden @@ -0,0 +1 @@ +{"payloadType":"application/vnd.sylabs.sif-metadata+json","payload":"eyJPbmUiOjEsIlR3byI6Mn0K","signatures":[{"keyid":"SHA256:BhCwr7qZulYcOMSl2Jt2DuYHxHNnN6th4NdMqR/PGa4","sig":"qtxC0N3TWRUOOF4nAFwf8izZMVhpGca/s0STBi2h/OU/lND9M4uPG70LMGJ+n2GhCOyKKLR5BpgtlUkBpwhsxiPDqyyXFE2/Rvu/MsNicNIal7A1E64X3iOrMmaXK7qHDY6TpwC0KlxTOsh2XHJSM/cItgebkiRn5ZaZl48/10IzMsq/nOr0k9fGdAdgeApnRAQzBuHzcSAMpz8k9ovbyecfwuNLxXk6PO3isetpFx2j1d11gNfmwE54lCQ9ZGC3hiTJVt9WLBP+xC5AGoiX9f5FQpRzQrg9xGjyfwZDF4PSE9UFfUAC4fGPdultxUPXp8afWocJwbDgZBOkUKgE2L16LtMYSPFMdmAy615Ah6AOyudDTY+6iUr8D7YFdXgkjuQOGxtk7Wh2AIwk1lTOF4nrpycNjOJawBW5AFxdjEJ0LvG/XEJgSC88RoAkQ0YdN7j5N8nNf4+bZJ+CmTXPWU0MdFVDgI59bJKUJU/lt1WM/ZEIzujCgtqYKwCc8LNl5Fruh+2nHmtsAS3bxxPv51Nbw5d8T316SBp0bhjY+R7OncQDaP2FQ+nwpUXuDX3Tr9pqMJxDgErbIATOdSaRQ3KB1iC5gzTwIikuwPIxAuB2Gb5wWGxhqqfx7iA38TpnP5x8YXsjGCseUxFjrKoj5uL1p6ayGXOPJy/D9FsQVtA="}]} diff --git a/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA384.golden b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA384.golden new file mode 100644 index 00000000..46c14754 --- /dev/null +++ b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA384.golden @@ -0,0 +1 @@ +{"payloadType":"application/vnd.sylabs.sif-metadata+json","payload":"eyJPbmUiOjEsIlR3byI6Mn0K","signatures":[{"keyid":"SHA256:BhCwr7qZulYcOMSl2Jt2DuYHxHNnN6th4NdMqR/PGa4","sig":"p30wIIYvuxg1vLuVMSBCb9HlLjCJuHcW4ORi1GHSFzFxqLMkUVcGZvLotyA+25tInLqwX8SfTXkuIM1LnvwrrjOubi2Om8Cd9gyZqYP5Dx+BsPbAEmDgBkmV/2am6voKmpXuzNlXYocyCezw+Px+oxJI3bmFMCTjkFf5q4ah5HWvDMgfpa9T7oj13iS1WtLE2W6S5yuaIu/5gGQAWrxKKIhjAGEyS9+6I9VJEP2d2hJPDyey7YatM2Z7BfukVml/IBKoYo4cz50Y5fLfA+DstjpxQzQ0t/LyuntvsMVnOEH0n1C59aEku7RTFDVoA7GKvhSnWmGr4lD/33ZHeUDSNDWGoYo2rKeWUby+Z4Jyf27AbK2TrPvB3bxjIt7EoB4yuG1bmHgv6Agf7U43o8SZxo5mWEU/HOxulNRyE/W4quoVnD3slAIAfhDoOPo1flqaWFApPj4toyCuieQq2AheJxyP//crJnI+iLy3eUVWPELbSFpD4Gg2NKJ3PjiTx2XZndy5QVguAbjQy5RmMiFZ9Hk5qmV71wGDaBsGPKIDesM8CAXu3YOkhAEa36HYg9whgPsWDWjPe0VNzoN4UqXcjsb986n2M7AHVv+XHURf9MepRPy/pjShR0xUAeRFrZz8sBlhqaIXZcPwLBNSgZ91B5y/+VOy5aeOFTtfDG4nows="}]} diff --git a/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA512.golden b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA512.golden new file mode 100644 index 00000000..5ade7939 --- /dev/null +++ b/pkg/integrity/testdata/Test_dsseEncoder_signMessage/SHA512.golden @@ -0,0 +1 @@ +{"payloadType":"application/vnd.sylabs.sif-metadata+json","payload":"eyJPbmUiOjEsIlR3byI6Mn0K","signatures":[{"keyid":"SHA256:BhCwr7qZulYcOMSl2Jt2DuYHxHNnN6th4NdMqR/PGa4","sig":"QgK1Nf31jAGUaF8uUHiTCQdRaXXh4tUvX1r/n8TPv+jKYhwKTrwYYMHol0gzo1m/OmhTn8XC3vZ1cuVyvhhhBjIgux2gee+7Rm9Vzu5WwDeH0haU1tgUXcGqqLSc7Udh1zkT8+peGSTytF1ZCPuDxYJhKe6b5ZkT8vbd6nC5zHOAe9PRAp+VoJg3okvQvdWgphHcdpFM4eF6h4qTi9A5BlKCkL4MCdfKJWg0T/aLNQLI7+eKxqk7mWPcraTXqPUnyptQ4z+pO6nocNaTYx6Ouh4vkiMKzokesQxTwZz+/80Y/Ig1hvcljR7VPFrCfInhKU5o7ljjB8CQEN+MaFqtFVsAatO6ttC0Msj7iozaj+Cqm0PwrysIfQaRWGkMgnFkMEhgO4aebBLNLoAVBf92oY06lNsIPQofNi6id7s0R8ch6fuQ0N/4GjmDRCfaYYS5pQuA8TYIVVBCkjzMLgjInD1QQV/MxSDRNPLOrKcnF8j6ub0iknWaDyuBsIzjIe0D4QDdOJDxzhC3G4cqtPXfnvfJaGs7n0t05Y2sT4Z/5mGKw2yAJRd4g7Ur1HawcLdN42Bt200C6yXovmpV6MZglNLUttfQBffZQOSB7nK9jkBtHZca7YsUQIas9sIBbRX3HLzNfnz3MDhjuquLo4w633ZPoY+IPmcs/3x3QVX3tU8="}]}