From 07c8e8fda381c18af6d86dca273d4494bfd4cb5e Mon Sep 17 00:00:00 2001 From: asraa <asraa@google.com> Date: Tue, 20 Jul 2021 14:02:17 -0400 Subject: [PATCH] Generalize SignedCheckpoint to take arbitrary Notes (#347) * generalize signed checkpoint Signed-off-by: Asra Ali <asraa@google.com> * store note as text representation Signed-off-by: Asra Ali <asraa@google.com> * cleanup diff Signed-off-by: Asra Ali <asraa@google.com> * simplify Signed-off-by: Asra Ali <asraa@google.com> * use signer/verifier Signed-off-by: Asra Ali <asraa@google.com> * address dan comments Signed-off-by: Asra Ali <asraa@google.com> --- cmd/rekor-cli/app/log_info.go | 11 +- cmd/rekor-cli/app/state/state.go | 6 +- cmd/rekor-server/app/watch.go | 12 +- pkg/api/tlog.go | 45 +--- pkg/client/rekor_client.go | 2 +- .../restapi/configure_rekor_server.go | 2 +- pkg/util/checkpoint.go | 211 +++--------------- pkg/util/checkpoint_test.go | 131 +++++------ pkg/util/signed_note.go | 192 ++++++++++++++++ pkg/util/timestamp_note.go | 173 ++++++++++++++ 10 files changed, 490 insertions(+), 295 deletions(-) create mode 100644 pkg/util/signed_note.go create mode 100644 pkg/util/timestamp_note.go diff --git a/cmd/rekor-cli/app/log_info.go b/cmd/rekor-cli/app/log_info.go index e7615f7..7671857 100644 --- a/cmd/rekor-cli/app/log_info.go +++ b/cmd/rekor-cli/app/log_info.go @@ -17,6 +17,7 @@ package app import ( "bytes" + "crypto" "crypto/x509" "encoding/hex" "encoding/pem" @@ -35,6 +36,7 @@ import ( "github.com/sigstore/rekor/pkg/generated/client/tlog" "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/util" + "github.com/sigstore/sigstore/pkg/signature" ) type logInfoCmdOutput struct { @@ -72,7 +74,7 @@ var logInfoCmd = &cobra.Command{ logInfo := result.GetPayload() - sth := util.RekorSTH{} + sth := util.SignedCheckpoint{} if err := sth.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { return nil, err } @@ -97,7 +99,12 @@ var logInfoCmd = &cobra.Command{ return nil, err } - if !sth.Verify(pub) { + verifier, err := signature.LoadVerifier(pub, crypto.SHA256) + if err != nil { + return nil, err + } + + if !sth.Verify(verifier) { return nil, errors.New("signature on tree head did not verify") } diff --git a/cmd/rekor-cli/app/state/state.go b/cmd/rekor-cli/app/state/state.go index 9ae0807..bd837fb 100644 --- a/cmd/rekor-cli/app/state/state.go +++ b/cmd/rekor-cli/app/state/state.go @@ -25,9 +25,9 @@ import ( "github.com/sigstore/rekor/pkg/util" ) -type persistedState map[string]*util.RekorSTH +type persistedState map[string]*util.SignedCheckpoint -func Dump(url string, sth *util.RekorSTH) error { +func Dump(url string, sth *util.SignedCheckpoint) error { rekorDir, err := getRekorDir() if err != nil { return err @@ -67,7 +67,7 @@ func loadStateFile() persistedState { return result } -func Load(url string) *util.RekorSTH { +func Load(url string) *util.SignedCheckpoint { if state := loadStateFile(); state != nil { return state[url] } diff --git a/cmd/rekor-server/app/watch.go b/cmd/rekor-server/app/watch.go index f8e0fb1..a1a7e2c 100644 --- a/cmd/rekor-server/app/watch.go +++ b/cmd/rekor-server/app/watch.go @@ -38,6 +38,7 @@ import ( genclient "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/log" "github.com/sigstore/rekor/pkg/util" + "github.com/sigstore/sigstore/pkg/signature" ) const rekorSthBucketEnv = "REKOR_STH_BUCKET" @@ -134,12 +135,17 @@ func doCheck(c *genclient.Rekor, pub crypto.PublicKey) (*SignedAndUnsignedLogRoo if err != nil { return nil, errors.Wrap(err, "getting log info") } - sth := util.RekorSTH{} + sth := util.SignedCheckpoint{} if err := sth.UnmarshalText([]byte(*li.Payload.SignedTreeHead)); err != nil { return nil, errors.Wrap(err, "unmarshalling tree head") } - if !sth.Verify(pub) { + verifier, err := signature.LoadVerifier(pub, crypto.SHA256) + if err != nil { + return nil, err + } + + if !sth.Verify(verifier) { return nil, errors.Wrap(err, "signed tree head failed verification") } @@ -168,5 +174,5 @@ func uploadToBlobStorage(ctx context.Context, bucket *blob.Bucket, lr *SignedAnd // For JSON marshalling type SignedAndUnsignedLogRoot struct { - VerifiedLogRoot *util.RekorSTH + VerifiedLogRoot *util.SignedCheckpoint } diff --git a/pkg/api/tlog.go b/pkg/api/tlog.go index a5b7658..2cfa5c8 100644 --- a/pkg/api/tlog.go +++ b/pkg/api/tlog.go @@ -16,9 +16,6 @@ package api import ( - "bytes" - "encoding/base64" - "encoding/binary" "encoding/hex" "fmt" "net/http" @@ -27,7 +24,6 @@ import ( "github.com/go-openapi/runtime/middleware" "github.com/google/trillian/types" "github.com/spf13/viper" - "golang.org/x/mod/sumdb/note" "google.golang.org/grpc/codes" "github.com/sigstore/rekor/pkg/generated/models" @@ -54,46 +50,23 @@ func GetLogInfoHandler(params tlog.GetLogInfoParams) middleware.Responder { hashString := hex.EncodeToString(root.RootHash) treeSize := int64(root.TreeSize) - sth := util.RekorSTH{ - SignedCheckpoint: util.SignedCheckpoint{ - Checkpoint: util.Checkpoint{ - Ecosystem: "Rekor", - Size: root.TreeSize, - Hash: root.RootHash, - }, - }, + sth, err := util.CreateSignedCheckpoint(util.Checkpoint{ + Ecosystem: "Rekor", + Size: root.TreeSize, + Hash: root.RootHash, + }) + if err != nil { + return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("marshalling error: %w", err), sthGenerateError) } sth.SetTimestamp(uint64(time.Now().UnixNano())) - // TODO: once api.signer implements crypto.Signer, switch to using Sign() API on Checkpoint - // var opts crypto.SignerOpts - // cs, ok := api.signer.(crypto.Signer) - // if !ok { - // kmsSigner, ok := api.signer.(kms.SignerVerifier) - // if !ok { - // return handleRekorAPIError(params, http.StatusInternalServerError, errors.New("unable to cast to crypto.Signer"), signingError) - // } - // var err error - // cs, opts, err = kmsSigner.CryptoSigner(params.HTTPRequest.Context(), nil) - // if err != nil { - // return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("unable to obtain crypto.Signer: %v", err), signingError) - // } - // } - // sth.Checkpoint.Sign(viper.GetString("rekor_server.hostname"), cs, opts) // sign the log root ourselves to get the log root signature - cpString, _ := sth.Checkpoint.MarshalText() - sig, err := api.signer.SignMessage(bytes.NewReader(cpString), options.WithContext(params.HTTPRequest.Context())) + _, err = sth.Sign(viper.GetString("rekor_server.hostname"), api.signer, options.WithContext(params.HTTPRequest.Context())) if err != nil { return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("signing error: %w", err), signingError) } - sth.Signatures = append(sth.Signatures, note.Signature{ - Name: viper.GetString("rekor_server.hostname"), - Hash: binary.BigEndian.Uint32([]byte(api.pubkeyHash)[0:4]), - Base64: base64.StdEncoding.EncodeToString(sig), - }) - - scBytes, err := sth.MarshalText() + scBytes, err := sth.SignedNote.MarshalText() if err != nil { return handleRekorAPIError(params, http.StatusInternalServerError, fmt.Errorf("marshalling error: %w", err), sthGenerateError) } diff --git a/pkg/client/rekor_client.go b/pkg/client/rekor_client.go index 07653d4..c341fd2 100644 --- a/pkg/client/rekor_client.go +++ b/pkg/client/rekor_client.go @@ -44,6 +44,6 @@ func GetRekorClient(rekorServerURL string) (*client.Rekor, error) { } registry := strfmt.Default - registry.Add("signedCheckpoint", &util.SignedCheckpoint{}, util.SignedCheckpointValidator) + registry.Add("signedCheckpoint", &util.SignedNote{}, util.SignedCheckpointValidator) return client.New(rt, registry), nil } diff --git a/pkg/generated/restapi/configure_rekor_server.go b/pkg/generated/restapi/configure_rekor_server.go index c97ef62..4e83ce1 100644 --- a/pkg/generated/restapi/configure_rekor_server.go +++ b/pkg/generated/restapi/configure_rekor_server.go @@ -95,7 +95,7 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler { api.TimestampGetTimestampResponseHandler = timestamp.GetTimestampResponseHandlerFunc(pkgapi.TimestampResponseHandler) api.TimestampGetTimestampCertChainHandler = timestamp.GetTimestampCertChainHandlerFunc(pkgapi.GetTimestampCertChainHandler) - api.RegisterFormat("signedCheckpoint", &util.SignedCheckpoint{}, util.SignedCheckpointValidator) + api.RegisterFormat("signedCheckpoint", &util.SignedNote{}, util.SignedCheckpointValidator) api.PreServerShutdown = func() {} diff --git a/pkg/util/checkpoint.go b/pkg/util/checkpoint.go index d16f0f0..0358ded 100644 --- a/pkg/util/checkpoint.go +++ b/pkg/util/checkpoint.go @@ -16,23 +16,13 @@ package util import ( - "bufio" "bytes" - "crypto" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" "encoding/base64" - "encoding/binary" "fmt" "strconv" "strings" "github.com/pkg/errors" - "golang.org/x/mod/sumdb/note" ) // heavily borrowed from https://github.com/google/trillian-examples/blob/master/formats/log/checkpoint.go @@ -108,192 +98,50 @@ func (c *Checkpoint) UnmarshalText(data []byte) error { return nil } -func (c Checkpoint) Sign(identity string, signer crypto.Signer, opts crypto.SignerOpts) (*note.Signature, error) { - hf := crypto.SHA256 - if opts != nil { - hf = opts.HashFunc() - } - - input, _ := c.MarshalText() - var digest []byte - if hf != crypto.Hash(0) { - hasher := hf.New() - _, err := hasher.Write(input) - if err != nil { - return nil, errors.Wrap(err, "hashing checkpoint before signing") - } - digest = hasher.Sum(nil) - } else { - digest, _ = c.MarshalText() - } - - sig, err := signer.Sign(rand.Reader, digest, opts) - if err != nil { - return nil, errors.Wrap(err, "signing checkpoint") - } - pubKeyBytes, err := x509.MarshalPKIXPublicKey(signer.Public()) - if err != nil { - return nil, errors.Wrap(err, "marshalling public key") - } - - pkSha := sha256.Sum256(pubKeyBytes) - - signature := note.Signature{ - Name: identity, - Hash: binary.BigEndian.Uint32(pkSha[:]), - Base64: base64.StdEncoding.EncodeToString(sig), - } - - return &signature, nil - -} - type SignedCheckpoint struct { Checkpoint - // Signatures are one or more signature lines covering the payload - Signatures []note.Signature -} - -// String returns the String representation of the SignedCheckpoint -func (s SignedCheckpoint) String() string { - var b strings.Builder - b.WriteString(s.Checkpoint.String()) - b.WriteRune('\n') - for _, sig := range s.Signatures { - var hbuf [4]byte - binary.BigEndian.PutUint32(hbuf[:], sig.Hash) - sigBytes, _ := base64.StdEncoding.DecodeString(sig.Base64) - b64 := base64.StdEncoding.EncodeToString(append(hbuf[:], sigBytes...)) - fmt.Fprintf(&b, "%c %s %s\n", '\u2014', sig.Name, b64) - } - - return b.String() -} - -// UnmarshalText parses the common formatted checkpoint data and stores the result -// in the SignedCheckpoint. THIS DOES NOT VERIFY SIGNATURES INSIDE THE CONTENT! -// -// The supplied data is expected to contain a single Checkpoint, followed by a single -// line with no comment, followed by one or more lines with the following format: -// -// \u2014 name signature -// -// * name is the string associated with the signer -// * signature is a base64 encoded string; the first 4 bytes of the decoded value is a -// hint to the public key; it is a big-endian encoded uint32 representing the first -// 4 bytes of the SHA256 hash of the public key -func (s *SignedCheckpoint) UnmarshalText(data []byte) error { - sc := SignedCheckpoint{} - - if err := sc.Checkpoint.UnmarshalText(data); err != nil { - return errors.Wrap(err, "parsing checkpoint portion") - } - - b := bufio.NewScanner(bytes.NewReader(data)) - var pastCheckpoint bool - for b.Scan() { - if len(b.Text()) == 0 { - pastCheckpoint = true - continue - } - if pastCheckpoint { - var name, signature string - if _, err := fmt.Fscanf(strings.NewReader(b.Text()), "\u2014 %s %s\n", &name, &signature); err != nil { - return errors.Wrap(err, "parsing signature") - } - - sigBytes, err := base64.StdEncoding.DecodeString(signature) - if err != nil { - return errors.Wrap(err, "decoding signature") - } - if len(sigBytes) < 5 { - return errors.New("signature is too small") - } - - sig := note.Signature{ - Name: name, - Hash: binary.BigEndian.Uint32(sigBytes[0:4]), - Base64: base64.StdEncoding.EncodeToString(sigBytes[4:]), - } - sc.Signatures = append(sc.Signatures, sig) - } - } - if len(sc.Signatures) == 0 { - return errors.New("no signatures found in input") - } - - // copy sc to s - *s = sc - return nil + SignedNote } -// Verify checks that one of the signatures can be successfully verified using -// the supplied public key -func (s SignedCheckpoint) Verify(public crypto.PublicKey) bool { - if len(s.Signatures) == 0 { - return false - } - - msg, _ := s.Checkpoint.MarshalText() - //TODO: generalize this - digest := sha256.Sum256(msg) - - for _, s := range s.Signatures { - sigBytes, err := base64.StdEncoding.DecodeString(s.Base64) - if err != nil { - return false - } - switch pk := public.(type) { - case *rsa.PublicKey: - if err := rsa.VerifyPSS(pk, crypto.SHA256, digest[:], sigBytes, &rsa.PSSOptions{Hash: crypto.SHA256}); err == nil { - return true - } - case *ecdsa.PublicKey: - if ecdsa.VerifyASN1(pk, digest[:], sigBytes) { - return true - } - case *ed25519.PublicKey: - if ed25519.Verify(*pk, msg, sigBytes) { - return true - } - default: - return false - } - } - return false -} - -// Sign adds an additional signature to a SignedCheckpoint object -// The signature is added to the signature array as well as being directly returned to the caller -func (s *SignedCheckpoint) Sign(identity string, signer crypto.Signer, opts crypto.SignerOpts) (*note.Signature, error) { - sig, err := s.Checkpoint.Sign(identity, signer, opts) +func CreateSignedCheckpoint(c Checkpoint) (*SignedCheckpoint, error) { + text, err := c.MarshalText() if err != nil { return nil, err } - s.Signatures = append(s.Signatures, *sig) - return sig, nil -} - -// MarshalText returns the common format representation of this SignedCheckpoint. -func (s SignedCheckpoint) MarshalText() ([]byte, error) { - return []byte(s.String()), nil + return &SignedCheckpoint{ + Checkpoint: c, + SignedNote: SignedNote{Note: string(text)}, + }, nil } func SignedCheckpointValidator(strToValidate string) bool { - s := SignedCheckpoint{} - return s.UnmarshalText([]byte(strToValidate)) == nil + s := SignedNote{} + if err := s.UnmarshalText([]byte(strToValidate)); err != nil { + return false + } + c := &Checkpoint{} + return c.UnmarshalText([]byte(s.Note)) == nil } func CheckpointValidator(strToValidate string) bool { - c := Checkpoint{} + c := &Checkpoint{} return c.UnmarshalText([]byte(strToValidate)) == nil } -type RekorSTH struct { - SignedCheckpoint +func (r *SignedCheckpoint) UnmarshalText(data []byte) error { + s := SignedNote{} + if err := s.UnmarshalText([]byte(data)); err != nil { + return errors.Wrap(err, "unmarshalling signed note") + } + c := Checkpoint{} + if err := c.UnmarshalText([]byte(s.Note)); err != nil { + return errors.Wrap(err, "unmarshalling checkpoint") + } + *r = SignedCheckpoint{Checkpoint: c, SignedNote: s} + return nil } -func (r *RekorSTH) SetTimestamp(timestamp uint64) { +func (r *SignedCheckpoint) SetTimestamp(timestamp uint64) { var ts uint64 for i, val := range r.OtherContent { if n, _ := fmt.Fscanf(strings.NewReader(val), "Timestamp: %d", &ts); n == 1 { @@ -303,7 +151,7 @@ func (r *RekorSTH) SetTimestamp(timestamp uint64) { r.OtherContent = append(r.OtherContent, fmt.Sprintf("Timestamp: %d", timestamp)) } -func (r *RekorSTH) GetTimestamp() uint64 { +func (r *SignedCheckpoint) GetTimestamp() uint64 { var ts uint64 for _, val := range r.OtherContent { if n, _ := fmt.Fscanf(strings.NewReader(val), "Timestamp: %d", &ts); n == 1 { @@ -312,8 +160,3 @@ func (r *RekorSTH) GetTimestamp() uint64 { } return ts } - -func RekorSTHValidator(strToValidate string) bool { - r := RekorSTH{} - return r.UnmarshalText([]byte(strToValidate)) == nil -} diff --git a/pkg/util/checkpoint_test.go b/pkg/util/checkpoint_test.go index 2a91a43..2ce9821 100644 --- a/pkg/util/checkpoint_test.go +++ b/pkg/util/checkpoint_test.go @@ -25,6 +25,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" "golang.org/x/mod/sumdb/note" ) @@ -213,7 +215,7 @@ func TestSigningRoundtripCheckpoint(t *testing.T) { }, identity: "someone", signer: edPrivKey, - pubKey: &edPubKey, + pubKey: edPubKey, opts: crypto.Hash(0), wantSignErr: false, wantVerifyErr: false, @@ -253,7 +255,7 @@ func TestSigningRoundtripCheckpoint(t *testing.T) { identity: "someone", signer: edPrivKey, pubKey: rsaKey.Public(), - opts: crypto.Hash(0), + opts: &rsa.PSSOptions{Hash: crypto.SHA256}, wantSignErr: false, wantVerifyErr: true, }, @@ -265,28 +267,36 @@ func TestSigningRoundtripCheckpoint(t *testing.T) { }, identity: "someone", signer: ecdsaKey, - pubKey: &edPubKey, + pubKey: edPubKey, opts: nil, wantSignErr: false, wantVerifyErr: true, }, } { t.Run(string(test.c.Ecosystem), func(t *testing.T) { - sig, err := test.c.Sign(test.identity, test.signer, test.opts) + text, _ := test.c.MarshalText() + sc := &SignedNote{ + Note: string(text), + } + signer, _ := signature.LoadSigner(test.signer, crypto.SHA256) + if _, ok := test.signer.(*rsa.PrivateKey); ok { + signer, _ = signature.LoadRSAPSSSigner(test.signer.(*rsa.PrivateKey), crypto.SHA256, test.opts.(*rsa.PSSOptions)) + } + + _, err := sc.Sign(test.identity, signer, options.WithCryptoSignerOpts(test.opts)) if (err != nil) != test.wantSignErr { t.Fatalf("signing test failed: wantSignErr %v, err %v", test.wantSignErr, err) } if !test.wantSignErr { - sc := SignedCheckpoint{ - Checkpoint: test.c, - Signatures: []note.Signature{ - *sig, - }, + verifier, _ := signature.LoadVerifier(test.pubKey, crypto.SHA256) + if _, ok := test.pubKey.(*rsa.PublicKey); ok { + verifier, _ = signature.LoadRSAPSSVerifier(test.pubKey.(*rsa.PublicKey), crypto.SHA256, test.opts.(*rsa.PSSOptions)) } - if !sc.Verify(test.pubKey) != test.wantVerifyErr { - t.Fatalf("verification test failed %v", sc.Verify(test.pubKey)) + + if !sc.Verify(verifier) != test.wantVerifyErr { + t.Fatalf("verification test failed %v", sc.Verify(verifier)) } - if _, err := sc.Sign("second", test.signer, test.opts); err != nil { + if _, err := sc.Sign("second", signer, options.WithCryptoSignerOpts(test.opts)); err != nil { t.Fatalf("adding second signature failed: %v", err) } if len(sc.Signatures) != 2 { @@ -297,7 +307,10 @@ func TestSigningRoundtripCheckpoint(t *testing.T) { if err != nil { t.Fatalf("error during marshalling: %v", err) } - sc2 := SignedCheckpoint{} + text, _ = test.c.MarshalText() + sc2 := &SignedNote{ + Note: string(text), + } if err := sc2.UnmarshalText(marshalledSc); err != nil { t.Fatalf("error unmarshalling just marshalled object %v\n%v", err, string(marshalledSc)) } @@ -310,77 +323,65 @@ func TestSigningRoundtripCheckpoint(t *testing.T) { } func TestInvalidSigVerification(t *testing.T) { + ecdsaKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) for _, test := range []struct { - sc SignedCheckpoint + checkpoint Checkpoint + s []note.Signature pubKey crypto.PublicKey expectedResult bool }{ { - sc: SignedCheckpoint{ - Checkpoint: Checkpoint{ - Ecosystem: "Log Checkpoint v0", - Size: 123, - Hash: []byte("bananas"), - }, - Signatures: []note.Signature{}, + checkpoint: Checkpoint{ + Ecosystem: "Log Checkpoint v0", + Size: 123, + Hash: []byte("bananas"), }, + s: []note.Signature{}, + pubKey: ecdsaKey.Public(), expectedResult: false, }, { - sc: SignedCheckpoint{ - Checkpoint: Checkpoint{ - Ecosystem: "Log Checkpoint v0", - Size: 123, - Hash: []byte("bananas"), - }, - Signatures: []note.Signature{ - { - Name: "something", - Hash: 1234, - Base64: "not_base 64 string", - }, - }, + + checkpoint: Checkpoint{ + Ecosystem: "Log Checkpoint v0 not base64", + Size: 123, + Hash: []byte("bananas"), }, - expectedResult: false, - }, - { - sc: SignedCheckpoint{ - Checkpoint: Checkpoint{ - Ecosystem: "Log Checkpoint v0", - Size: 123, - Hash: []byte("bananas"), - }, - Signatures: []note.Signature{ - { - Name: "someone", - Hash: 142, - Base64: "bm90IGEgc2ln", // valid base64, not a valid signature - }, + pubKey: ecdsaKey.Public(), + s: []note.Signature{ + { + Name: "something", + Hash: 1234, + Base64: "not_base 64 string", }, }, expectedResult: false, }, { - sc: SignedCheckpoint{ - Checkpoint: Checkpoint{ - Ecosystem: "Log Checkpoint Ed25519 v0", - Size: 123, - Hash: []byte("bananas"), - }, - Signatures: []note.Signature{ - { - Name: "someone", - Hash: 1390313051, - Base64: "pOhM+S/mYjEYtQsOF4lL8o/dR+nbjoz5Cvg/n486KIismpVq0s4wxBaakmryI7zThjWAqRUyECPL3WSEcVDEBQ==", - }, + checkpoint: Checkpoint{ + Ecosystem: "Log Checkpoint v0 invalid signature", + Size: 123, + Hash: []byte("bananas"), + }, + pubKey: ecdsaKey.Public(), + s: []note.Signature{ + { + Name: "someone", + Hash: 142, + Base64: "bm90IGEgc2ln", // valid base64, not a valid signature }, }, - pubKey: nil, // valid input, invalid key expectedResult: false, }, } { - t.Run(string(test.sc.Ecosystem), func(t *testing.T) { - result := test.sc.Verify(test.pubKey) + t.Run(string(test.checkpoint.Ecosystem), func(t *testing.T) { + text, _ := test.checkpoint.MarshalText() + sc := SignedNote{ + Note: string(text), + Signatures: test.s, + } + verifier, _ := signature.LoadVerifier(test.pubKey, crypto.SHA256) + result := sc.Verify(verifier) if result != test.expectedResult { t.Fatal("verification test generated unexpected result") } @@ -430,7 +431,7 @@ func TestUnmarshalSignedCheckpoint(t *testing.T) { }, } { t.Run(string(test.desc), func(t *testing.T) { - var got SignedCheckpoint + var got SignedNote var gotErr error if gotErr = got.UnmarshalText([]byte(test.m)); (gotErr != nil) != test.wantErr { t.Fatalf("UnmarshalText(%s) = %q, wantErr: %v", test.desc, gotErr, test.wantErr) diff --git a/pkg/util/signed_note.go b/pkg/util/signed_note.go new file mode 100644 index 0000000..0cfd62f --- /dev/null +++ b/pkg/util/signed_note.go @@ -0,0 +1,192 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bufio" + "bytes" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/options" + "golang.org/x/mod/sumdb/note" +) + +type SignedNote struct { + // Textual representation of a note to sign. + Note string + // Signatures are one or more signature lines covering the payload + Signatures []note.Signature +} + +// Sign adds a signature to a SignedCheckpoint object +// The signature is added to the signature array as well as being directly returned to the caller +func (s *SignedNote) Sign(identity string, signer signature.Signer, opts signature.SignOption) (*note.Signature, error) { + sig, err := signer.SignMessage(bytes.NewReader([]byte(s.Note)), opts) + if err != nil { + return nil, errors.Wrap(err, "signing note") + } + + pk, err := signer.PublicKey() + if err != nil { + return nil, errors.Wrap(err, "retrieving public key") + } + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pk) + if err != nil { + return nil, errors.Wrap(err, "marshalling public key") + } + + pkSha := sha256.Sum256(pubKeyBytes) + + signature := note.Signature{ + Name: identity, + Hash: binary.BigEndian.Uint32(pkSha[:]), + Base64: base64.StdEncoding.EncodeToString(sig), + } + + s.Signatures = append(s.Signatures, signature) + return &signature, nil +} + +// Verify checks that one of the signatures can be successfully verified using +// the supplied public key +func (s SignedNote) Verify(verifier signature.Verifier) bool { + if len(s.Signatures) == 0 { + return false + } + + msg := []byte(s.Note) + digest := sha256.Sum256(msg) + + for _, s := range s.Signatures { + sigBytes, err := base64.StdEncoding.DecodeString(s.Base64) + if err != nil { + return false + } + pk, err := verifier.PublicKey() + if err != nil { + return false + } + opts := []signature.VerifyOption{} + switch pk.(type) { + case *rsa.PublicKey, *ecdsa.PublicKey: + opts = append(opts, options.WithDigest(digest[:])) + case ed25519.PublicKey: + break + default: + return false + } + if err := verifier.VerifySignature(bytes.NewReader(sigBytes), bytes.NewReader(msg), opts...); err != nil { + return false + } + } + return true +} + +// MarshalText returns the common format representation of this SignedNote. +func (s SignedNote) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + +// String returns the String representation of the SignedNote +func (s SignedNote) String() string { + var b strings.Builder + b.WriteString(s.Note) + b.WriteRune('\n') + for _, sig := range s.Signatures { + var hbuf [4]byte + binary.BigEndian.PutUint32(hbuf[:], sig.Hash) + sigBytes, _ := base64.StdEncoding.DecodeString(sig.Base64) + b64 := base64.StdEncoding.EncodeToString(append(hbuf[:], sigBytes...)) + fmt.Fprintf(&b, "%c %s %s\n", '\u2014', sig.Name, b64) + } + + return b.String() +} + +// UnmarshalText parses the common formatted signed note data and stores the result +// in the SignedNote. THIS DOES NOT VERIFY SIGNATURES INSIDE THE CONTENT! +// +// The supplied data is expected to contain a single Note, followed by a single +// line with no comment, followed by one or more lines with the following format: +// +// \u2014 name signature +// +// * name is the string associated with the signer +// * signature is a base64 encoded string; the first 4 bytes of the decoded value is a +// hint to the public key; it is a big-endian encoded uint32 representing the first +// 4 bytes of the SHA256 hash of the public key +func (s *SignedNote) UnmarshalText(data []byte) error { + sigSplit := []byte("\n\n") + // Must end with signature block preceded by blank line. + split := bytes.LastIndex(data, sigSplit) + if split < 0 { + return errors.New("malformed note") + } + text, data := data[:split+1], data[split+2:] + if len(data) == 0 || data[len(data)-1] != '\n' { + return errors.New("malformed note") + } + + sn := SignedNote{ + Note: string(text), + } + + b := bufio.NewScanner(bytes.NewReader(data)) + for b.Scan() { + var name, signature string + if _, err := fmt.Fscanf(strings.NewReader(b.Text()), "\u2014 %s %s\n", &name, &signature); err != nil { + return errors.Wrap(err, "parsing signature") + } + + sigBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return errors.Wrap(err, "decoding signature") + } + if len(sigBytes) < 5 { + return errors.New("signature is too small") + } + + sig := note.Signature{ + Name: name, + Hash: binary.BigEndian.Uint32(sigBytes[0:4]), + Base64: base64.StdEncoding.EncodeToString(sigBytes[4:]), + } + sn.Signatures = append(sn.Signatures, sig) + + } + if len(sn.Signatures) == 0 { + return errors.New("no signatures found in input") + } + + // copy sc to s + *s = sn + return nil +} + +func SignedNoteValidator(strToValidate string) bool { + s := SignedNote{} + return s.UnmarshalText([]byte(strToValidate)) == nil +} diff --git a/pkg/util/timestamp_note.go b/pkg/util/timestamp_note.go new file mode 100644 index 0000000..8399469 --- /dev/null +++ b/pkg/util/timestamp_note.go @@ -0,0 +1,173 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "golang.org/x/mod/sumdb/note" +) + +// Signed note based timestamp responses + +type TimestampNote struct { + // Ecosystem is the ecosystem/version string + Ecosystem string + // MessageImprint is the hash of the message to timestamp, of the form sha256:<sha> + MessageImprint []byte + // Nonce is a short random bytes to prove response freshness + Nonce []byte + // Time is the timestamp to imprint on the message + Time time.Time + // Radius is the time in microseconds used to indicate certainty + Radius int64 + // CertChainRef is a reference URL to the valid timestamping cert chain used to sign the response + CertChainRef *url.URL + // OtherContent is any additional data to be included in the signed payload; each element is assumed to be one line + OtherContent []string +} + +// String returns the String representation of the TimestampNote +func (t TimestampNote) String() string { + var b strings.Builder + time, _ := t.Time.MarshalText() + fmt.Fprintf(&b, "%s\n%s\n%d\n%s\n%d\n%s", t.Ecosystem, base64.StdEncoding.EncodeToString(t.MessageImprint), + t.Nonce, time, t.Radius, t.CertChainRef) + for _, line := range t.OtherContent { + fmt.Fprintf(&b, "%s\n", line) + } + return b.String() +} + +// MarshalText returns the common format representation of this TimestampNote. +func (t TimestampNote) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +// UnmarshalText parses the common formatted timestamp note data and stores the result +// in the TimestampNote. +// +// The supplied data is expected to begin with the following 6 lines of text, +// each followed by a newline: +// <ecosystem/version string> +// <base64 representation of message hash> +// <base64 representation of the nonce> +// <RFC 3339 representation of the time> +// <decimal representation of radius> +// <cert chain URI> +// <optional non-empty line of other content>... +// <optional non-empty line of other content>... +// +// This will discard any content found after the checkpoint (including signatures) +func (t *TimestampNote) UnmarshalText(data []byte) error { + l := bytes.Split(data, []byte("\n")) + if len(l) < 7 { + return errors.New("invalid timestamp note - too few newlines") + } + eco := string(l[0]) + if len(eco) == 0 { + return errors.New("invalid timestamp note - empty ecosystem") + } + h, err := base64.StdEncoding.DecodeString(string(l[1])) + if err != nil { + return fmt.Errorf("invalid timestamp note - invalid message hash: %w", err) + } + nonce, err := base64.StdEncoding.DecodeString(string(l[2])) + if err != nil { + return fmt.Errorf("invalid timestamp note - invalid nonce: %w", err) + } + var timestamp time.Time + if err := timestamp.UnmarshalText(l[3]); err != nil { + return fmt.Errorf("invalid timestamp note - invalid time: %w", err) + } + r, err := strconv.ParseInt(string(l[4]), 10, 64) + if err != nil { + return fmt.Errorf("invalid timestamp note - invalid radius: %w", err) + } + u, err := url.Parse(string(l[5])) + if err != nil { + return fmt.Errorf("invalid timestamp note - invalid URI: %w", err) + + } + *t = TimestampNote{ + Ecosystem: eco, + MessageImprint: h, + Nonce: nonce, + Time: timestamp, + Radius: r, + CertChainRef: u, + } + if len(l) >= 5 { + for _, line := range l[3:] { + if len(line) == 0 { + break + } + t.OtherContent = append(t.OtherContent, string(line)) + } + } + return nil +} + +func (t TimestampNote) Sign(identity string, signer crypto.Signer, opts crypto.SignerOpts) (*note.Signature, error) { + hf := crypto.SHA256 + if opts != nil { + hf = opts.HashFunc() + } + + input, _ := t.MarshalText() + var digest []byte + if hf != crypto.Hash(0) { + hasher := hf.New() + _, err := hasher.Write(input) + if err != nil { + return nil, errors.Wrap(err, "hashing timestamp note before signing") + } + digest = hasher.Sum(nil) + } else { + digest, _ = t.MarshalText() + } + + sig, err := signer.Sign(rand.Reader, digest, opts) + if err != nil { + return nil, errors.Wrap(err, "signing timestamp note") + } + pubKeyBytes, err := x509.MarshalPKIXPublicKey(signer.Public()) + if err != nil { + return nil, errors.Wrap(err, "marshalling public key") + } + + pkSha := sha256.Sum256(pubKeyBytes) + + signature := note.Signature{ + Name: identity, + Hash: binary.BigEndian.Uint32(pkSha[:]), + Base64: base64.StdEncoding.EncodeToString(sig), + } + + return &signature, nil +} -- GitLab