Skip to content

Commit

Permalink
Add timestamped signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
FiloSottile committed May 2, 2023
1 parent ade17a7 commit 91d36ea
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 41 deletions.
7 changes: 2 additions & 5 deletions cmd/omniwitness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ The `.env` file required for the Docker service is a key-value format with this

```
WITNESS_PRIVATE_KEY=PRIVATE+KEY+YourTokenHere+XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
WITNESS_PUBLIC_KEY=YourTokenHere+01234567+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
GITHUB_AUTH_TOKEN=ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
GIT_USERNAME=johndoe
Expand All @@ -67,7 +66,7 @@ [email protected]
WITNESS_VERSION=latest
```

`WITNESS_PRIVATE_KEY` and `WITNESS_PUBLIC_KEY` should be generated as documented in [Witness Key Generation](#witness-key-generation).
`WITNESS_PRIVATE_KEY` should be generated as documented in [Witness Key Generation](#witness-key-generation).

If you wish to use the distributors to push to GitHub, follow the steps in [GitHub Credentials](#github-credentials) and then:
* The token should be set as `GITHUB_AUTH_TOKEN`
Expand All @@ -81,14 +80,13 @@ If you have some reason to run the OmniWitness outside of Docker, then you can r
### Simple

The simplest possible configuration brings up the OmniWitness to follow all of the logs,
but the witnessed checkpoints will not be distributed and can only be disovered via the
but the witnessed checkpoints will not be distributed and can only be discovered via the
witness HTTP endpoints.
You will need to have followed the steps in [Witness Key Generation](#witness-key-generation).

```
go run github.com/transparency-dev/witness/cmd/omniwitness@master --alsologtostderr --v=1 \
--private_key PRIVATE+KEY+my.witness+67890abc+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--public_key my.witness+67890abc+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--db_file ~/witness.db
```

Expand All @@ -103,7 +101,6 @@ This is described in [GitHub Credentials](#github-credentials).
```
go run github.com/transparency-dev/witness/cmd/omniwitness@master --alsologtostderr --v=1 \
--private_key PRIVATE+KEY+my.witness+67890abc+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--public_key my.witness+67890abc+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
--gh_user my-github-user \
--gh_email [email protected] \
--gh_token ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
Expand Down
1 change: 0 additions & 1 deletion cmd/omniwitness/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ services:
- "--listen=:8100"
- "--db_file=/data/witness.sqlite"
- "--private_key=${WITNESS_PRIVATE_KEY}"
- "--public_key=${WITNESS_PUBLIC_KEY}"
- "--gh_user=${GIT_USERNAME}"
- "--gh_email=${GIT_EMAIL}"
- "--gh_token=${GITHUB_AUTH_TOKEN}"
Expand Down
15 changes: 2 additions & 13 deletions cmd/omniwitness/monolith.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
"github.com/transparency-dev/witness/internal/persistence/inmemory"
psql "github.com/transparency-dev/witness/internal/persistence/sql"
"github.com/transparency-dev/witness/omniwitness"
"golang.org/x/mod/sumdb/note"

_ "github.com/mattn/go-sqlite3" // Load drivers for sqlite3
)
Expand All @@ -38,8 +37,7 @@ var (
addr = flag.String("listen", ":8080", "Address to listen on")
dbFile = flag.String("db_file", "", "path to a file to be used as sqlite3 storage for checkpoints, e.g. /tmp/chkpts.db")

signingKey = flag.String("private_key", "", "The note-compatible signing key to use")
verifierKey = flag.String("public_key", "", "The note-compatible verifier key to use")
signingKey = flag.String("private_key", "", "The note-compatible signing key to use")

githubUser = flag.String("gh_user", "", "The github user account to propose witnessed PRs from")
githubEmail = flag.String("gh_email", "", "The email that witnessed checkopoint git commits should be done under")
Expand All @@ -60,17 +58,8 @@ func main() {
Timeout: *httpTimeout,
}

signer, err := note.NewSigner(*signingKey)
if err != nil {
glog.Exitf("Failed to init signer: %v", err)
}
verifier, err := note.NewVerifier(*verifierKey)
if err != nil {
glog.Exitf("Failed to init verifier: %v", err)
}
opConfig := omniwitness.OperatorConfig{
WitnessSigner: signer,
WitnessVerifier: verifier,
WitnessKey: *signingKey,

GithubUser: *githubUser,
GithubEmail: *githubEmail,
Expand Down
2 changes: 1 addition & 1 deletion internal/http/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func newWitness(t *testing.T, logs []logOpts) *witness.Witness {
}
opts := witness.Opts{
Persistence: inmemory.NewPersistence(),
Signer: ns,
Signers: []note.Signer{ns},
KnownLogs: logMap,
}
// Create the witness
Expand Down
147 changes: 147 additions & 0 deletions internal/note/note_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package note

import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"strings"
"time"
"unicode"
"unicode/utf8"

"golang.org/x/mod/sumdb/note"
)

const (
algEd25519 = 1
algEd25519CosignatureV1 = 4
)

// NewSignerForCosignatureV1 constructs a new Signer that produces timestamped
// cosignature/v1 signatures from a standard Ed25519 encoded signer key.
//
// (The returned Signer has a different key hash from a non-timestamped one,
// meaning it will differ from the key hash in the input encoding.)
func NewSignerForCosignatureV1(skey string) (*Signer, error) {
priv1, skey, _ := strings.Cut(skey, "+")
priv2, skey, _ := strings.Cut(skey, "+")
name, skey, _ := strings.Cut(skey, "+")
hash16, key64, _ := strings.Cut(skey, "+")
key, err := base64.StdEncoding.DecodeString(key64)
if priv1 != "PRIVATE" || priv2 != "KEY" || len(hash16) != 8 || err != nil || !isValidName(name) || len(key) == 0 {
return nil, errSignerID
}

s := &Signer{name: name}

alg, key := key[0], key[1:]
switch alg {
default:
return nil, errSignerAlg

case algEd25519:
if len(key) != ed25519.SeedSize {
return nil, errSignerID
}
key := ed25519.NewKeyFromSeed(key)
pubkey := append([]byte{algEd25519CosignatureV1}, key.Public().(ed25519.PublicKey)...)
s.hash = keyHashEd25519(name, pubkey)
s.sign = func(msg []byte) ([]byte, error) {
t := uint64(time.Now().Unix())
m, err := formatCosignatureV1(t, msg)
if err != nil {
return nil, err
}

// The signature itself is encoded as timestamp || signature.
sig := make([]byte, 0, 8+ed25519.SignatureSize)
sig = binary.LittleEndian.AppendUint64(sig, t)
sig = append(sig, ed25519.Sign(key, m)...)
return sig, nil
}
s.verify = func(msg, sig []byte) bool {
if len(sig) != 8+ed25519.SignatureSize {
return false
}
t := binary.LittleEndian.Uint64(sig)
sig = sig[8:]
m, err := formatCosignatureV1(t, msg)
if err != nil {
return false
}
return ed25519.Verify(key.Public().(ed25519.PublicKey), m, sig)
}
}

return s, nil
}

func formatCosignatureV1(t uint64, msg []byte) ([]byte, error) {
// The signed message is in the following format
//
// cosignature/v1
// time TTTTTTTTTT
// origin line
// NNNNNNNNN
// tree hash
//
// where TTTTTTTTTT is the current UNIX timestamp, and the following
// three lines are the first three lines of the note. All other
// lines are not processed by the witness, so are not signed.

lines := bytes.Split(msg, []byte("\n"))
if len(lines) < 3 {
return nil, errors.New("cosigned note format invalid")
}
return []byte(fmt.Sprintf(
"cosignature/v1\ntime %d\n%s\n%s\n%s\n",
t, lines[0], lines[1], lines[2])), nil
}

var (
errSignerID = errors.New("malformed verifier id")
errSignerAlg = errors.New("unknown verifier algorithm")
errSignerHash = errors.New("invalid verifier hash")
)

type Signer struct {
name string
hash uint32
sign func([]byte) ([]byte, error)
verify func(msg, sig []byte) bool
}

func (s *Signer) Name() string { return s.name }
func (s *Signer) KeyHash() uint32 { return s.hash }
func (s *Signer) Sign(msg []byte) ([]byte, error) { return s.sign(msg) }

func (s *Signer) Verifier() note.Verifier {
return &verifier{
name: s.name,
keyHash: s.hash,
v: s.verify,
}
}

// isValidName reports whether name is valid.
// It must be non-empty and not have any Unicode spaces or pluses.
func isValidName(name string) bool {
return name != "" && utf8.ValidString(name) && strings.IndexFunc(name, unicode.IsSpace) < 0 && !strings.Contains(name, "+")
}

func keyHashEd25519(name string, key []byte) uint32 {
h := sha256.New()
h.Write([]byte(name))
h.Write([]byte("\n"))
h.Write(key)
sum := h.Sum(nil)
return binary.BigEndian.Uint32(sum)
}
34 changes: 34 additions & 0 deletions internal/note/note_signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package note

import (
"crypto/rand"
"testing"

"golang.org/x/mod/sumdb/note"
)

func TestSignerRoundtrip(t *testing.T) {
skey, _, err := note.GenerateKey(rand.Reader, "test")
if err != nil {
t.Fatal(err)
}

s, err := NewSignerForCosignatureV1(skey)
if err != nil {
t.Fatal(err)
}

msg := "test\n123\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP0=\n"
n, err := note.Sign(&note.Note{Text: msg}, s)
if err != nil {
t.Fatal(err)
}

if _, err := note.Open(n, note.VerifierList(s.Verifier())); err != nil {
t.Fatal(err)
}
}
14 changes: 7 additions & 7 deletions internal/note/note_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package note provides note-compatible signature verifiers.
// Package note provides note-compatible signature verifiers and signers.
package note

import (
Expand All @@ -25,7 +25,7 @@ import (
"strconv"
"strings"

sdb_note "golang.org/x/mod/sumdb/note"
"golang.org/x/mod/sumdb/note"
)

const (
Expand All @@ -41,12 +41,12 @@ const (
)

// NewVerifier returns a verifier for the given key type and key.
func NewVerifier(keyType, key string) (sdb_note.Verifier, error) {
func NewVerifier(keyType, key string) (note.Verifier, error) {
switch keyType {
case ECDSA:
return NewECDSAVerifier(key)
case Note:
return sdb_note.NewVerifier(key)
return note.NewVerifier(key)
default:
return nil, fmt.Errorf("unknown key type %q", keyType)
}
Expand Down Expand Up @@ -91,7 +91,7 @@ func (v *verifier) Verify(msg, sig []byte) bool {
// e.g.:
//
// "rekor.sigstore.dev+12345678+AjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNhtmPtrWm3U1eQXBogSMdGvXwBcK5AW5i0hrZLOC96l+smGNM7nwZ4QvFK/4sueRoVj//QP22Ni4Qt9DPfkWLc=
func NewECDSAVerifier(key string) (sdb_note.Verifier, error) {
func NewECDSAVerifier(key string) (note.Verifier, error) {
parts := strings.SplitN(key, "+", 3)
if got, want := len(parts), 3; got != want {
return nil, fmt.Errorf("key has %d parts, expected %d: %q", got, want, key)
Expand All @@ -107,7 +107,7 @@ func NewECDSAVerifier(key string) (sdb_note.Verifier, error) {
return nil, fmt.Errorf("key has incorrect type %d", keyBytes[0])
}
der := keyBytes[1:]
kh := keyHash(der)
kh := keyHashECDSA(der)

khProvided, err := strconv.ParseUint(parts[1], 16, 32)
if err != nil {
Expand Down Expand Up @@ -136,7 +136,7 @@ func NewECDSAVerifier(key string) (sdb_note.Verifier, error) {
}, nil
}

func keyHash(i []byte) uint32 {
func keyHashECDSA(i []byte) uint32 {
h := sha256.Sum256(i)
return binary.BigEndian.Uint32(h[:])
}
14 changes: 7 additions & 7 deletions internal/witness/witness.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (
// Opts is the options passed to a witness.
type Opts struct {
Persistence persistence.LogStatePersistence
Signer note.Signer
Signers []note.Signer
KnownLogs map[string]LogInfo
}

Expand All @@ -56,8 +56,8 @@ type LogInfo struct {
// Witness consists of a database for storing checkpoints, a signer, and a list
// of logs for which it stores and verifies checkpoints.
type Witness struct {
lsp persistence.LogStatePersistence
Signer note.Signer
lsp persistence.LogStatePersistence
Signers []note.Signer
// At some point we might want to store this information in a table in
// the database too but as I imagine it being populated from a static
// config file it doesn't seem very urgent to do that.
Expand All @@ -71,9 +71,9 @@ func New(wo Opts) (*Witness, error) {
return nil, fmt.Errorf("Persistence.Init(): %v", err)
}
return &Witness{
lsp: wo.Persistence,
Signer: wo.Signer,
Logs: wo.KnownLogs,
lsp: wo.Persistence,
Signers: wo.Signers,
Logs: wo.KnownLogs,
}, nil
}

Expand Down Expand Up @@ -222,7 +222,7 @@ func (w *Witness) Update(ctx context.Context, logID string, nextRaw []byte, cPro

// signChkpt adds the witness' signature to a checkpoint.
func (w *Witness) signChkpt(n *note.Note) ([]byte, error) {
cosigned, err := note.Sign(n, w.Signer)
cosigned, err := note.Sign(n, w.Signers...)
if err != nil {
return nil, fmt.Errorf("couldn't sign checkpoint: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/witness/witness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func newWitness(t *testing.T, logs []logOpts) *Witness {
}
opts := Opts{
Persistence: inmemory.NewPersistence(),
Signer: ns,
Signers: []note.Signer{ns},
KnownLogs: logMap,
}
// Create the witness
Expand Down
Loading

0 comments on commit 91d36ea

Please sign in to comment.