From b8a8e56d4a311f15060efcd455c444949b2d20b9 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Fri, 30 Oct 2020 13:27:05 +0100 Subject: started refactoring add-entry code path according to doc --- handler.go | 9 +--- instance.go | 2 +- markup/api.md | 6 +-- reqres.go | 99 +++++++++++++++++-------------------------- server/testdata/cmd/add-entry | 4 +- type.go | 16 ++++--- x509.go | 68 +++++++++++++++++++++++++++++ 7 files changed, 127 insertions(+), 77 deletions(-) diff --git a/handler.go b/handler.go index c86281f..ae91bef 100644 --- a/handler.go +++ b/handler.go @@ -45,15 +45,10 @@ func (a appHandler) sendHTTPError(w http.ResponseWriter, statusCode int, err err func addEntry(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { glog.Info("in addEntry") - request, err := NewAddEntryRequest(r) + leaf, appendix, err := NewAddEntryRequest(i.LogParameters, r) if err != nil { return http.StatusBadRequest, err - } // request can be decoded - - leaf, appendix, err := VerifyAddEntryRequest(i.LogParameters, request) - if err != nil { - return http.StatusBadRequest, err - } // valid add-entry request + } // request is well-formed, signed, and chains back to a trust anchor trillianRequest := trillian.QueueLeafRequest{ LogId: i.LogParameters.TreeId, diff --git a/instance.go b/instance.go index 12415f0..6b67096 100644 --- a/instance.go +++ b/instance.go @@ -44,7 +44,7 @@ func NewInstance(lp *LogParameters, client trillian.TrillianLogClient, deadline return i, nil } -// NewLogParameters returns an initialized LogParameters +// NewLogParameters returns initialized log parameters using only ed25519 func NewLogParameters(treeId int64, prefix string, anchorPath, keyPath string) (*LogParameters, error) { anchorList, anchorPool, err := LoadTrustAnchors(anchorPath) if err != nil { diff --git a/markup/api.md b/markup/api.md index 60658b5..d9286bf 100644 --- a/markup/api.md +++ b/markup/api.md @@ -136,12 +136,12 @@ POST https:///st/v1/add-entry Input: - item: an `StItem` that corresponds to a valid leaf type. Only `checksum_v1` at this time. -- signature_algorithm: decimal, possible values are defined in RFC 8446 +- signature_scheme: decimal, possible values are defined in RFC 8446 [§4.2.3](https://tools.ietf.org/html/rfc8446#section-4.2.3). The serialized signature encoding follows from this. - signature: covers the submitted item. -- certificate: base-64 encoded X.509 certificate that is vouched for by a trust -anchor and which produced the above signature. +- chain: a list of base-64 encoded X.509 certificates that chain back to +a trust anchor and which produced the above signature. Output: - an `StItem` structure of type `signed_debug_info_v1` that covers the added diff --git a/reqres.go b/reqres.go index 66e07b8..59709cf 100644 --- a/reqres.go +++ b/reqres.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" + stdtls "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" @@ -16,9 +17,10 @@ import ( // AddEntryRequest is a collection of add-entry input parameters type AddEntryRequest struct { - Item string `json:"item"` // base64-encoded StItem - Signature string `json:"signature"` // base64-encoded DigitallySigned - Certificate string `json:"certificate"` // base64-encoded X.509 certificate + Item string `json:"item"` // base64-encoded StItem + Signature string `json:"signature"` // base64-encoded DigitallySigned + SignatureScheme uint16 `json:"signature_scheme"` // RFC 8446, §4.2.3 + Chain []string `json:"chain"` // base64-encoded X.509 certificates } // GetEntriesRequest is a collection of get-entry input parameters @@ -48,24 +50,46 @@ type GetEntryResponse struct { } // NewAddEntryRequest parses and sanitizes the JSON-encoded add-entry -// parameters from an incoming HTTP post. The resulting AddEntryRequest is -// well-formed, but not necessarily trusted (further sanitization is needed). -func NewAddEntryRequest(r *http.Request) (AddEntryRequest, error) { - var ret AddEntryRequest - if err := UnpackJsonPost(r, &ret); err != nil { - return ret, err +// parameters from an incoming HTTP post. The serialized leaf value and +// associated appendix is returned if the submitted data is valid: well-formed, +// signed using a supported scheme, and chains back to a valid trust anchor. +func NewAddEntryRequest(lp *LogParameters, r *http.Request) ([]byte, []byte, error) { + var entry AddEntryRequest + if err := UnpackJsonPost(r, &entry); err != nil { + return nil, nil, err } - item, err := StItemFromB64(ret.Item) + item, err := StItemFromB64(entry.Item) if err != nil { - return ret, fmt.Errorf("failed decoding StItem: %v", err) + return nil, nil, fmt.Errorf("StItem(%s): %v", item.Format, err) } if item.Format != StFormatChecksumV1 { - return ret, fmt.Errorf("invalid StItem format: %s", item.Format) + return nil, nil, fmt.Errorf("invalid StItem format: %s", item.Format) + } // note that decode would have failed if invalid checksum/package length + + chain, err := buildChainFromB64List(lp, entry.Chain) + if err != nil { + return nil, nil, fmt.Errorf("invalid certificate chain: %v", err) + } // the final entry in chain is a valid trust anchor + + signature, err := base64.StdEncoding.DecodeString(entry.Signature) + if err != nil { + return nil, nil, fmt.Errorf("invalid signature encoding: %v", err) + } + serialized, err := tls.Marshal(item) + if err != nil { + return nil, nil, fmt.Errorf("failed tls marshaling StItem(%s): %v", item.Format, err) + } + if err := verifySignature(lp, chain[0], stdtls.SignatureScheme(entry.SignatureScheme), serialized, signature); err != nil { + return nil, nil, fmt.Errorf("invalid signature: %v", err) + } + + extra, err := tls.Marshal(NewAppendix(chain, signature, entry.SignatureScheme)) + if err != nil { + return nil, nil, fmt.Errorf("failed tls marshaling appendix: %v", err) } - // TODO: verify that we got a checksum length - // TODO: verify that we got a signature and certificate - return ret, nil + + return serialized, extra, nil } // NewGetEntriesRequest parses and sanitizes the URL-encoded get-entries @@ -173,51 +197,6 @@ func NewGetAnchorsResponse(anchors []*x509.Certificate) []string { return certificates } -// VerifyAddEntryRequest determines whether a well-formed AddEntryRequest should -// be inserted into the log. The corresponding leaf and appendix is returned. -func VerifyAddEntryRequest(ld *LogParameters, r AddEntryRequest) ([]byte, []byte, error) { - item, err := StItemFromB64(r.Item) - if err != nil { - return nil, nil, fmt.Errorf("failed decoding StItem: %v", err) - } - - leaf, err := tls.Marshal(item) - if err != nil { - return nil, nil, fmt.Errorf("failed tls marshaling StItem: %v", err) - } // leaf is the serialized data that should be added to the tree - - c, err := base64.StdEncoding.DecodeString(r.Certificate) - if err != nil { - return nil, nil, fmt.Errorf("failed decoding certificate: %v", err) - } - certificate, err := x509.ParseCertificate(c) - if err != nil { - return nil, nil, fmt.Errorf("failed decoding certificate: %v", err) - } // certificate is the end-entity certificate that signed leaf - - chain, err := VerifyChain(ld, certificate) - if err != nil { - return nil, nil, fmt.Errorf("chain verification failed: %v", err) - } // chain is a valid path to some trust anchor - - signature, err := base64.StdEncoding.DecodeString(r.Signature) - if err != nil { - return nil, nil, fmt.Errorf("failed decoding signature: %v", err) - } - if err := VerifySignature(leaf, signature, certificate); err != nil { - return nil, nil, fmt.Errorf("signature verification failed: %v", err) - } // signature is valid for certificate - - // TODO: update doc of what signature "is", i.e., w/e x509 does - // TODO: doc in markdown/api.md what signature schemes we expect - appendix, err := tls.Marshal(NewAppendix(chain, signature)) - if err != nil { - return nil, nil, fmt.Errorf("failed tls marshaling appendix: %v", err) - } - - return leaf, appendix, nil -} - // UnpackJsonPost unpacks a json-encoded HTTP POST request into `unpack` func UnpackJsonPost(r *http.Request, unpack interface{}) error { body, err := ioutil.ReadAll(r.Body) diff --git a/server/testdata/cmd/add-entry b/server/testdata/cmd/add-entry index d5b404d..0935614 100755 --- a/server/testdata/cmd/add-entry +++ b/server/testdata/cmd/add-entry @@ -4,6 +4,7 @@ set -eo pipefail source config algo=ecdsa +signature_scheme=1027 key_path="../chain/rgdd-$algo.key" cert_path="../chain/rgdd-$algo.pem" name="foobar-1.2.3" @@ -19,9 +20,10 @@ pushd ../entry >/dev/null openssl dgst -sha256 -sign $key_path -out stitem/$name.sig stitem/$name openssl base64 -A -in stitem/$name -out stitem/$name.b64 openssl base64 -A -in stitem/$name.sig -out stitem/$name.sig.b64 - json=$(printf '{"item":"%s","signature":"%s","certificate":"%s"}'\ + json=$(printf '{"item":"%s","signature":"%s","signature_scheme":%s,"chain":["%s"]}'\ $(cat stitem/$name.b64)\ $(cat stitem/$name.sig.b64)\ + $signature_scheme\ $(cat $cert_path |\ sed '1,1d;$ d' |\ xargs |\ diff --git a/type.go b/type.go index 060ce30..5a32964 100644 --- a/type.go +++ b/type.go @@ -272,8 +272,9 @@ func StItemToB64(i StItem) (string, error) { // Appendix is extra data that Trillian can store about a leaf type Appendix struct { - Signature []byte `tls:"minlen:0,maxlen:16383"` - Chain []RawCertificate `tls:"minlen:0,maxlen:65535"` + Signature []byte `tls:"minlen:0,maxlen:16383"` + SignatureScheme uint16 + Chain []RawCertificate `tls:"minlen:0,maxlen:65535"` } // RawCertificate is a serialized X.509 certificate @@ -282,10 +283,15 @@ type RawCertificate struct { } // NewAppendix creates a new leaf Appendix for an X.509 chain and signature -func NewAppendix(x509Chain []*x509.Certificate, signature []byte) Appendix { - chain := make([]RawCertificate, 0, 2) // TODO: base length on config param +func NewAppendix(x509Chain []*x509.Certificate, signature []byte, signatureScheme uint16) Appendix { + chain := make([]RawCertificate, 0, len(x509Chain)) for _, c := range x509Chain { chain = append(chain, RawCertificate{c.Raw}) } - return Appendix{Signature: signature, Chain: chain} + + return Appendix{ + Signature: signature, + Chain: chain, + SignatureScheme: signatureScheme, + } } diff --git a/x509.go b/x509.go index 04b3534..b78e5e5 100644 --- a/x509.go +++ b/x509.go @@ -8,7 +8,9 @@ import ( "crypto/ed25519" "crypto/rand" "crypto/rsa" + stdtls "crypto/tls" "crypto/x509" + "encoding/base64" "encoding/pem" "io/ioutil" @@ -135,3 +137,69 @@ func GenV1STH(ld *LogParameters, th TreeHeadV1) (StItem, error) { } return NewSignedTreeHeadV1(th, ld.LogId, sig), nil } + +// ParseB64Chain parses a list of base64 DER-encoded X.509 certificates, such +// that the first (zero-index) string is interpretted as an end-entity +// certificate and the remaining ones as the an intermediate CertPool. +func ParseB64Chain(chain []string) (*x509.Certificate, *x509.CertPool, error) { + var certificate *x509.Certificate + intermediatePool := x509.NewCertPool() + for index, cert := range chain { + der, err := base64.StdEncoding.DecodeString(cert) + if err != nil { + return nil, nil, fmt.Errorf("certificate decoding failed: %v", err) + } + c, err := x509.ParseCertificate(der) + if err != nil { + return nil, nil, fmt.Errorf("certificate decoding failed: %v", err) + } + + if index == 0 { + certificate = c + } else { + intermediatePool.AddCert(c) + } + } + if certificate == nil { + return nil, nil, fmt.Errorf("certificate chain is empty") + } + return certificate, intermediatePool, nil +} + +func buildChainFromB64List(lp *LogParameters, b64chain []string) ([]*x509.Certificate, error) { + certificate, _, err := ParseB64Chain(b64chain) // TODO: use intermediatePool + if err != nil { + return nil, err + } + + opts := x509.VerifyOptions{ + Roots: lp.AnchorPool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, // TODO: move to ld + } + + chains, err := certificate.Verify(opts) + if err != nil { + return nil, fmt.Errorf("chain verification failed: %v", err) + } + if len(chains) == 0 { + return nil, fmt.Errorf("chain verification failed: no chain") + } + + chain := chains[0] // if we found multiple paths just pick the first one + // TODO: check that len(chain) is OK + + return chain, nil +} + +// verifySignature checks if signature is valid for some serialized data. The +// only supported signature scheme is ecdsa_secp256r1_sha256(0x0403), see §4.3.2 +// in RFC 8446. TODO: replace ECDSA with ed25519(0x0807) +func verifySignature(_ *LogParameters, certificate *x509.Certificate, scheme stdtls.SignatureScheme, serialized, signature []byte) error { + if scheme != stdtls.ECDSAWithP256AndSHA256 { + return fmt.Errorf("unsupported signature scheme: %v", scheme) + } + if err := certificate.CheckSignature(x509.ECDSAWithSHA256, serialized, signature); err != nil { + return fmt.Errorf("invalid signature: %v", err) + } + return nil +} -- cgit v1.2.3