Skip to content
Snippets Groups Projects
Unverified Commit 07c8e8fd authored by asraa's avatar asraa Committed by GitHub
Browse files

Generalize SignedCheckpoint to take arbitrary Notes (#347)


* generalize signed checkpoint

Signed-off-by: default avatarAsra Ali <asraa@google.com>

* store note as text representation

Signed-off-by: default avatarAsra Ali <asraa@google.com>

* cleanup diff

Signed-off-by: default avatarAsra Ali <asraa@google.com>

* simplify

Signed-off-by: default avatarAsra Ali <asraa@google.com>

* use signer/verifier

Signed-off-by: default avatarAsra Ali <asraa@google.com>

* address dan comments

Signed-off-by: default avatarAsra Ali <asraa@google.com>
parent 12077f5d
No related branches found
No related tags found
No related merge requests found
......@@ -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")
}
......
......@@ -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]
}
......
......@@ -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
}
......@@ -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)
}
......
......@@ -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
}
......@@ -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() {}
......
......@@ -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
}
......@@ -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)
......
//
// 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
}
//
// 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
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment