From e7801b268c97c6b72bfcd76549ce5fd50ab0b1b5 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Tue, 27 Oct 2020 19:16:10 +0100 Subject: added ed25519 signing and SDIs --- handler.go | 13 +++++++++++- instance.go | 20 +++++++++++++++++-- reqres.go | 16 +++++++++++++++ server/main.go | 3 ++- server/testdata/chain/stfe.key | 3 +++ server/testdata/cmd/add-entry | 2 ++ type.go | 28 ++++++++++++++++++++++++++ x509.go | 45 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 server/testdata/chain/stfe.key diff --git a/handler.go b/handler.go index 6e5fe49..2a23dbb 100644 --- a/handler.go +++ b/handler.go @@ -68,7 +68,18 @@ func addEntry(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.R } // note: more detail could be provided here, see addChainInternal in ctfe glog.Infof("Queued leaf: %v", trillianResponse.QueuedLeaf.Leaf.LeafValue) - // TODO: respond with an SDI + sdi, err := GenV1SDI(i.LogParameters, trillianResponse.QueuedLeaf.Leaf.LeafValue) + if err != nil { + return http.StatusInternalServerError, fmt.Errorf("failed creating signed debug info: %v", err) + } + + response, err := NewAddEntryResponse(sdi) + if err != nil { + return http.StatusInternalServerError, fmt.Errorf("failed creating AddEntryResponse: %v", err) + } + if err := WriteJsonResponse(response, w); err != nil { + return http.StatusInternalServerError, err + } return http.StatusOK, nil } diff --git a/instance.go b/instance.go index d4fc004..8914a4b 100644 --- a/instance.go +++ b/instance.go @@ -2,10 +2,12 @@ package stfe import ( "crypto" - "crypto/x509" "fmt" "time" + "crypto/x509" + "crypto/sha256" + "encoding/base64" "net/http" @@ -42,18 +44,32 @@ func NewInstance(lp *LogParameters, client trillian.TrillianLogClient, deadline } // NewLogParameters returns an initialized LogParameters -func NewLogParameters(logId []byte, treeId int64, prefix string, anchorPath string) (*LogParameters, error) { +func NewLogParameters(treeId int64, prefix string, anchorPath, keyPath string) (*LogParameters, error) { anchorList, anchorPool, err := LoadTrustAnchors(anchorPath) if err != nil { return nil, err } + key, err := LoadEd25519SigningKey(keyPath) + if err != nil { + return nil, err + } + + pub, err := x509.MarshalPKIXPublicKey(key.Public()) + if err != nil { + return nil, fmt.Errorf("failed DER encoding SubjectPublicKeyInfo: %v", err) + } + hasher := sha256.New() + hasher.Write(pub) + logId := hasher.Sum(nil) + return &LogParameters{ LogId: logId, TreeId: treeId, Prefix: prefix, AnchorPool: anchorPool, AnchorList: anchorList, + Signer: key, }, nil } diff --git a/reqres.go b/reqres.go index e1f97ee..fe79d51 100644 --- a/reqres.go +++ b/reqres.go @@ -33,6 +33,11 @@ type GetProofByHashRequest struct { TreeSize int64 `json:"tree_size"` // Tree head size to base proof on } +// AddEntryResponse is an assembled add-entry response +type AddEntryResponse struct { + SignedDebugInfo string `json:"sdi"` +} + // GetEntryResponse is an assembled log entry and its associated appendix type GetEntryResponse struct { Leaf string `json:"leaf"` // base64-encoded StItem @@ -118,6 +123,17 @@ func NewGetProofByHashRequest(httpRequest *http.Request) (GetProofByHashRequest, return GetProofByHashRequest{TreeSize: treeSize, Hash: hash}, nil } +// NewAddEntryResponse assembles an add-entry response from an SDI +func NewAddEntryResponse(sdi StItem) (AddEntryResponse, error) { + b, err := tls.Marshal(sdi) + if err != nil { + return AddEntryResponse{}, fmt.Errorf("tls marshal failed: %v", err) + } + return AddEntryResponse{ + SignedDebugInfo: base64.StdEncoding.EncodeToString(b), + }, nil +} + // NewGetEntryResponse assembles a log entry and its appendix func NewGetEntryResponse(leaf, appendix []byte) (GetEntryResponse, error) { var app Appendix diff --git a/server/main.go b/server/main.go index 84d92ea..924cfc9 100644 --- a/server/main.go +++ b/server/main.go @@ -20,6 +20,7 @@ var ( trillianID = flag.Int64("trillian_id", 5991359069696313945, "log identifier in the Trillian database") rpcDeadline = flag.Duration("rpc_deadline", time.Second*10, "deadline for backend RPC requests") anchorPath = flag.String("anchor_path", "testdata/chain/rgdd-root.pem", "path to a file containing PEM-encoded X.509 root certificates") + keyPath = flag.String("key_path", "testdata/chain/stfe.key", "path to a PEM-encoded ed25519 signing key") ) func main() { @@ -37,7 +38,7 @@ func main() { mux := http.NewServeMux() http.Handle("/", mux) - lp, err := stfe.NewLogParameters([]byte("rgdd"), *trillianID, *prefix, *anchorPath) + lp, err := stfe.NewLogParameters(*trillianID, *prefix, *anchorPath, *keyPath) if err != nil { glog.Fatalf("failed setting up log parameters: %v", err) } diff --git a/server/testdata/chain/stfe.key b/server/testdata/chain/stfe.key new file mode 100644 index 0000000..ffc5df4 --- /dev/null +++ b/server/testdata/chain/stfe.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIAhqlhKgY/TiEyTIe5BcZKLELGa2kODtJ3S+oMP4JwsA +-----END PRIVATE KEY----- diff --git a/server/testdata/cmd/add-entry b/server/testdata/cmd/add-entry index 9efd3bf..d5b404d 100755 --- a/server/testdata/cmd/add-entry +++ b/server/testdata/cmd/add-entry @@ -31,3 +31,5 @@ popd >/dev/null info "sending add-entry request" curl --header "application/json" --request POST --data $json $base_url/add-entry +newline +# TODO: try decoding and verifying signature diff --git a/type.go b/type.go index a629259..726b215 100644 --- a/type.go +++ b/type.go @@ -25,6 +25,7 @@ const ( // StItem references a versioned item based on a given format specifier. type StItem struct { Format StFormat `tls:"maxval:65535"` + SignedDebugInfoV1 *SignedDebugInfoV1 `tls:"selector:Format,val:2"` InclusionProofV1 *InclusionProofV1 `tls:"selector:Format,val:4"` ChecksumV1 *ChecksumV1 `tls:"selector:Format,val:5"` // TODO: add more items @@ -44,11 +45,32 @@ type InclusionProofV1 struct { InclusionPath []NodeHash `tls:"minlen:1,maxlen:65535"` } +// SignedDebugInfoV1 is a signed statement that we intend (but do not promise) +// to insert an entry into the log. Only Ed25519 signatures are supported. +// TODO: double-check that crypto/ed25519 encodes signature as in RFC 8032 +// TODO: need to think about signature format, then update markdown/api.md +type SignedDebugInfoV1 struct { + LogId []byte `tls:"minlen:32,maxlen:127"` + Message []byte `tls:"minlen:0,maxlen:65535"` + Signature []byte `tls:"minlen:0,maxlen:65535"` // defined in RFC 8032 +} + // NodeHash is a hashed Merkle tree node, see RFC 6962/bis (ยง4.9) type NodeHash struct { Data []byte `tls:"minlen:32,maxlen:255"` } +func NewSignedDebugInfoV1(logId, message, signature []byte) StItem { + return StItem{ + Format: StFormatSignedDebugInfoV1, + SignedDebugInfoV1: &SignedDebugInfoV1{ + LogId: logId, + Message: message, + Signature: signature, + }, + } +} + // NewChecksumV1 creates a new StItem of type checksum_v1 func NewChecksumV1(identifier []byte, checksum []byte) StItem { return StItem{ @@ -103,11 +125,17 @@ func (i StItem) String() string { return fmt.Sprintf("Format(%s): %s", i.Format, *i.ChecksumV1) case StFormatInclusionProofV1: return fmt.Sprintf("Format(%s): %s", i.Format, *i.InclusionProofV1) + case StFormatSignedDebugInfoV1: + return fmt.Sprintf("Format(%s): %s", i.Format, *i.SignedDebugInfoV1) default: return fmt.Sprintf("unknown StItem: %s", i.Format) } } +func (i SignedDebugInfoV1) String() string { + return fmt.Sprintf("LogId(%s) Message(%s) Signature(%s)", base64.StdEncoding.EncodeToString(i.LogId), string(i.Message), base64.StdEncoding.EncodeToString(i.Signature)) +} + func (i ChecksumV1) String() string { return fmt.Sprintf("Package(%v) Checksum(%v)", string(i.Package), base64.StdEncoding.EncodeToString(i.Checksum)) } diff --git a/x509.go b/x509.go index 4e5a4d6..1e443a1 100644 --- a/x509.go +++ b/x509.go @@ -3,6 +3,9 @@ package stfe import ( "fmt" + "crypto" + "crypto/rand" + "crypto/ed25519" "crypto/ecdsa" "crypto/rsa" "crypto/x509" @@ -44,6 +47,38 @@ func LoadTrustAnchors(path string) ([]*x509.Certificate, *x509.CertPool, error) return anchors, pool, nil } + +func LoadEd25519SigningKey(path string) (ed25519.PrivateKey, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed reading private key: %v", err) + } + + var block *pem.Block + block, data = pem.Decode(data) + if block == nil { + return nil, fmt.Errorf("private key not loaded") + } + if block.Type != "PRIVATE KEY" { + return nil, fmt.Errorf("unexpected PEM block type: %s", block.Type) + } + if len(data) != 0 { + return nil, fmt.Errorf("trailing data found after key: %v", data) + } + + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed parsing signing key: %v", err) + } + + switch t := key.(type) { + case ed25519.PrivateKey: + return key.(ed25519.PrivateKey), nil + default: + return nil, fmt.Errorf("unexpected signing key type: %v", t) + } +} + func VerifyChain(ld *LogParameters, certificate *x509.Certificate) ([]*x509.Certificate, error) { opts := x509.VerifyOptions{ Roots: ld.AnchorPool, @@ -76,3 +111,13 @@ func VerifySignature(leaf, signature []byte, certificate *x509.Certificate) erro } return nil } + + +func GenV1SDI(ld *LogParameters, leaf []byte) (StItem, error) { + // Note that ed25519 does not use the passed io.Reader + sig, err := ld.Signer.Sign(rand.Reader, leaf, crypto.Hash(0)) + if err != nil { + return StItem{}, fmt.Errorf("ed25519 signature failed: %v", err) + } + return NewSignedDebugInfoV1(ld.LogId, []byte("reserved"), sig), nil +} -- cgit v1.2.3