Skip to content

Commit

Permalink
Sign stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCutter committed Jul 19, 2024
1 parent bf79532 commit 6a8f8ab
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 42 deletions.
83 changes: 59 additions & 24 deletions cmd/ct-example-gcp/ct.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package main

import (
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
Expand All @@ -28,10 +30,17 @@ import (
"github.com/google/certificate-transparency-go/trillian/ctfe"
"github.com/google/certificate-transparency-go/x509"
"github.com/transparency-dev/trillian-tessera/ctonly"
"golang.org/x/crypto/cryptobyte"
"k8s.io/klog/v2"
)

func parseAddChain(r *http.Request) (e *ctonly.Entry, code int, err error) {
e, code, err = parseChainOrPreChain(r.Context(), r.Body)
type log struct {
k *ecdsa.PrivateKey
add func(context.Context, *ctonly.Entry) (uint64, error)
}

func (l *log) parseAddChain(r *http.Request) (rsp []byte, code int, err error) {
e, rsp, code, err := l.parseChainOrPreChain(r.Context(), r.Body)
if err != nil {
return nil, code, err
}
Expand All @@ -41,8 +50,8 @@ func parseAddChain(r *http.Request) (e *ctonly.Entry, code int, err error) {
return
}

func parsePreChain(r *http.Request) (e *ctonly.Entry, code int, err error) {
e, code, err = parseChainOrPreChain(r.Context(), r.Body)
func (l *log) parsePreChain(r *http.Request) (rsp []byte, code int, err error) {
e, rsp, code, err := l.parseChainOrPreChain(r.Context(), r.Body)
if err != nil {
return nil, code, err
}
Expand All @@ -52,48 +61,48 @@ func parsePreChain(r *http.Request) (e *ctonly.Entry, code int, err error) {
return
}

func parseChainOrPreChain(ctx context.Context, reqBody io.ReadCloser) (e *ctonly.Entry, code int, err error) {
func (l *log) parseChainOrPreChain(ctx context.Context, reqBody io.ReadCloser) (e *ctonly.Entry, rsp []byte, code int, err error) {
body, err := io.ReadAll(reqBody)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err)
}
var req struct {
Chain [][]byte
}
if err := json.Unmarshal(body, &req); err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("failed to parse request: %w", err)
return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse request: %w", err)
}
if len(req.Chain) == 0 {
return nil, http.StatusBadRequest, fmt.Errorf("empty chain")
return nil, nil, http.StatusBadRequest, fmt.Errorf("empty chain")
}
notBefore := time.Date(2000, 0, 0, 0, 0, 0, 0, time.UTC)
notAfter := time.Date(3000, 0, 0, 0, 0, 0, 0, time.UTC)

chain, err := ctfe.ValidateChain(req.Chain, ctfe.NewCertValidationOpts(rootsPool, time.Time{}, false, false, &notBefore, &notAfter, false, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}))
if err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("invalid chain: %w", err)
return nil, nil, http.StatusBadRequest, fmt.Errorf("invalid chain: %w", err)
}
e = &ctonly.Entry{Certificate: chain[0].Raw}
issuers := chain[1:]
if isPrecert, err := ctfe.IsPrecertificate(chain[0]); err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("invalid precertificate: %w", err)
return nil, nil, http.StatusBadRequest, fmt.Errorf("invalid precertificate: %w", err)
} else if isPrecert {
if len(issuers) == 0 {
return nil, http.StatusBadRequest, fmt.Errorf("missing precertificate issuer")
return nil, nil, http.StatusBadRequest, fmt.Errorf("missing precertificate issuer")
}

var preIssuer *x509.Certificate
if ct.IsPreIssuer(issuers[0]) {
preIssuer = issuers[0]
issuers = issuers[1:]
if len(issuers) == 0 {
return nil, http.StatusBadRequest, fmt.Errorf("missing precertificate signing certificate issuer")
return nil, nil, http.StatusBadRequest, fmt.Errorf("missing precertificate signing certificate issuer")
}
}

defangedTBS, err := x509.BuildPrecertTBS(chain[0].RawTBSCertificate, preIssuer)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to build TBSCertificate: %w", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to build TBSCertificate: %w", err)
}

e.IsPrecert = true
Expand All @@ -106,34 +115,60 @@ func parseChainOrPreChain(ctx context.Context, reqBody io.ReadCloser) (e *ctonly
e.IssuerKeyHash = kh[:]
}

return e, 0, nil
}

func buildResponse(ctx context.Context, seq uint64, timestamp uint64, merkleLeaf []byte) (resp []byte, code int, err error) {
seq, err := l.add(ctx, e)
if err != nil {
klog.V(3).Infof("add: %v", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to add entry: %v", err)
}

ext, err := ctonly.Extensions{LeafIndex: seq}.Marshal()
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to encode extensions: %w", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to encode extensions: %w", err)
}
// The digitally-signed data of an SCT is technically not a MerkleTreeLeaf,
// but it's a completely identical structure, except for the second field,
// which is a SignatureType of value 0 and length 1 instead of a
// MerkleLeafType of value 0 and length 1.
sctSignature := []byte("signature")
sctSignature, err := l.digitallySign(e.MerkleLeaf(seq))
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to sign SCT: %w", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to sign SCT: %w", err)
}

rsp, err := json.Marshal(&ct.AddChainResponse{
rsp, err = json.Marshal(&ct.AddChainResponse{
SCTVersion: ct.V1,
Timestamp: uint64(timestamp),
Timestamp: uint64(e.Timestamp),
ID: []byte("LogID"),
Extensions: base64.StdEncoding.EncodeToString(ext),
Signature: sctSignature,
})
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to encode response: %w", err)
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to encode response: %w", err)
}

return rsp, http.StatusOK, nil
return e, rsp, http.StatusOK, nil
}

// digitallySign produces an encoded digitally-signed signature.
//
// It reimplements tls.CreateSignature and tls.Marshal from
// github.com/google/certificate-transparency-go/tls, in part to limit
// complexity and in part because tls.CreateSignature expects non-pointer
// {rsa,ecdsa}.PrivateKey types, which is unusual.
//
// We use deterministic RFC 6979 ECDSA signatures so that when fetching a
// previous SCT's timestamp and index from the deduplication cache, the new SCT
// we produce is identical.
func (l *log) digitallySign(msg []byte) ([]byte, error) {
h := sha256.Sum256(msg)
sig, err := ecdsa.SignASN1(rand.Reader, l.k, h[:])
if err != nil {
return nil, err
}
var b cryptobyte.Builder
b.AddUint8(4 /* hash = sha256 */)
b.AddUint8(3 /* signature = ecdsa */)
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(sig)
})
return b.Bytes()
}
45 changes: 28 additions & 17 deletions cmd/ct-example-gcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package main

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"flag"
"fmt"
"net/http"
Expand All @@ -27,6 +30,7 @@ import (
"github.com/google/certificate-transparency-go/x509util"
tessera "github.com/transparency-dev/trillian-tessera"
"github.com/transparency-dev/trillian-tessera/storage/gcp"
"golang.org/x/crypto/hkdf"
"golang.org/x/mod/sumdb/note"
"k8s.io/klog/v2"
)
Expand All @@ -53,35 +57,36 @@ func main() {
Bucket: *bucket,
Spanner: *spanner,
}
storage, err := gcp.New(ctx, gcpCfg, tessera.WithCheckpointSigner(signerFromFlags()))
signer, logK := signerFromFlags()
storage, err := gcp.New(ctx, gcpCfg, tessera.WithCheckpointSigner(signer))
add := tessera.NewCertificateTransparencySequencedWriter(storage)
if err != nil {
klog.Exitf("Failed to create new GCP storage: %v", err)
}

handler := func(w http.ResponseWriter, r *http.Request) {
e, code, err := parseChainOrPreChain(r.Context(), r.Body)
l := log{
k: logK,
add: add,
}

http.HandleFunc("POST /ct/v1/add-chain", func(w http.ResponseWriter, r *http.Request) {
rsp, code, err := l.parseAddChain(r)
if err != nil {
klog.V(3).Infof("parseChain: %v", err)
klog.V(3).Infof("parseAddChain: %v", err)
http.Error(w, err.Error(), code)
return
}
seq, err := add(r.Context(), e)
if err != nil {
klog.V(3).Infof("add: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rsp, code, err := buildResponse(r.Context(), seq, e.Timestamp, e.MerkleLeafHash(seq))
_, _ = w.Write(rsp)
})
http.HandleFunc("POST /ct/v1/add-pre-chain", func(w http.ResponseWriter, r *http.Request) {
rsp, code, err := l.parsePreChain(r)
if err != nil {
klog.V(3).Infof("buildResponse: %v", err)
klog.V(3).Infof("parsePreChain: %v", err)
http.Error(w, err.Error(), code)
return
}
_, _ = w.Write(rsp)
}
http.HandleFunc("POST /ct/v1/add-chain", handler)
http.HandleFunc("POST /ct/v1/add-pre-chain", handler)
})

// TODO: remove this proxy
serveGCS := func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -103,7 +108,7 @@ func main() {
}
}

func signerFromFlags() note.Signer {
func signerFromFlags() (note.Signer, *ecdsa.PrivateKey) {
raw, err := os.ReadFile(*signer)
if err != nil {
klog.Exitf("Failed to read secret key file %q: %v", *signer, err)
Expand All @@ -112,7 +117,13 @@ func signerFromFlags() note.Signer {
if err != nil {
klog.Exitf("Failed to create new signer: %v", err)
}
return signer

// Look away now. 🙈
logK, err := ecdsa.GenerateKey(elliptic.P256(), hkdf.New(crypto.SHA256.New, raw, []byte("log key"), nil))
if err != nil {
panic(fmt.Errorf("failed to generate log key: %v", err))
}
return signer, logK
}

func getRoots() *x509util.PEMCertPool {
Expand Down
6 changes: 5 additions & 1 deletion ctonly/ct.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ func (c Entry) LeafData(idx uint64) []byte {
// Note that we embed an SCT extension which captures the index of the entry in the log according to
// the mechanism specified in https://c2sp.org/ct-static-api.
func (c Entry) MerkleLeafHash(leafIndex uint64) []byte {
return rfc6962.DefaultHasher.HashLeaf(c.MerkleLeaf(leafIndex))
}

func (c Entry) MerkleLeaf(leafIndex uint64) []byte {
b := &cryptobyte.Builder{}
b.AddUint8(0 /* version = v1 */)
b.AddUint8(0 /* leaf_type = timestamped_entry */)
Expand All @@ -106,7 +110,7 @@ func (c Entry) MerkleLeafHash(leafIndex uint64) []byte {
})
}
addExtensions(b, leafIndex)
return rfc6962.DefaultHasher.HashLeaf(b.BytesOrPanic())
return b.BytesOrPanic()
}

func (c Entry) Identity() []byte {
Expand Down

0 comments on commit 6a8f8ab

Please sign in to comment.