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