diff --git a/ctonly/ct.go b/ctonly/ct.go index 0e3666e0..3d4552fa 100644 --- a/ctonly/ct.go +++ b/ctonly/ct.go @@ -84,29 +84,37 @@ func (c Entry) LeafData(idx uint64) []byte { return b.BytesOrPanic() } -// MerkleLeafHash returns the RFC6962 leaf hash for this entry. +// MerkleTreeLeaf returns a RFC 6962 MerkleTreeLeaf. // // 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 { +func (e *Entry) MerkleTreeLeaf(idx uint64) []byte { b := &cryptobyte.Builder{} b.AddUint8(0 /* version = v1 */) b.AddUint8(0 /* leaf_type = timestamped_entry */) - b.AddUint64(uint64(c.Timestamp)) - if !c.IsPrecert { + b.AddUint64(uint64(e.Timestamp)) + if !e.IsPrecert { b.AddUint16(0 /* entry_type = x509_entry */) b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { - b.AddBytes(c.Certificate) + b.AddBytes(e.Certificate) }) } else { b.AddUint16(1 /* entry_type = precert_entry */) - b.AddBytes(c.IssuerKeyHash[:]) + b.AddBytes(e.IssuerKeyHash[:]) b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { - b.AddBytes(c.Certificate) + b.AddBytes(e.Certificate) }) } - addExtensions(b, leafIndex) - return rfc6962.DefaultHasher.HashLeaf(b.BytesOrPanic()) + addExtensions(b, idx) + return b.BytesOrPanic() +} + +// MerkleLeafHash returns the RFC6962 leaf hash for this entry. +// +// 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.MerkleTreeLeaf(leafIndex)) } func (c Entry) Identity() []byte { diff --git a/go.mod b/go.mod index bf786e4a..a6502060 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,17 @@ require ( github.com/RobinUS2/golang-moving-average v1.0.0 github.com/gdamore/tcell/v2 v2.7.4 github.com/globocom/go-buffer v1.2.2 + github.com/google/certificate-transparency-go v1.2.1 github.com/google/go-cmp v0.6.0 + github.com/google/trillian v1.6.0 + github.com/kylelemons/godebug v1.1.0 + github.com/prometheus/client_golang v1.19.1 github.com/rivo/tview v0.0.0-20240625185742-b0a7293b8130 + github.com/rs/cors v1.11.0 + github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce github.com/transparency-dev/formats v0.0.0-20240715203801-9ff9b9e3905f github.com/transparency-dev/merkle v0.0.2 - golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/mod v0.19.0 google.golang.org/api v0.189.0 google.golang.org/grpc v1.65.0 @@ -23,6 +29,22 @@ require ( cel.dev/expr v0.15.0 // indirect cloud.google.com/go v0.115.0 // indirect cloud.google.com/go/auth v0.7.2 // indirect + cloud.google.com/go/monitoring v1.20.1 // indirect + cloud.google.com/go/trace v1.10.9 // indirect + contrib.go.opencensus.io/exporter/stackdriver v0.13.14 // indirect + github.com/aws/aws-sdk-go v1.46.4 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/letsencrypt/pkcs11key/v4 v4.0.0 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/prometheus v0.47.2 // indirect +) + +require ( cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/iam v1.1.10 // indirect @@ -66,5 +88,5 @@ require ( google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/protobuf v1.34.2 ) diff --git a/go.sum b/go.sum index 374f5d96..8edb5b73 100644 --- a/go.sum +++ b/go.sum @@ -378,6 +378,8 @@ cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhI cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/monitoring v1.20.1 h1:XmM6uk4+mI2ZhWdI2n/2GNhJdpeQN+1VdG2UWEDhX48= +cloud.google.com/go/monitoring v1.20.1/go.mod h1:FYSe/brgfuaXiEzOQFhTjsEsJv+WePyK71X7Y8qo6uQ= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= @@ -564,6 +566,8 @@ cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/trace v1.10.9 h1:Cy6D1Zdz8up4mIPUWModTuIGDr3fh7AZaCnR+uyxpgA= +cloud.google.com/go/trace v1.10.9/go.mod h1:vtWRnvEh+d8h2xljwxVwsdxxpoWZkxcNYnJF3FuJUV8= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= @@ -610,6 +614,8 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +contrib.go.opencensus.io/exporter/stackdriver v0.13.14 h1:zBakwHardp9Jcb8sQHcHpXy/0+JIb1M8KjigCJzx7+4= +contrib.go.opencensus.io/exporter/stackdriver v0.13.14/go.mod h1:5pSSGY0Bhuk7waTHuDf4aQ8D2DrhgETRo9fy6k3Xlzc= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= @@ -632,6 +638,10 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/aws/aws-sdk-go v1.46.4 h1:48tKgtm9VMPkb6y7HuYlsfhQmoIRAsTEXTsWLVlty4M= +github.com/aws/aws-sdk-go v1.46.4/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -664,8 +674,9 @@ github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnTh github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -692,8 +703,9 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= @@ -764,6 +776,8 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbBXbLqMpq3CifMyOnDUME= +github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGkqpvSsEdiVGramgaSC6RD3tEmeE= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -808,6 +822,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/trillian v1.6.0 h1:jMBeDBIkINFvS2n6oV5maDqfRlxREAc6CW9QYWQ0qT4= +github.com/google/trillian v1.6.0/go.mod h1:Yu3nIMITzNhhMJEHjAtp6xKiu+H/iHu2Oq5FjV2mCWI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -839,10 +855,16 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= @@ -859,6 +881,10 @@ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NB github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/pkcs11key/v4 v4.0.0 h1:qLc/OznH7xMr5ARJgkZCCWk+EomQkiNTOoOF5LAgagc= +github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= @@ -869,17 +895,22 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -889,11 +920,22 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/prometheus v0.47.2 h1:jWcnuQHz1o1Wu3MZ6nMJDuTI0kU5yJp9pkxh8XEkNvI= +github.com/prometheus/prometheus v0.47.2/go.mod h1:J/bmOSjgH7lFxz2gZhrWEZs2i64vMS+HIuZfmYNhJ/M= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/tview v0.0.0-20240625185742-b0a7293b8130 h1:o1CYtoFOm6xJK3DvDAEG5wDJPLj+SoxUtUDFaQgt1iY= github.com/rivo/tview v0.0.0-20240625185742-b0a7293b8130/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= @@ -905,6 +947,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= @@ -926,6 +970,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= +github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/transparency-dev/formats v0.0.0-20240715203801-9ff9b9e3905f h1:NKx8BtgVYeC75VJqlsdn1DAcbmSSDQCeDw8by0m6sbA= github.com/transparency-dev/formats v0.0.0-20240715203801-9ff9b9e3905f/go.mod h1:D/QMvgv1kz9Q1TfUcDnUcDPsiSbtLV8q8LvTCdcvygw= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= @@ -988,8 +1034,9 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1086,6 +1133,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= @@ -1220,6 +1268,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1231,6 +1280,7 @@ golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= @@ -1623,8 +1673,10 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/personalities/sctfe/cert_checker.go b/personalities/sctfe/cert_checker.go new file mode 100644 index 00000000..fbc3ffa2 --- /dev/null +++ b/personalities/sctfe/cert_checker.go @@ -0,0 +1,196 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "bytes" + "errors" + "fmt" + "time" + + "github.com/google/certificate-transparency-go/asn1" + "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" +) + +var ( + ErrNoRFCCompliantPathFound = errors.New("no RFC compliant path to root found when trying to validate chain") +) + +// isPrecertificate tests if a certificate is a pre-certificate as defined in CT. +// An error is returned if the CT extension is present but is not ASN.1 NULL as defined +// by the spec. +func isPrecertificate(cert *x509.Certificate) (bool, error) { + for _, ext := range cert.Extensions { + if x509.OIDExtensionCTPoison.Equal(ext.Id) { + if !ext.Critical || !bytes.Equal(asn1.NullBytes, ext.Value) { + return false, fmt.Errorf("CT poison ext is not critical or invalid: %v", ext) + } + + return true, nil + } + } + + return false, nil +} + +// validateChain takes the certificate chain as it was parsed from a JSON request. Ensures all +// elements in the chain decode as X.509 certificates. Ensures that there is a valid path from the +// end entity certificate in the chain to a trusted root cert, possibly using the intermediates +// supplied in the chain. Then applies the RFC requirement that the path must involve all +// the submitted chain in the order of submission. +func validateChain(rawChain [][]byte, validationOpts CertValidationOpts) ([]*x509.Certificate, error) { + // First make sure the certs parse as X.509 + chain := make([]*x509.Certificate, 0, len(rawChain)) + intermediatePool := x509util.NewPEMCertPool() + + for i, certBytes := range rawChain { + cert, err := x509.ParseCertificate(certBytes) + if x509.IsFatal(err) { + return nil, err + } + + chain = append(chain, cert) + + // All but the first cert form part of the intermediate pool + if i > 0 { + intermediatePool.AddCert(cert) + } + } + + naStart := validationOpts.notAfterStart + naLimit := validationOpts.notAfterLimit + cert := chain[0] + + // Check whether the expiry date of the cert is within the acceptable range. + if naStart != nil && cert.NotAfter.Before(*naStart) { + return nil, fmt.Errorf("certificate NotAfter (%v) < %v", cert.NotAfter, *naStart) + } + if naLimit != nil && !cert.NotAfter.Before(*naLimit) { + return nil, fmt.Errorf("certificate NotAfter (%v) >= %v", cert.NotAfter, *naLimit) + } + + if validationOpts.acceptOnlyCA && !cert.IsCA { + return nil, errors.New("only certificates with CA bit set are accepted") + } + + now := validationOpts.currentTime + if now.IsZero() { + now = time.Now() + } + expired := now.After(cert.NotAfter) + if validationOpts.rejectExpired && expired { + return nil, errors.New("rejecting expired certificate") + } + if validationOpts.rejectUnexpired && !expired { + return nil, errors.New("rejecting unexpired certificate") + } + + // Check for unwanted extension types, if required. + // TODO(al): Refactor CertValidationOpts c'tor to a builder pattern and + // pre-calc this in there + if len(validationOpts.rejectExtIds) != 0 { + badIDs := make(map[string]bool) + for _, id := range validationOpts.rejectExtIds { + badIDs[id.String()] = true + } + for idx, ext := range cert.Extensions { + extOid := ext.Id.String() + if _, ok := badIDs[extOid]; ok { + return nil, fmt.Errorf("rejecting certificate containing extension %v at index %d", extOid, idx) + } + } + } + + // TODO(al): Refactor CertValidationOpts c'tor to a builder pattern and + // pre-calc this in there too. + if len(validationOpts.extKeyUsages) > 0 { + acceptEKUs := make(map[x509.ExtKeyUsage]bool) + for _, eku := range validationOpts.extKeyUsages { + acceptEKUs[eku] = true + } + good := false + for _, certEKU := range cert.ExtKeyUsage { + if _, ok := acceptEKUs[certEKU]; ok { + good = true + break + } + } + if !good { + return nil, fmt.Errorf("rejecting certificate without EKU in %v", validationOpts.extKeyUsages) + } + } + + // We can now do the verification. Use fairly lax options for verification, as + // CT is intended to observe certificates rather than police them. + verifyOpts := x509.VerifyOptions{ + Roots: validationOpts.trustedRoots.CertPool(), + CurrentTime: now, + Intermediates: intermediatePool.CertPool(), + DisableTimeChecks: true, + // Precertificates have the poison extension; also the Go library code does not + // support the standard PolicyConstraints extension (which is required to be marked + // critical, RFC 5280 s4.2.1.11), so never check unhandled critical extensions. + DisableCriticalExtensionChecks: true, + // Pre-issued precertificates have the Certificate Transparency EKU; also some + // leaves have unknown EKUs that should not be bounced just because the intermediate + // does not also have them (cf. https://github.com/golang/go/issues/24590) so + // disable EKU checks inside the x509 library, but we've already done our own check + // on the leaf above. + DisableEKUChecks: true, + // Path length checks get confused by the presence of an additional + // pre-issuer intermediate, so disable them. + DisablePathLenChecks: true, + DisableNameConstraintChecks: true, + DisableNameChecks: false, + KeyUsages: validationOpts.extKeyUsages, + } + + verifiedChains, err := cert.Verify(verifyOpts) + if err != nil { + return nil, err + } + + if len(verifiedChains) == 0 { + return nil, errors.New("no path to root found when trying to validate chains") + } + + // Verify might have found multiple paths to roots. Now we check that we have a path that + // uses all the certs in the order they were submitted so as to comply with RFC 6962 + // requirements detailed in Section 3.1. + for _, verifiedChain := range verifiedChains { + if chainsEquivalent(chain, verifiedChain) { + return verifiedChain, nil + } + } + + return nil, ErrNoRFCCompliantPathFound +} + +func chainsEquivalent(inChain []*x509.Certificate, verifiedChain []*x509.Certificate) bool { + // The verified chain includes a root, but the input chain may or may not include a + // root (RFC 6962 s4.1/ s4.2 "the last [certificate] is either the root certificate + // or a certificate that chains to a known root certificate"). + if len(inChain) != len(verifiedChain) && len(inChain) != (len(verifiedChain)-1) { + return false + } + + for i, certInChain := range inChain { + if !certInChain.Equal(verifiedChain[i]) { + return false + } + } + return true +} diff --git a/personalities/sctfe/cert_checker_test.go b/personalities/sctfe/cert_checker_test.go new file mode 100644 index 00000000..fff0e814 --- /dev/null +++ b/personalities/sctfe/cert_checker_test.go @@ -0,0 +1,601 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "encoding/base64" + "encoding/pem" + "strings" + "testing" + "time" + + "github.com/google/certificate-transparency-go/asn1" + "github.com/google/certificate-transparency-go/trillian/ctfe/testonly" + "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509/pkix" + "github.com/google/certificate-transparency-go/x509util" +) + +func wipeExtensions(cert *x509.Certificate) *x509.Certificate { + cert.Extensions = cert.Extensions[:0] + return cert +} + +func makePoisonNonCritical(cert *x509.Certificate) *x509.Certificate { + // Invalid as a pre-cert because poison extension needs to be marked as critical. + cert.Extensions = []pkix.Extension{{Id: x509.OIDExtensionCTPoison, Critical: false, Value: asn1.NullBytes}} + return cert +} + +func makePoisonNonNull(cert *x509.Certificate) *x509.Certificate { + // Invalid as a pre-cert because poison extension is not ASN.1 NULL value. + cert.Extensions = []pkix.Extension{{Id: x509.OIDExtensionCTPoison, Critical: false, Value: []byte{0x42, 0x42, 0x42}}} + return cert +} + +func TestIsPrecertificate(t *testing.T) { + var tests = []struct { + desc string + cert *x509.Certificate + wantPrecert bool + wantErr bool + }{ + { + desc: "valid-precert", + cert: pemToCert(t, testonly.PrecertPEMValid), + wantPrecert: true, + }, + { + desc: "valid-cert", + cert: pemToCert(t, testonly.CACertPEM), + wantPrecert: false, + }, + { + desc: "remove-exts-from-precert", + cert: wipeExtensions(pemToCert(t, testonly.PrecertPEMValid)), + wantPrecert: false, + }, + { + desc: "poison-non-critical", + cert: makePoisonNonCritical(pemToCert(t, testonly.PrecertPEMValid)), + wantPrecert: false, + wantErr: true, + }, + { + desc: "poison-non-null", + cert: makePoisonNonNull(pemToCert(t, testonly.PrecertPEMValid)), + wantPrecert: false, + wantErr: true, + }, + } + + for _, test := range tests { + gotPrecert, err := isPrecertificate(test.cert) + t.Run(test.desc, func(t *testing.T) { + if err != nil { + if !test.wantErr { + t.Errorf("IsPrecertificate()=%v,%v; want %v,nil", gotPrecert, err, test.wantPrecert) + } + return + } + if test.wantErr { + t.Errorf("IsPrecertificate()=%v,%v; want _,%v", gotPrecert, err, test.wantErr) + } + if gotPrecert != test.wantPrecert { + t.Errorf("IsPrecertificate()=%v,%v; want %v,nil", gotPrecert, err, test.wantPrecert) + } + }) + } +} + +func TestValidateChain(t *testing.T) { + fakeCARoots := x509util.NewPEMCertPool() + if !fakeCARoots.AppendCertsFromPEM([]byte(testonly.FakeCACertPEM)) { + t.Fatal("failed to load fake root") + } + if !fakeCARoots.AppendCertsFromPEM([]byte(testonly.FakeRootCACertPEM)) { + t.Fatal("failed to load fake root") + } + if !fakeCARoots.AppendCertsFromPEM([]byte(testonly.CACertPEM)) { + t.Fatal("failed to load CA root") + } + if !fakeCARoots.AppendCertsFromPEM([]byte(testonly.RealPrecertIntermediatePEM)) { + t.Fatal("failed to load real intermediate") + } + validateOpts := CertValidationOpts{ + trustedRoots: fakeCARoots, + } + + var tests = []struct { + desc string + chain [][]byte + wantErr bool + wantPathLen int + modifyOpts func(v *CertValidationOpts) + }{ + { + desc: "missing-intermediate-cert", + chain: pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM}), + wantErr: true, + }, + { + desc: "wrong-cert-order", + chain: pemsToDERChain(t, []string{testonly.FakeIntermediateCertPEM, testonly.LeafSignedByFakeIntermediateCertPEM}), + wantErr: true, + }, + { + desc: "unrelated-cert-in-chain", + chain: pemsToDERChain(t, []string{testonly.FakeIntermediateCertPEM, testonly.TestCertPEM}), + wantErr: true, + }, + { + desc: "unrelated-cert-after-chain", + chain: pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM, testonly.TestCertPEM}), + wantErr: true, + }, + { + desc: "valid-chain", + chain: pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM}), + wantPathLen: 3, + }, + { + desc: "valid-chain-with-policyconstraints", + chain: pemsToDERChain(t, []string{testonly.LeafCertPEM, testonly.FakeIntermediateWithPolicyConstraintsCertPEM}), + wantPathLen: 3, + }, + { + desc: "valid-chain-with-policyconstraints-inc-root", + chain: pemsToDERChain(t, []string{testonly.LeafCertPEM, testonly.FakeIntermediateWithPolicyConstraintsCertPEM, testonly.FakeRootCACertPEM}), + wantPathLen: 3, + }, + { + desc: "valid-chain-with-nameconstraints", + chain: pemsToDERChain(t, []string{testonly.LeafCertPEM, testonly.FakeIntermediateWithNameConstraintsCertPEM}), + wantPathLen: 3, + }, + { + desc: "chain-with-invalid-nameconstraints", + chain: pemsToDERChain(t, []string{testonly.LeafCertPEM, testonly.FakeIntermediateWithInvalidNameConstraintsCertPEM}), + wantPathLen: 3, + }, + { + desc: "chain-of-len-4", + chain: pemFileToDERChain(t, "./testdata/subleaf.chain"), + wantPathLen: 4, + }, + { + desc: "misordered-chain-of-len-4", + chain: pemFileToDERChain(t, "./testdata/subleaf.misordered.chain"), + wantErr: true, + }, + { + desc: "reject-non-existent-ext-id", + chain: pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM}), + modifyOpts: func(v *CertValidationOpts) { + // reject SubjectKeyIdentifier extension + v.rejectExtIds = []asn1.ObjectIdentifier{[]int{99, 99, 99, 99}} + }, + wantPathLen: 3, + }, + { + desc: "reject-non-existent-ext-id-precert", + chain: pemsToDERChain(t, []string{testonly.PrecertPEMValid}), + modifyOpts: func(v *CertValidationOpts) { + // reject SubjectKeyIdentifier extension + v.rejectExtIds = []asn1.ObjectIdentifier{[]int{99, 99, 99, 99}} + }, + wantPathLen: 2, + }, + { + desc: "reject-ext-id", + chain: pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM}), + wantErr: true, + modifyOpts: func(v *CertValidationOpts) { + // reject SubjectKeyIdentifier extension + v.rejectExtIds = []asn1.ObjectIdentifier{[]int{2, 5, 29, 14}} + }, + }, + { + desc: "reject-ext-id-precert", + chain: pemsToDERChain(t, []string{testonly.PrecertPEMValid}), + wantErr: true, + modifyOpts: func(v *CertValidationOpts) { + // reject SubjectKeyIdentifier extension + v.rejectExtIds = []asn1.ObjectIdentifier{[]int{2, 5, 29, 14}} + }, + }, + { + desc: "reject-eku-not-present-in-cert", + chain: pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM}), + wantErr: true, + modifyOpts: func(v *CertValidationOpts) { + // reject cert without ExtKeyUsageEmailProtection + v.extKeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection} + }, + }, + { + desc: "allow-eku-present-in-cert", + chain: pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM}), + wantPathLen: 3, + modifyOpts: func(v *CertValidationOpts) { + v.extKeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + }, + }, + { + desc: "reject-eku-not-present-in-precert", + chain: pemsToDERChain(t, []string{testonly.RealPrecertWithEKUPEM}), + wantErr: true, + modifyOpts: func(v *CertValidationOpts) { + // reject cert without ExtKeyUsageEmailProtection + v.extKeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection} + }, + }, + { + desc: "allow-eku-present-in-precert", + chain: pemsToDERChain(t, []string{testonly.RealPrecertWithEKUPEM}), + wantPathLen: 2, + modifyOpts: func(v *CertValidationOpts) { + v.extKeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + }, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + validateOpts := validateOpts + if test.modifyOpts != nil { + test.modifyOpts(&validateOpts) + } + gotPath, err := validateChain(test.chain, validateOpts) + if err != nil { + if !test.wantErr { + t.Errorf("ValidateChain()=%v,%v; want _,nil", gotPath, err) + } + return + } + if test.wantErr { + t.Errorf("ValidateChain()=%v,%v; want _,non-nil", gotPath, err) + return + } + if len(gotPath) != test.wantPathLen { + t.Errorf("|ValidateChain()|=%d; want %d", len(gotPath), test.wantPathLen) + for _, c := range gotPath { + t.Logf("Subject: %s Issuer: %s", x509util.NameToString(c.Subject), x509util.NameToString(c.Issuer)) + } + } + }) + } +} + +func TestCA(t *testing.T) { + fakeCARoots := x509util.NewPEMCertPool() + if !fakeCARoots.AppendCertsFromPEM([]byte(testonly.FakeCACertPEM)) { + t.Fatal("failed to load fake root") + } + validateOpts := CertValidationOpts{ + trustedRoots: fakeCARoots, + } + chain := pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM}) + leaf, err := x509.ParseCertificate(chain[0]) + if x509.IsFatal(err) { + t.Fatalf("Failed to parse golden certificate DER: %v", err) + } + t.Logf("Cert expiry date: %v", leaf.NotAfter) + + var tests = []struct { + desc string + chain [][]byte + caOnly bool + wantErr bool + }{ + { + desc: "end-entity, allow non-CA", + chain: chain, + }, + { + desc: "end-entity, disallow non-CA", + chain: chain, + caOnly: true, + wantErr: true, + }, + { + desc: "intermediate, allow non-CA", + chain: chain[1:], + }, + { + desc: "intermediate, disallow non-CA", + chain: chain[1:], + caOnly: true, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + validateOpts.acceptOnlyCA = test.caOnly + gotPath, err := validateChain(test.chain, validateOpts) + if err != nil { + if !test.wantErr { + t.Errorf("ValidateChain()=%v,%v; want _,nil", gotPath, err) + } + return + } + if test.wantErr { + t.Errorf("ValidateChain()=%v,%v; want _,non-nil", gotPath, err) + } + }) + } +} + +func TestNotAfterRange(t *testing.T) { + fakeCARoots := x509util.NewPEMCertPool() + if !fakeCARoots.AppendCertsFromPEM([]byte(testonly.FakeCACertPEM)) { + t.Fatal("failed to load fake root") + } + validateOpts := CertValidationOpts{ + trustedRoots: fakeCARoots, + rejectExpired: false, + } + + chain := pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM}) + + var tests = []struct { + desc string + chain [][]byte + notAfterStart time.Time + notAfterLimit time.Time + wantErr bool + }{ + { + desc: "valid-chain, no range", + chain: chain, + }, + { + desc: "valid-chain, valid range", + chain: chain, + notAfterStart: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), + notAfterLimit: time.Date(2020, 7, 1, 0, 0, 0, 0, time.UTC), + }, + { + desc: "before valid range", + chain: chain, + notAfterStart: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + wantErr: true, + }, + { + desc: "after valid range", + chain: chain, + notAfterLimit: time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC), + wantErr: true, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + if !test.notAfterStart.IsZero() { + validateOpts.notAfterStart = &test.notAfterStart + } + if !test.notAfterLimit.IsZero() { + validateOpts.notAfterLimit = &test.notAfterLimit + } + gotPath, err := validateChain(test.chain, validateOpts) + if err != nil { + if !test.wantErr { + t.Errorf("ValidateChain()=%v,%v; want _,nil", gotPath, err) + } + return + } + if test.wantErr { + t.Errorf("ValidateChain()=%v,%v; want _,non-nil", gotPath, err) + } + }) + } +} + +func TestRejectExpiredUnexpired(t *testing.T) { + fakeCARoots := x509util.NewPEMCertPool() + // Validity period: Jul 11, 2016 - Jul 11, 2017. + if !fakeCARoots.AppendCertsFromPEM([]byte(testonly.FakeCACertPEM)) { + t.Fatal("failed to load fake root") + } + // Validity period: May 13, 2016 - Jul 12, 2019. + chain := pemsToDERChain(t, []string{testonly.LeafSignedByFakeIntermediateCertPEM, testonly.FakeIntermediateCertPEM}) + validateOpts := CertValidationOpts{ + trustedRoots: fakeCARoots, + extKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + beforeValidPeriod := time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC) + currentValidPeriod := time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC) + afterValidPeriod := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + + for _, tc := range []struct { + desc string + rejectExpired bool + rejectUnexpired bool + now time.Time + wantErr string + }{ + // No flags: accept anything. + { + desc: "no-reject-current", + now: currentValidPeriod, + }, + { + desc: "no-reject-after", + now: afterValidPeriod, + }, + { + desc: "no-reject-before", + now: beforeValidPeriod, + }, + // Reject-Expired: only allow currently-valid and not yet valid + { + desc: "reject-expired-current", + rejectExpired: true, + now: currentValidPeriod, + }, + { + desc: "reject-expired-after", + rejectExpired: true, + now: afterValidPeriod, + wantErr: "rejecting expired certificate", + }, + { + desc: "reject-expired-before", + rejectExpired: true, + now: beforeValidPeriod, + }, + // Reject-Unexpired: only allow expired + { + desc: "reject-non-expired-after", + rejectUnexpired: true, + now: afterValidPeriod, + }, + { + desc: "reject-non-expired-before", + rejectUnexpired: true, + now: beforeValidPeriod, + wantErr: "rejecting unexpired certificate", + }, + { + desc: "reject-non-expired-current", + rejectUnexpired: true, + now: currentValidPeriod, + wantErr: "rejecting unexpired certificate", + }, + // Reject-Expired AND Reject-Unexpired: nothing allowed + { + desc: "reject-all-after", + rejectExpired: true, + rejectUnexpired: true, + now: afterValidPeriod, + wantErr: "rejecting expired certificate", + }, + { + desc: "reject-all-before", + rejectExpired: true, + rejectUnexpired: true, + now: beforeValidPeriod, + wantErr: "rejecting unexpired certificate", + }, + { + desc: "reject-all-current", + rejectExpired: true, + rejectUnexpired: true, + now: currentValidPeriod, + wantErr: "rejecting unexpired certificate", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + validateOpts.currentTime = tc.now + validateOpts.rejectExpired = tc.rejectExpired + validateOpts.rejectUnexpired = tc.rejectUnexpired + _, err := validateChain(chain, validateOpts) + if err != nil { + if len(tc.wantErr) == 0 { + t.Errorf("ValidateChain()=_,%v; want _,nil", err) + } else if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("ValidateChain()=_,%v; want err containing %q", err, tc.wantErr) + } + } else if len(tc.wantErr) != 0 { + t.Errorf("ValidateChain()=_,nil; want err containing %q", tc.wantErr) + } + }) + } +} + +// Builds a chain of DER-encoded certs. +// Note: ordering is important +func pemsToDERChain(t *testing.T, pemCerts []string) [][]byte { + t.Helper() + chain := make([][]byte, 0, len(pemCerts)) + for _, pemCert := range pemCerts { + cert := pemToCert(t, pemCert) + chain = append(chain, cert.Raw) + } + return chain +} + +func pemToCert(t *testing.T, pemData string) *x509.Certificate { + t.Helper() + bytes, rest := pem.Decode([]byte(pemData)) + if len(rest) > 0 { + t.Fatalf("Extra data after PEM: %v", rest) + return nil + } + + cert, err := x509.ParseCertificate(bytes.Bytes) + if x509.IsFatal(err) { + t.Fatal(err) + } + + return cert +} + +func pemFileToDERChain(t *testing.T, filename string) [][]byte { + t.Helper() + rawChain, err := x509util.ReadPossiblePEMFile(filename, "CERTIFICATE") + if err != nil { + t.Fatalf("failed to load testdata: %v", err) + } + return rawChain +} + +// Validate a chain including a pre-issuer as produced by Google's Compliance Monitor. +func TestCMPreIssuedCert(t *testing.T) { + var b64Chain = []string{ + "MIID+jCCAuKgAwIBAgIHBWW7shJizTANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJHQjEPMA0GA1UEBwwGTG9uZG9uMTowOAYDVQQKDDFHb29nbGUgQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5IChQcmVjZXJ0IFNpZ25pbmcpMRkwFwYDVQQFExAxNTE5MjMxNzA0MTczNDg3MB4XDTE4MDIyMTE2NDgyNFoXDTE4MTIwMTIwMzMyN1owYzELMAkGA1UEBhMCR0IxDzANBgNVBAcMBkxvbmRvbjEoMCYGA1UECgwfR29vZ2xlIENlcnRpZmljYXRlIFRyYW5zcGFyZW5jeTEZMBcGA1UEBRMQMTUxOTIzMTcwNDM5MjM5NzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKnKP9TP6hkEuD+d1rPeA8mxo5xFYffhCcEitP8PtTl7G2RqFrndPeAkzgvOxPB3Jrhx7LtMtg0IvS8y7Sy1qDqDou1/OrJgwCeWMc1/KSneuGP8GTX0Rqy4z8+LsiBN/tMDbt94RuiyCeltIAaHGmsNeYXV34ayD3vSIAQbtLUOD39KqrJWO0tQ//nshBuFlebiUrDP7rirPusYYW0stJKiCKeORhHvL3/I8mCYGNO0XIWMpASH2S9LGMwg+AQM13whC1KL65EGuVs4Ta0rO+Tl8Yi0is0RwdUmgdSGtl0evPTzyUXbA1n1BpkLcSQ5E3RxY3O6Ge9Whvtmg9vAJiMCAwEAAaOBoDCBnTATBgNVHSUEDDAKBggrBgEFBQcDATAjBgNVHREEHDAaghhmbG93ZXJzLXRvLXRoZS13b3JsZC5jb20wDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRKCM/Ajh0Fu6FFjJ9F4gVWK2oj/jAdBgNVHQ4EFgQUVjYl6wDey3DxvmTG2HL4vdiUt+MwEwYKKwYBBAHWeQIEAwEB/wQCBQAwDQYJKoZIhvcNAQELBQADggEBAAvyEFDIAWr0URsZzrJLZEL8p6FMTzVxY/MOvGP8QMXA6xNVElxYnDPF32JERAl+poR7syByhVFcEjrw7f2FTlMc04+hT/hsYzi8cMAmfX9KA36xUBVjyqvqwofxTwoWYdf+eGZW0EG8Yp1pM7iUy9bdlh3sgdOpmT9Z5XGCRwvdW1+mctv0JMKDdWzxBqYyNMnNjvjHBmkiuHeDDGFsV2zq+wV64RwJa2eVrnkMDMV1mscL6KzNRLPP2ZpNz/8H7SPock+fk4cZrdqj+0IzFt+6ixSoKyltyD+nkbWjRGY4iyboo/nPgTQ1IQCS2OPVHWw3NijFD8hqgAnYvz0Dn+k=", + "MIIE4jCCAsqgAwIBAgIHBWW7sg8LrzANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEPMA0GA1UECAwGTG9uZG9uMRcwFQYDVQQKDA5Hb29nbGUgVUsgTHRkLjEhMB8GA1UECwwYQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5MSMwIQYDVQQDDBpNZXJnZSBEZWxheSBJbnRlcm1lZGlhdGUgMTAeFw0xODAyMjExNjQ4MjRaFw0xODEyMDEyMDM0MjdaMHUxCzAJBgNVBAYTAkdCMQ8wDQYDVQQHDAZMb25kb24xOjA4BgNVBAoMMUdvb2dsZSBDZXJ0aWZpY2F0ZSBUcmFuc3BhcmVuY3kgKFByZWNlcnQgU2lnbmluZykxGTAXBgNVBAUTEDE1MTkyMzE3MDQxNzM0ODcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCKWlc3A43kJ9IzmkCPXcsGwTxlIvtl9sNYBWlx9qqHa1i6tU6rZuH9uXAb3wsn39fqY22HzF/yrx9pd05doFfRq6dvvm4eHNFfFm4cJur1kmPe8vLKpSI/P2DPx4/mRzrHnPAI8Jo9QgKcj91AyYeB689ZFzH30ay32beo6PxQvtoJkzl+dzf9Hs1ezavS7nDCuqDnu1V1Og7J5xTHZeNyTKgD5Kx28ukmIp2wGOvg3omuInABg/ew0VxnG/txKV+69zfV9dhclU3m16L81e3RkJ8Kg4RLb0mh9X3EMn90SpJ9yw0j8FF0Esk6wxuYeUGLShUji8BPnnbactY9B6ORAgMBAAGjbTBrMBgGA1UdJQEB/wQOMAwGCisGAQQB1nkCBAQwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTpPAThgC/ChBMtJnCe8v0az6r+xjAdBgNVHQ4EFgQUSgjPwI4dBbuhRYyfReIFVitqI/4wDQYJKoZIhvcNAQEFBQADggIBAEep2uWAFsdq1nLtLWLGh7DfVPc/K+1lcqNx64ucjpVZbDnMnYKFagf2Z9rHEWqR7kwuLac5xW8woSlLa/NHmJmdg18HGUhlS+x8iMPv7dK6hfNsRFdjLZkZOFneuf9j1b0dV+rXoRvyY+Oq+lomC98bEr+g9zq+M7wJ4wS/KeaNHpPw1pBeTtCdw+1c4ZgRTOEa2OUUpkpueJ+9psD/hbp6HLF+WYijWQ0/iYSxJ4TbjTC+omKRsGhvxSLbP8cSMt3X1pJgrFK1BvH4lqqEXGDNEiVNoPCHraEa8JtMZIo47/Af13lDfp6sBdZ0lvLAVDduWgg/2RkWCbHefAe81h+cYdDS775TF2TCMTwsR6GsM9sVCbfPvHXI/pUzamRn0i0CrhyccBBdPrUhj+cXuc9kqSkLegun9D8EBDMM9va5wb1HM0ruSno+YuLtfhCdBRHr/RG2BKJi7uUDjJ8goHov/EUJmHjAIARKz74IPWRkxMrnOvGhnNa2Hz+da3hpusz0Mj4rsqv1EKTC2wbCs6Rk2MRPSxdRbywdWLSmGn249SMfXK4An+dqoRk1fwKqdXc4swoUvxnGUi5ajBaRtc6631zBTmvmSFQnvGmS42aF7q2PjfvWPIuO+d//m8KgN6o2YyjrdPDDslI2RZUE5ngOR+JynvhjYrrB7Bat1EY7", + "MIIFyDCCA7CgAwIBAgICEAEwDQYJKoZIhvcNAQEFBQAwfTELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEXMBUGA1UECgwOR29vZ2xlIFVLIEx0ZC4xITAfBgNVBAsMGENlcnRpZmljYXRlIFRyYW5zcGFyZW5jeTEhMB8GA1UEAwwYTWVyZ2UgRGVsYXkgTW9uaXRvciBSb290MB4XDTE0MDcxNzEyMjYzMFoXDTE5MDcxNjEyMjYzMFowfzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEXMBUGA1UECgwOR29vZ2xlIFVLIEx0ZC4xITAfBgNVBAsMGENlcnRpZmljYXRlIFRyYW5zcGFyZW5jeTEjMCEGA1UEAwwaTWVyZ2UgRGVsYXkgSW50ZXJtZWRpYXRlIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDB6HT+/5ru8wO7+mNFOIH6r43BwiwJZB2vQwOB8zvBV79sTIqNV7Grx5KFnSDyGRUJxZfEN7FGc96lr0vqFDlt1DbcYgVV15U+Dt4B9/+0Tz/3zeZO0kVjTg3wqvzpw6xetj2N4dlpysiFQZVAOp+dHUw9zu3xNR7dlFdDvFSrdFsgT7Uln+Pt9pXCz5C4hsSP9oC3RP7CaRtDRSQrMcNvMRi3J8XeXCXsGqMKTCRhxRGe9ruQ2Bbm5ExbmVW/ou00Fr9uSlPJL6+sDR8Li/PTW+DU9hygXSj8Zi36WI+6PuA4BHDAEt7Z5Ru/Hnol76dFeExJ0F6vjc7gUnNh7JExJgBelyz0uGORT4NhWC7SRWP/ngPFLoqcoyZMVsGGtOxSt+aVzkKuF+x64CVxMeHb9I8t3iQubpHqMEmIE1oVSCsF/AkTVTKLOeWG6N06SjoUy5fu9o+faXKMKR8hldLM5z1K6QhFsb/F+uBAuU/DWaKVEZgbmWautW06fF5I+OyoFeW+hrPTbmon4OLE3ubjDxKnyTa4yYytWSisojjfw5z58sUkbLu7KAy2+Z60m/0deAiVOQcsFkxwgzcXRt7bxN7By5Q5Bzrz8uYPjFBfBnlhqMU5RU/FNBFY7Mx4Uy8+OcMYfJQ5/A/4julXEx1HjfBj3VCyrT/noHDpBeOGiwIDAQABo1AwTjAdBgNVHQ4EFgQU6TwE4YAvwoQTLSZwnvL9Gs+q/sYwHwYDVR0jBBgwFoAU8197dUnjeEE5aiC2fGtMXMk9WEEwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAgEACFjL1UXy6S4JkGrDnz1VwTYHplFDY4bG6Q8Sh3Og6z9HJdivNft/iAQ2tIHyz0eAGCXeVPE/j1kgvz2RbnUxQd5eWdLeu/w/wiZyHxWhbTt6RhjqBVFjnx0st7n6rRt+Bw8jpugZfD11SbumVT/V20Gc45lHf2oEgbkPUcnTB9gssFz5Z4KKGs5lIHz4a20WeSJF3PJLTBefkRhHNufi/LhjpLXImwrC82g5ChBZS5XIVuJZx3VkMWiYz4emgX0YWF/JdtaB2dUQ7yrTforQ5J9b1JnJ7H/o9DsX3/ubfQ39gwDBxTicnqC+Q3Dcv3i9PvwjCNJQuGa7ygMcDEn/d6elQg2qHxtqRE02ZlOXTC0XnDAJhx7myJFA/Knv3yO9S4jG6665KG9Y88/CHkh08YLR7NYFiRmwOxjbe3lb6csl/FFmqUXvjhEzzWAxKjI09GSd9hZkB8u17Mg46eEYwF3ufIlqmYdlWufjSc2BZuaNNN6jtK6JKp8jhQUycehgtUK+NlBQOXTzu28miDdasoSH2mdR0PLDo1547+MLGdV4COvqLERTmQrYHrliicD5nFCA+CCSvGEjo0DGOmF/O8StwSmNiKJ4ppPvk2iGEdO07e0LbQI+2fbC6og2SDGXUlsbG85wqQw0A7CU1fQSqhFBuZZauDFMUvdy3v/BAIw=", + "MIIFzTCCA7WgAwIBAgIJAJ7TzLHRLKJyMA0GCSqGSIb3DQEBBQUAMH0xCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xFzAVBgNVBAoMDkdvb2dsZSBVSyBMdGQuMSEwHwYDVQQLDBhDZXJ0aWZpY2F0ZSBUcmFuc3BhcmVuY3kxITAfBgNVBAMMGE1lcmdlIERlbGF5IE1vbml0b3IgUm9vdDAeFw0xNDA3MTcxMjA1NDNaFw00MTEyMDIxMjA1NDNaMH0xCzAJBgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xFzAVBgNVBAoMDkdvb2dsZSBVSyBMdGQuMSEwHwYDVQQLDBhDZXJ0aWZpY2F0ZSBUcmFuc3BhcmVuY3kxITAfBgNVBAMMGE1lcmdlIERlbGF5IE1vbml0b3IgUm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKoWHPIgXtgaxWVIPNpCaj2y5Yj9t1ixe5PqjWhJXVNKAbpPbNHA/AoSivecBm3FTD9DfgW6J17mHb+cvbKSgYNzgTk5e2GJrnOP7yubYJpt2OCw0OILJD25NsApzcIiCvLA4aXkqkGgBq9FiVfisReNJxVu8MtxfhbVQCXZf0PpkW+yQPuF99V5Ri+grHbHYlaEN1C/HM3+t2yMR4hkd2RNXsMjViit9qCchIi/pQNt5xeQgVGmtYXyc92ftTMrmvduj7+pHq9DEYFt3ifFxE8v0GzCIE1xR/d7prFqKl/KRwAjYUcpU4vuazywcmRxODKuwWFVDrUBkGgCIVIjrMJWStH5i7WTSSTrVtOD/HWYvkXInZlSgcDvsNIG0pptJaEKSP4jUzI3nFymnoNZn6pnfdIII/XISpYSVeyl1IcdVMod8HdKoRew9CzW6f2n6KSKU5I8X5QEM1NUTmRLWmVi5c75/CvS/PzOMyMzXPf+fE2Dwbf4OcR5AZLTupqp8yCTqo7ny+cIBZ1TjcZjzKG4JTMaqDZ1Sg0T3mO/ZbbiBE3N8EHxoMWpw8OP50z1dtRRwj6qUZ2zLvngOb2EihlMO15BpVZC3Cg929c9Hdl65pUd4YrYnQBQB/rn6IvHo8zot8zElgOg22fHbViijUt3qnRggB40N30MXkYGwuJbAgMBAAGjUDBOMB0GA1UdDgQWBBTzX3t1SeN4QTlqILZ8a0xcyT1YQTAfBgNVHSMEGDAWgBTzX3t1SeN4QTlqILZ8a0xcyT1YQTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQB3HP6jRXmpdSDYwkI9aOzQeJH4x/HDi/PNMOqdNje/xdNzUy7HZWVYvvSVBkZ1DG/ghcUtn/wJ5m6/orBn3ncnyzgdKyXbWLnCGX/V61PgIPQpuGo7HzegenYaZqWz7NeXxGaVo3/y1HxUEmvmvSiioQM1cifGtz9/aJsJtIkn5umlImenKKEV1Ly7R3Uz3Cjz/Ffac1o+xU+8NpkLF/67fkazJCCMH6dCWgy6SL3AOB6oKFIVJhw8SD8vptHaDbpJSRBxifMtcop/85XUNDCvO4zkvlB1vPZ9ZmYZQdyL43NA+PkoKy0qrdaQZZMq1Jdp+Lx/yeX255/zkkILp43jFyd44rZ+TfGEQN1WHlp4RMjvoGwOX1uGlfoGkRSgBRj7TBn514VYMbXu687RS4WY2v+kny3PUFv/ZBfYSyjoNZnU4Dce9kstgv+gaKMQRPcyL+4vZU7DV8nBIfNFilCXKMN/VnNBKtDV52qmtOsVghgai+QE09w15x7dg+44gIfWFHxNhvHKys+s4BBN8fSxAMLOsb5NGFHE8x58RAkmIYWHjyPM6zB5AUPw1b2A0sDtQmCqoxJZfZUKrzyLz8gS2aVujRYN13KklHQ3EKfkeKBG2KXVBe5rjMN/7Anf1MtXxsTY6O8qIuHZ5QlXhSYzE41yIlPlG6d7AGnTiBIgeg==", + } + rawChain := make([][]byte, len(b64Chain)) + for i, b64Data := range b64Chain { + var err error + rawChain[i], err = base64.StdEncoding.DecodeString(b64Data) + if err != nil { + t.Fatalf("failed to base64.Decode(chain[%d]): %v", i, err) + } + } + + root, err := x509.ParseCertificate(rawChain[len(rawChain)-1]) + if err != nil { + t.Fatalf("failed to parse root cert: %v", err) + } + cmRoot := x509util.NewPEMCertPool() + cmRoot.AddCert(root) + + for _, tc := range []struct { + desc string + eku []x509.ExtKeyUsage + }{ + { + desc: "no EKU specified", + }, { + desc: "EKU ServerAuth", + eku: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + opts := CertValidationOpts{ + trustedRoots: cmRoot, + extKeyUsages: tc.eku, + } + chain, err := validateChain(rawChain, opts) + if err != nil { + t.Fatalf("failed to ValidateChain: %v", err) + } + for i, c := range chain { + t.Logf("chain[%d] = \n%s", i, x509util.CertificateToString(c)) + } + }) + } +} diff --git a/personalities/sctfe/cert_quota.go b/personalities/sctfe/cert_quota.go new file mode 100644 index 00000000..d4d916cc --- /dev/null +++ b/personalities/sctfe/cert_quota.go @@ -0,0 +1,43 @@ +// Copyright 2018 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + + "github.com/google/certificate-transparency-go/x509" +) + +// CertificateQuotaUserPrefix is prepended to all User quota ids association +// with intermediate certificates. +const CertificateQuotaUserPrefix = "@intermediate" + +// QuotaUserForCert returns a User quota id string for the passed in +// certificate. +// This is intended to be used for quota limiting by intermediate certificates, +// but the function does not enforce anything about the passed in cert. +// +// Format returned is: +// +// "CertificateQuotaUserPrefix Subject hex(SHA256(SubjectPublicKeyInfo)[0:5])" +// +// See tests for examples. +func QuotaUserForCert(c *x509.Certificate) string { + spkiHash := sha256.Sum256(c.RawSubjectPublicKeyInfo) + return fmt.Sprintf("%s %s %s", CertificateQuotaUserPrefix, strings.ReplaceAll(c.Subject.String(), "/", "%2F"), hex.EncodeToString(spkiHash[0:5])) +} diff --git a/personalities/sctfe/cert_quota_test.go b/personalities/sctfe/cert_quota_test.go new file mode 100644 index 00000000..3c9aa2ab --- /dev/null +++ b/personalities/sctfe/cert_quota_test.go @@ -0,0 +1,57 @@ +// Copyright 2018 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "testing" + + "github.com/google/certificate-transparency-go/trillian/ctfe/testonly" + "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" +) + +func mustDePEM(t *testing.T, pem string) *x509.Certificate { + t.Helper() + c, err := x509util.CertificateFromPEM([]byte(pem)) + if x509.IsFatal(err) { + t.Fatalf("Failed to parse PEM: %v", err) + } + return c +} + +func TestQuotaUserForCert(t *testing.T) { + for _, test := range []struct { + desc string + cert *x509.Certificate + want string + }{ + { + desc: "cacert", + cert: mustDePEM(t, testonly.CACertPEM), + want: "@intermediate O=Certificate Transparency CA,L=Erw Wen,ST=Wales,C=GB 02adddca08", + }, + { + desc: "intermediate", + cert: mustDePEM(t, testonly.FakeIntermediateCertPEM), + want: "@intermediate CN=FakeIntermediateAuthority,OU=Eng,O=Google,L=London,ST=London,C=GB 6e62e56f67", + }, + } { + t.Run(test.desc, func(t *testing.T) { + if got := QuotaUserForCert(test.cert); got != test.want { + t.Fatalf("QuotaUserForCert() = %q, want %q", got, test.want) + } + }) + } +} diff --git a/personalities/sctfe/config.go b/personalities/sctfe/config.go new file mode 100644 index 00000000..fc0458b5 --- /dev/null +++ b/personalities/sctfe/config.go @@ -0,0 +1,188 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "crypto" + "errors" + "fmt" + "os" + "time" + + "github.com/google/certificate-transparency-go/x509" + "github.com/transparency-dev/trillian-tessera/personalities/sctfe/configpb" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + "k8s.io/klog/v2" +) + +// ValidatedLogConfig represents the LogConfig with the information that has +// been successfully parsed as a result of validating it. +type ValidatedLogConfig struct { + Config *configpb.LogConfig + PubKey crypto.PublicKey + PrivKey proto.Message + KeyUsages []x509.ExtKeyUsage + NotAfterStart *time.Time + NotAfterLimit *time.Time +} + +// LogConfigSetFromFile creates a slice of LogConfigSet options from the given +// filename, which should contain text or binary-encoded protobuf configuration +// data. +func LogConfigSetFromFile(filename string) (*configpb.LogConfigSet, error) { + cfgBytes, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var cfg configpb.LogConfigSet + if txtErr := prototext.Unmarshal(cfgBytes, &cfg); txtErr != nil { + if binErr := proto.Unmarshal(cfgBytes, &cfg); binErr != nil { + return nil, fmt.Errorf("failed to parse LogConfigSet from %q as text protobuf (%v) or binary protobuf (%v)", filename, txtErr, binErr) + } + } + + if len(cfg.Config) == 0 { + return nil, errors.New("empty log config found") + } + return &cfg, nil +} + +// validateLogConfig checks that a single log config is valid. In particular: +// - A log has a private, and optionally a public key (both valid). +// - Each of NotBeforeStart and NotBeforeLimit, if set, is a valid timestamp +// proto. If both are set then NotBeforeStart <= NotBeforeLimit. +// - Merge delays (if present) are correct. +// +// Returns the validated structures (useful to avoid double validation). +func validateLogConfig(cfg *configpb.LogConfig) (*ValidatedLogConfig, error) { + if len(cfg.Origin) == 0 { + return nil, errors.New("empty log origin") + } + + vCfg := ValidatedLogConfig{Config: cfg} + + // Validate the public key. + if pubKey := cfg.PublicKey; pubKey != nil { + var err error + if vCfg.PubKey, err = x509.ParsePKIXPublicKey(pubKey.Der); err != nil { + return nil, fmt.Errorf("x509.ParsePKIXPublicKey: %w", err) + } + } + + // Validate the private key. + if cfg.PrivateKey == nil { + return nil, errors.New("empty private key") + } + privKey, err := cfg.PrivateKey.UnmarshalNew() + if err != nil { + return nil, fmt.Errorf("invalid private key: %v", err) + } + vCfg.PrivKey = privKey + + if cfg.RejectExpired && cfg.RejectUnexpired { + return nil, errors.New("rejecting all certificates") + } + + // validate storage config + if cfg.StorageConfig == nil { + return nil, errors.New("empty storage config") + } + + // Validate the extended key usages list. + if len(cfg.ExtKeyUsages) > 0 { + for _, kuStr := range cfg.ExtKeyUsages { + if ku, ok := stringToKeyUsage[kuStr]; ok { + // If "Any" is specified, then we can ignore the entire list and + // just disable EKU checking. + if ku == x509.ExtKeyUsageAny { + klog.Infof("%s: Found ExtKeyUsageAny, allowing all EKUs", cfg.Origin) + vCfg.KeyUsages = nil + break + } + vCfg.KeyUsages = append(vCfg.KeyUsages, ku) + } else { + return nil, fmt.Errorf("unknown extended key usage: %s", kuStr) + } + } + } + + // Validate the time interval. + start, limit := cfg.NotAfterStart, cfg.NotAfterLimit + if start != nil { + vCfg.NotAfterStart = &time.Time{} + if err := start.CheckValid(); err != nil { + return nil, fmt.Errorf("invalid start timestamp: %v", err) + } + *vCfg.NotAfterStart = start.AsTime() + } + if limit != nil { + vCfg.NotAfterLimit = &time.Time{} + if err := limit.CheckValid(); err != nil { + return nil, fmt.Errorf("invalid limit timestamp: %v", err) + } + *vCfg.NotAfterLimit = limit.AsTime() + } + if start != nil && limit != nil && (*vCfg.NotAfterLimit).Before(*vCfg.NotAfterStart) { + return nil, errors.New("limit before start") + } + + switch { + case cfg.MaxMergeDelaySec < 0: + return nil, errors.New("negative maximum merge delay") + case cfg.ExpectedMergeDelaySec < 0: + return nil, errors.New("negative expected merge delay") + case cfg.ExpectedMergeDelaySec > cfg.MaxMergeDelaySec: + return nil, errors.New("expected merge delay exceeds MMD") + } + + return &vCfg, nil +} + +// ValidateLogConfigSet validate each configs independently and makes sure +// there aren't any duplicate entries. +func ValidateLogConfigSet(cfg *configpb.LogConfigSet) ([]*ValidatedLogConfig, error) { + logNameMap := make(map[string]bool) + ret := []*ValidatedLogConfig{} + for _, logCfg := range cfg.Config { + vcfg, err := validateLogConfig(logCfg) + if err != nil { + return nil, fmt.Errorf("log config: %v: %v", err, logCfg) + } + if logNameMap[logCfg.Origin] { + return nil, fmt.Errorf("log config: duplicate origin: %s: %v", logCfg.Origin, logCfg) + } + logNameMap[logCfg.Origin] = true + ret = append(ret, vcfg) + } + + return ret, nil +} + +var stringToKeyUsage = map[string]x509.ExtKeyUsage{ + "Any": x509.ExtKeyUsageAny, + "ServerAuth": x509.ExtKeyUsageServerAuth, + "ClientAuth": x509.ExtKeyUsageClientAuth, + "CodeSigning": x509.ExtKeyUsageCodeSigning, + "EmailProtection": x509.ExtKeyUsageEmailProtection, + "IPSECEndSystem": x509.ExtKeyUsageIPSECEndSystem, + "IPSECTunnel": x509.ExtKeyUsageIPSECTunnel, + "IPSECUser": x509.ExtKeyUsageIPSECUser, + "TimeStamping": x509.ExtKeyUsageTimeStamping, + "OCSPSigning": x509.ExtKeyUsageOCSPSigning, + "MicrosoftServerGatedCrypto": x509.ExtKeyUsageMicrosoftServerGatedCrypto, + "NetscapeServerGatedCrypto": x509.ExtKeyUsageNetscapeServerGatedCrypto, +} diff --git a/personalities/sctfe/config_test.go b/personalities/sctfe/config_test.go new file mode 100644 index 00000000..ab280d4c --- /dev/null +++ b/personalities/sctfe/config_test.go @@ -0,0 +1,321 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/trillian/crypto/keyspb" + "github.com/transparency-dev/trillian-tessera/personalities/sctfe/configpb" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + invalidTimestamp = ×tamppb.Timestamp{Nanos: int32(1e9)} +) + +func mustMarshalAny(pb proto.Message) *anypb.Any { + ret, err := anypb.New(pb) + if err != nil { + panic(fmt.Sprintf("MarshalAny failed: %v", err)) + } + return ret +} + +func TestValidateLogConfig(t *testing.T) { + privKey := mustMarshalAny(&keyspb.PEMKeyFile{Path: "../testdata/ct-http-server.privkey.pem", Password: "dirk"}) + + for _, tc := range []struct { + desc string + cfg *configpb.LogConfig + wantErr string + }{ + { + desc: "empty-submission-prefix", + wantErr: "empty log origin", + cfg: &configpb.LogConfig{}, + }, + { + desc: "empty-private-key", + wantErr: "empty private key", + cfg: &configpb.LogConfig{Origin: "testlog"}, + }, + { + desc: "invalid-private-key", + wantErr: "invalid private key", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: &anypb.Any{}, + }, + }, + { + desc: "empty-storage-config", + wantErr: "empty storage config", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + }, + }, + { + desc: "rejecting-all", + wantErr: "rejecting all certificates", + cfg: &configpb.LogConfig{ + Origin: "testlog", + RejectExpired: true, + RejectUnexpired: true, + PrivateKey: privKey, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "unknown-ext-key-usage-1", + wantErr: "unknown extended key usage", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + ExtKeyUsages: []string{"wrong_usage"}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "unknown-ext-key-usage-2", + wantErr: "unknown extended key usage", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + ExtKeyUsages: []string{"ClientAuth", "ServerAuth", "TimeStomping"}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "unknown-ext-key-usage-3", + wantErr: "unknown extended key usage", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + ExtKeyUsages: []string{"Any "}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "invalid-start-timestamp", + wantErr: "invalid start timestamp", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + NotAfterStart: invalidTimestamp, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "invalid-limit-timestamp", + wantErr: "invalid limit timestamp", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + NotAfterLimit: invalidTimestamp, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "limit-before-start", + wantErr: "limit before start", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + NotAfterStart: ×tamppb.Timestamp{Seconds: 200}, + NotAfterLimit: ×tamppb.Timestamp{Seconds: 100}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "negative-maximum-merge", + wantErr: "negative maximum merge", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + MaxMergeDelaySec: -100, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "negative-expected-merge", + wantErr: "negative expected merge", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + ExpectedMergeDelaySec: -100, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "expected-exceeds-max", + wantErr: "expected merge delay exceeds MMD", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + MaxMergeDelaySec: 50, + ExpectedMergeDelaySec: 100, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "ok", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + // Note: Substituting an arbitrary proto.Message as a PrivateKey will not + // fail the validation because the actual key loading happens at runtime. + // TODO(pavelkalinnikov): Decouple key protos validation and loading, and + // make this test fail. + desc: "ok-not-a-key", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: mustMarshalAny(&configpb.LogConfig{}), + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "ok-ext-key-usages", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + ExtKeyUsages: []string{"ServerAuth", "ClientAuth", "OCSPSigning"}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "ok-start-timestamp", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + NotAfterStart: ×tamppb.Timestamp{Seconds: 100}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "ok-limit-timestamp", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + NotAfterLimit: ×tamppb.Timestamp{Seconds: 200}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "ok-range-timestamp", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + NotAfterStart: ×tamppb.Timestamp{Seconds: 300}, + NotAfterLimit: ×tamppb.Timestamp{Seconds: 400}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "ok-merge-delay", + cfg: &configpb.LogConfig{ + Origin: "testlog", + PrivateKey: privKey, + MaxMergeDelaySec: 86400, + ExpectedMergeDelaySec: 7200, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + vc, err := validateLogConfig(tc.cfg) + if len(tc.wantErr) == 0 && err != nil { + t.Errorf("ValidateLogConfig()=%v, want nil", err) + } + if len(tc.wantErr) > 0 && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) { + t.Errorf("ValidateLogConfig()=%v, want err containing %q", err, tc.wantErr) + } + if err == nil && vc == nil { + t.Error("err and ValidatedLogConfig are both nil") + } + // TODO(pavelkalinnikov): Test that ValidatedLogConfig is correct. + }) + } +} + +func TestValidateLogConfigSet(t *testing.T) { + privKey := mustMarshalAny(&keyspb.PEMKeyFile{Path: "../testdata/ct-http-server.privkey.pem", Password: "dirk"}) + for _, tc := range []struct { + desc string + cfg *configpb.LogConfigSet + wantErr string + }{ + // TODO(phboneff): add config for multiple storage + { + desc: "duplicate-prefix", + wantErr: "duplicate origin", + cfg: &configpb.LogConfigSet{ + Config: []*configpb.LogConfig{ + { + Origin: "pref1", + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + PrivateKey: privKey, + }, + { + Origin: "pref1", + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + PrivateKey: privKey, + }, + }, + }, + }, + { + desc: "ok-all-distinct", + cfg: &configpb.LogConfigSet{ + Config: []*configpb.LogConfig{ + { + Origin: "pref1", + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + PrivateKey: privKey, + }, + { + Origin: "pref2", + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + PrivateKey: privKey, + }, + { + Origin: "pref3", + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + PrivateKey: privKey, + }, + }, + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + _, err := ValidateLogConfigSet(tc.cfg) + if len(tc.wantErr) == 0 && err != nil { + t.Fatalf("ValidateLogConfigSet()=%v, want nil", err) + } + if len(tc.wantErr) > 0 && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) { + t.Errorf("ValidateLogConfigSet()=%v, want err containing %q", err, tc.wantErr) + } + }) + } +} diff --git a/personalities/sctfe/configpb/config.pb.go b/personalities/sctfe/configpb/config.pb.go new file mode 100644 index 00000000..15790811 --- /dev/null +++ b/personalities/sctfe/configpb/config.pb.go @@ -0,0 +1,534 @@ +// Copyright 2017 Google LLC. All Rights Reserved. +// +// 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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.21.12 +// source: configpb/config.proto + +package configpb + +import ( + keyspb "github.com/google/trillian/crypto/keyspb" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// LogConfigSet is a set of LogConfig messages. +type LogConfigSet struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Config []*LogConfig `protobuf:"bytes,1,rep,name=config,proto3" json:"config,omitempty"` +} + +func (x *LogConfigSet) Reset() { + *x = LogConfigSet{} + if protoimpl.UnsafeEnabled { + mi := &file_configpb_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LogConfigSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogConfigSet) ProtoMessage() {} + +func (x *LogConfigSet) ProtoReflect() protoreflect.Message { + mi := &file_configpb_config_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LogConfigSet.ProtoReflect.Descriptor instead. +func (*LogConfigSet) Descriptor() ([]byte, []int) { + return file_configpb_config_proto_rawDescGZIP(), []int{0} +} + +func (x *LogConfigSet) GetConfig() []*LogConfig { + if x != nil { + return x.Config + } + return nil +} + +// LogConfig describes the configuration options for a log instance. +// +// NEXT_ID: 15 +type LogConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // origin identifies the log. It will be used in its checkpoint, and + // is also its submission prefix, as per https://c2sp.org/static-ct-api + Origin string `protobuf:"bytes,1,opt,name=origin,proto3" json:"origin,omitempty"` + // Paths to the files containing root certificates that are acceptable to the + // log. The certs are served through get-roots endpoint. + RootsPemFile []string `protobuf:"bytes,2,rep,name=roots_pem_file,json=rootsPemFile,proto3" json:"roots_pem_file,omitempty"` + // The private key used for signing STHs etc. + PrivateKey *anypb.Any `protobuf:"bytes,3,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` + // The public key matching the above private key (if both are present). + // It can be specified for the convenience of test tools, but it not used + // by the server. + PublicKey *keyspb.PublicKey `protobuf:"bytes,4,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + // If reject_expired is true then the certificate validity period will be + // checked against the current time during the validation of submissions. + // This will cause expired certificates to be rejected. + RejectExpired bool `protobuf:"varint,5,opt,name=reject_expired,json=rejectExpired,proto3" json:"reject_expired,omitempty"` + // If reject_unexpired is true then CTFE rejects certificates that are either + // currently valid or not yet valid. + RejectUnexpired bool `protobuf:"varint,6,opt,name=reject_unexpired,json=rejectUnexpired,proto3" json:"reject_unexpired,omitempty"` + // If set, ext_key_usages will restrict the set of such usages that the + // server will accept. By default all are accepted. The values specified + // must be ones known to the x509 package. + ExtKeyUsages []string `protobuf:"bytes,7,rep,name=ext_key_usages,json=extKeyUsages,proto3" json:"ext_key_usages,omitempty"` + // not_after_start defines the start of the range of acceptable NotAfter + // values, inclusive. + // Leaving this unset implies no lower bound to the range. + NotAfterStart *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=not_after_start,json=notAfterStart,proto3" json:"not_after_start,omitempty"` + // not_after_limit defines the end of the range of acceptable NotAfter values, + // exclusive. + // Leaving this unset implies no upper bound to the range. + NotAfterLimit *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=not_after_limit,json=notAfterLimit,proto3" json:"not_after_limit,omitempty"` + // accept_only_ca controls whether or not *only* certificates with the CA bit + // set will be accepted. + AcceptOnlyCa bool `protobuf:"varint,10,opt,name=accept_only_ca,json=acceptOnlyCa,proto3" json:"accept_only_ca,omitempty"` + // The Maximum Merge Delay (MMD) of this log in seconds. See RFC6962 section 3 + // for definition of MMD. If zero, the log does not provide an MMD guarantee + // (for example, it is a frozen log). + MaxMergeDelaySec int32 `protobuf:"varint,11,opt,name=max_merge_delay_sec,json=maxMergeDelaySec,proto3" json:"max_merge_delay_sec,omitempty"` + // The merge delay that the underlying log implementation is able/targeting to + // provide. This option is exposed in CTFE metrics, and can be particularly + // useful to catch when the log is behind but has not yet violated the strict + // MMD limit. + // Log operator should decide what exactly EMD means for them. For example, it + // can be a 99-th percentile of merge delays that they observe, and they can + // alert on the actual merge delay going above a certain multiple of this EMD. + ExpectedMergeDelaySec int32 `protobuf:"varint,12,opt,name=expected_merge_delay_sec,json=expectedMergeDelaySec,proto3" json:"expected_merge_delay_sec,omitempty"` + // A list of X.509 extension OIDs, in dotted string form (e.g. "2.3.4.5") + // which should cause submissions to be rejected. + RejectExtensions []string `protobuf:"bytes,13,rep,name=reject_extensions,json=rejectExtensions,proto3" json:"reject_extensions,omitempty"` + // storage_config describes Trillian Tessera storage config + // + // Types that are assignable to StorageConfig: + // + // *LogConfig_Gcp + StorageConfig isLogConfig_StorageConfig `protobuf_oneof:"storage_config"` +} + +func (x *LogConfig) Reset() { + *x = LogConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_configpb_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LogConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogConfig) ProtoMessage() {} + +func (x *LogConfig) ProtoReflect() protoreflect.Message { + mi := &file_configpb_config_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LogConfig.ProtoReflect.Descriptor instead. +func (*LogConfig) Descriptor() ([]byte, []int) { + return file_configpb_config_proto_rawDescGZIP(), []int{1} +} + +func (x *LogConfig) GetOrigin() string { + if x != nil { + return x.Origin + } + return "" +} + +func (x *LogConfig) GetRootsPemFile() []string { + if x != nil { + return x.RootsPemFile + } + return nil +} + +func (x *LogConfig) GetPrivateKey() *anypb.Any { + if x != nil { + return x.PrivateKey + } + return nil +} + +func (x *LogConfig) GetPublicKey() *keyspb.PublicKey { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *LogConfig) GetRejectExpired() bool { + if x != nil { + return x.RejectExpired + } + return false +} + +func (x *LogConfig) GetRejectUnexpired() bool { + if x != nil { + return x.RejectUnexpired + } + return false +} + +func (x *LogConfig) GetExtKeyUsages() []string { + if x != nil { + return x.ExtKeyUsages + } + return nil +} + +func (x *LogConfig) GetNotAfterStart() *timestamppb.Timestamp { + if x != nil { + return x.NotAfterStart + } + return nil +} + +func (x *LogConfig) GetNotAfterLimit() *timestamppb.Timestamp { + if x != nil { + return x.NotAfterLimit + } + return nil +} + +func (x *LogConfig) GetAcceptOnlyCa() bool { + if x != nil { + return x.AcceptOnlyCa + } + return false +} + +func (x *LogConfig) GetMaxMergeDelaySec() int32 { + if x != nil { + return x.MaxMergeDelaySec + } + return 0 +} + +func (x *LogConfig) GetExpectedMergeDelaySec() int32 { + if x != nil { + return x.ExpectedMergeDelaySec + } + return 0 +} + +func (x *LogConfig) GetRejectExtensions() []string { + if x != nil { + return x.RejectExtensions + } + return nil +} + +func (m *LogConfig) GetStorageConfig() isLogConfig_StorageConfig { + if m != nil { + return m.StorageConfig + } + return nil +} + +func (x *LogConfig) GetGcp() *GCPConfig { + if x, ok := x.GetStorageConfig().(*LogConfig_Gcp); ok { + return x.Gcp + } + return nil +} + +type isLogConfig_StorageConfig interface { + isLogConfig_StorageConfig() +} + +type LogConfig_Gcp struct { + Gcp *GCPConfig `protobuf:"bytes,14,opt,name=gcp,proto3,oneof"` +} + +func (*LogConfig_Gcp) isLogConfig_StorageConfig() {} + +// GCPConfig describes Trillian Tessera GCP config +type GCPConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProjectId string `protobuf:"bytes,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + SpannerDbPath string `protobuf:"bytes,3,opt,name=spanner_db_path,json=spannerDbPath,proto3" json:"spanner_db_path,omitempty"` +} + +func (x *GCPConfig) Reset() { + *x = GCPConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_configpb_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GCPConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GCPConfig) ProtoMessage() {} + +func (x *GCPConfig) ProtoReflect() protoreflect.Message { + mi := &file_configpb_config_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GCPConfig.ProtoReflect.Descriptor instead. +func (*GCPConfig) Descriptor() ([]byte, []int) { + return file_configpb_config_proto_rawDescGZIP(), []int{2} +} + +func (x *GCPConfig) GetProjectId() string { + if x != nil { + return x.ProjectId + } + return "" +} + +func (x *GCPConfig) GetBucket() string { + if x != nil { + return x.Bucket + } + return "" +} + +func (x *GCPConfig) GetSpannerDbPath() string { + if x != nil { + return x.SpannerDbPath + } + return "" +} + +var File_configpb_config_proto protoreflect.FileDescriptor + +var file_configpb_config_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x70, 0x62, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x70, + 0x62, 0x1a, 0x1a, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2f, 0x6b, 0x65, 0x79, 0x73, 0x70, 0x62, + 0x2f, 0x6b, 0x65, 0x79, 0x73, 0x70, 0x62, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, + 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3b, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x65, 0x74, 0x12, 0x2b, 0x0a, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xa8, 0x05, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x24, 0x0a, 0x0e, + 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x6d, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x50, 0x65, 0x6d, 0x46, 0x69, + 0x6c, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x0a, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x0a, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x6b, 0x65, 0x79, 0x73, 0x70, 0x62, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x72, + 0x65, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0d, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x6e, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x72, 0x65, + 0x6a, 0x65, 0x63, 0x74, 0x55, 0x6e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x12, 0x24, 0x0a, + 0x0e, 0x65, 0x78, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x4b, 0x65, 0x79, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x73, 0x12, 0x42, 0x0a, 0x0f, 0x6e, 0x6f, 0x74, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, + 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x42, 0x0a, 0x0f, 0x6e, 0x6f, 0x74, 0x5f, 0x61, + 0x66, 0x74, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, 0x6e, 0x6f, + 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x61, + 0x63, 0x63, 0x65, 0x70, 0x74, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x63, 0x61, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x4f, 0x6e, 0x6c, 0x79, 0x43, + 0x61, 0x12, 0x2d, 0x0a, 0x13, 0x6d, 0x61, 0x78, 0x5f, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x5f, 0x64, + 0x65, 0x6c, 0x61, 0x79, 0x5f, 0x73, 0x65, 0x63, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, + 0x6d, 0x61, 0x78, 0x4d, 0x65, 0x72, 0x67, 0x65, 0x44, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x65, 0x63, + 0x12, 0x37, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x72, + 0x67, 0x65, 0x5f, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x5f, 0x73, 0x65, 0x63, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x15, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x72, 0x67, + 0x65, 0x44, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x65, 0x63, 0x12, 0x2b, 0x0a, 0x11, 0x72, 0x65, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x45, 0x78, 0x74, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x0a, 0x03, 0x67, 0x63, 0x70, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x70, 0x62, 0x2e, 0x47, + 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x03, 0x67, 0x63, 0x70, 0x42, + 0x10, 0x0a, 0x0e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x22, 0x6a, 0x0a, 0x09, 0x47, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1d, + 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, + 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, + 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x70, 0x61, 0x6e, 0x6e, 0x65, 0x72, + 0x5f, 0x64, 0x62, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x73, 0x70, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x44, 0x62, 0x50, 0x61, 0x74, 0x68, 0x42, 0x46, 0x5a, + 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x2d, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2d, 0x67, 0x6f, 0x2f, 0x74, + 0x72, 0x69, 0x6c, 0x6c, 0x69, 0x61, 0x6e, 0x2f, 0x63, 0x74, 0x66, 0x65, 0x2f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_configpb_config_proto_rawDescOnce sync.Once + file_configpb_config_proto_rawDescData = file_configpb_config_proto_rawDesc +) + +func file_configpb_config_proto_rawDescGZIP() []byte { + file_configpb_config_proto_rawDescOnce.Do(func() { + file_configpb_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_configpb_config_proto_rawDescData) + }) + return file_configpb_config_proto_rawDescData +} + +var file_configpb_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_configpb_config_proto_goTypes = []any{ + (*LogConfigSet)(nil), // 0: configpb.LogConfigSet + (*LogConfig)(nil), // 1: configpb.LogConfig + (*GCPConfig)(nil), // 2: configpb.GCPConfig + (*anypb.Any)(nil), // 3: google.protobuf.Any + (*keyspb.PublicKey)(nil), // 4: keyspb.PublicKey + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp +} +var file_configpb_config_proto_depIdxs = []int32{ + 1, // 0: configpb.LogConfigSet.config:type_name -> configpb.LogConfig + 3, // 1: configpb.LogConfig.private_key:type_name -> google.protobuf.Any + 4, // 2: configpb.LogConfig.public_key:type_name -> keyspb.PublicKey + 5, // 3: configpb.LogConfig.not_after_start:type_name -> google.protobuf.Timestamp + 5, // 4: configpb.LogConfig.not_after_limit:type_name -> google.protobuf.Timestamp + 2, // 5: configpb.LogConfig.gcp:type_name -> configpb.GCPConfig + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_configpb_config_proto_init() } +func file_configpb_config_proto_init() { + if File_configpb_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_configpb_config_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*LogConfigSet); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_configpb_config_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*LogConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_configpb_config_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*GCPConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_configpb_config_proto_msgTypes[1].OneofWrappers = []any{ + (*LogConfig_Gcp)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_configpb_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_configpb_config_proto_goTypes, + DependencyIndexes: file_configpb_config_proto_depIdxs, + MessageInfos: file_configpb_config_proto_msgTypes, + }.Build() + File_configpb_config_proto = out.File + file_configpb_config_proto_rawDesc = nil + file_configpb_config_proto_goTypes = nil + file_configpb_config_proto_depIdxs = nil +} diff --git a/personalities/sctfe/configpb/config.proto b/personalities/sctfe/configpb/config.proto new file mode 100644 index 00000000..5e445542 --- /dev/null +++ b/personalities/sctfe/configpb/config.proto @@ -0,0 +1,98 @@ +// Copyright 2017 Google LLC. All Rights Reserved. +// +// 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. + +syntax = "proto3"; + +option go_package = "github.com/google/certificate-transparency-go/trillian/ctfe/configpb"; + +package configpb; + +import "crypto/keyspb/keyspb.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/timestamp.proto"; + +// LogConfigSet is a set of LogConfig messages. +message LogConfigSet { + repeated LogConfig config = 1; +} + +// LogConfig describes the configuration options for a log instance. +// +// NEXT_ID: 15 +message LogConfig { + // origin identifies the log. It will be used in its checkpoint, and + // is also its submission prefix, as per https://c2sp.org/static-ct-api + string origin = 1; + // Paths to the files containing root certificates that are acceptable to the + // log. The certs are served through get-roots endpoint. + repeated string roots_pem_file = 2; + // The private key used for signing Checkpoints or SCTs. + google.protobuf.Any private_key = 3; + // The public key matching the above private key (if both are present). + // It can be specified for the convenience of test tools, but it not used + // by the server. + keyspb.PublicKey public_key = 4; + // If reject_expired is true then the certificate validity period will be + // checked against the current time during the validation of submissions. + // This will cause expired certificates to be rejected. + bool reject_expired = 5; + // If reject_unexpired is true then CTFE rejects certificates that are either + // currently valid or not yet valid. + bool reject_unexpired = 6; + // If set, ext_key_usages will restrict the set of such usages that the + // server will accept. By default all are accepted. The values specified + // must be ones known to the x509 package. + repeated string ext_key_usages = 7; + // not_after_start defines the start of the range of acceptable NotAfter + // values, inclusive. + // Leaving this unset implies no lower bound to the range. + google.protobuf.Timestamp not_after_start = 8; + // not_after_limit defines the end of the range of acceptable NotAfter values, + // exclusive. + // Leaving this unset implies no upper bound to the range. + google.protobuf.Timestamp not_after_limit = 9; + // accept_only_ca controls whether or not *only* certificates with the CA bit + // set will be accepted. + bool accept_only_ca = 10; + + // The Maximum Merge Delay (MMD) of this log in seconds. See RFC6962 section 3 + // for definition of MMD. If zero, the log does not provide an MMD guarantee + // (for example, it is a frozen log). + int32 max_merge_delay_sec = 11; + // The merge delay that the underlying log implementation is able/targeting to + // provide. This option is exposed in CTFE metrics, and can be particularly + // useful to catch when the log is behind but has not yet violated the strict + // MMD limit. + // Log operator should decide what exactly EMD means for them. For example, it + // can be a 99-th percentile of merge delays that they observe, and they can + // alert on the actual merge delay going above a certain multiple of this EMD. + int32 expected_merge_delay_sec = 12; + + // A list of X.509 extension OIDs, in dotted string form (e.g. "2.3.4.5") + // which should cause submissions to be rejected. + repeated string reject_extensions = 13; + + // TODO(phboneff): re-think how this could work with other storage systems + // storage_config describes Trillian Tessera storage config + oneof storage_config { + GCPConfig gcp = 14; + } +} + +// GCPConfig describes Trillian Tessera GCP config +message GCPConfig { + string project_id = 1; + string bucket = 2; + string spanner_db_path = 3; +} diff --git a/personalities/sctfe/ct_server_gcp/main.go b/personalities/sctfe/ct_server_gcp/main.go new file mode 100644 index 00000000..67d350af --- /dev/null +++ b/personalities/sctfe/ct_server_gcp/main.go @@ -0,0 +1,311 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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. + +// The ct_server binary runs the CT personality. +package main + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/tls" + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/google/certificate-transparency-go/trillian/ctfe/cache" + "github.com/google/trillian/crypto/keys" + "github.com/google/trillian/crypto/keys/der" + "github.com/google/trillian/crypto/keys/pem" + "github.com/google/trillian/crypto/keys/pkcs11" + "github.com/google/trillian/crypto/keyspb" + "github.com/google/trillian/monitoring/opencensus" + "github.com/google/trillian/monitoring/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/cors" + "github.com/tomasen/realip" + "github.com/transparency-dev/trillian-tessera/personalities/sctfe" + "github.com/transparency-dev/trillian-tessera/personalities/sctfe/configpb" + "github.com/transparency-dev/trillian-tessera/storage/gcp" + "google.golang.org/protobuf/proto" + "k8s.io/klog/v2" +) + +// Global flags that affect all log instances. +var ( + httpEndpoint = flag.String("http_endpoint", "localhost:6962", "Endpoint for HTTP (host:port)") + tlsCert = flag.String("tls_certificate", "", "Path to server TLS certificate") + tlsKey = flag.String("tls_key", "", "Path to server TLS private key") + metricsEndpoint = flag.String("metrics_endpoint", "", "Endpoint for serving metrics; if left empty, metrics will be visible on --http_endpoint") + rpcDeadline = flag.Duration("rpc_deadline", time.Second*10, "Deadline for backend RPC requests") + logConfig = flag.String("log_config", "", "File holding log config in text proto format") + maskInternalErrors = flag.Bool("mask_internal_errors", false, "Don't return error strings with Internal Server Error HTTP responses") + tracing = flag.Bool("tracing", false, "If true opencensus Stackdriver tracing will be enabled. See https://opencensus.io/.") + tracingProjectID = flag.String("tracing_project_id", "", "project ID to pass to stackdriver. Can be empty for GCP, consult docs for other platforms.") + tracingPercent = flag.Int("tracing_percent", 0, "Percent of requests to be traced. Zero is a special case to use the DefaultSampler") + quotaRemote = flag.Bool("quota_remote", true, "Enable requesting of quota for IP address sending incoming requests") + quotaIntermediate = flag.Bool("quota_intermediate", true, "Enable requesting of quota for intermediate certificates in submitted chains") + pkcs11ModulePath = flag.String("pkcs11_module_path", "", "Path to the PKCS#11 module to use for keys that use the PKCS#11 interface") + cacheType = flag.String("cache_type", "noop", "Supported cache type: noop, lru (Default: noop)") + cacheSize = flag.Int("cache_size", -1, "Size parameter set to 0 makes cache of unlimited size") + cacheTTL = flag.Duration("cache_ttl", -1*time.Second, "Providing 0 TTL turns expiring off") +) + +const unknownRemoteUser = "UNKNOWN_REMOTE" + +// nolint:staticcheck +func main() { + klog.InitFlags(nil) + flag.Parse() + ctx := context.Background() + + keys.RegisterHandler(&keyspb.PEMKeyFile{}, pem.FromProto) + keys.RegisterHandler(&keyspb.PrivateKey{}, der.FromProto) + keys.RegisterHandler(&keyspb.PKCS11Config{}, func(ctx context.Context, pb proto.Message) (crypto.Signer, error) { + if cfg, ok := pb.(*keyspb.PKCS11Config); ok { + return pkcs11.FromConfig(*pkcs11ModulePath, cfg) + } + return nil, fmt.Errorf("pkcs11: got %T, want *keyspb.PKCS11Config", pb) + }) + + cfgs, err := sctfe.LogConfigSetFromFile(*logConfig) + if err != nil { + klog.Exitf("Failed to read config: %v", err) + } + + vCfgs, err := sctfe.ValidateLogConfigSet(cfgs) + if err != nil { + klog.Exitf("Invalid config: %v", err) + } + + klog.CopyStandardLogTo("WARNING") + klog.Info("**** CT HTTP Server Starting ****") + + metricsAt := *metricsEndpoint + if metricsAt == "" { + metricsAt = *httpEndpoint + } + + // Allow cross-origin requests to all handlers registered on corsMux. + // This is safe for CT log handlers because the log is public and + // unauthenticated so cross-site scripting attacks are not a concern. + corsMux := http.NewServeMux() + corsHandler := cors.AllowAll().Handler(corsMux) + http.Handle("/", corsHandler) + + // Register handlers for all the configured logs using the correct RPC + // client. + var publicKeys []crypto.PublicKey + for _, vc := range vCfgs { + inst, err := setupAndRegister(ctx, + *rpcDeadline, + vc, + corsMux, + *maskInternalErrors, + cache.Type(*cacheType), + cache.Option{ + Size: *cacheSize, + TTL: *cacheTTL, + }, + ) + if err != nil { + klog.Exitf("Failed to set up log instance for %+v: %v", cfgs, err) + } + + // Ensure that this log does not share the same private key as any other + // log that has already been set up and registered. + if publicKey := inst.GetPublicKey(); publicKey != nil { + for _, p := range publicKeys { + switch pub := publicKey.(type) { + case *ecdsa.PublicKey: + if pub.Equal(p) { + klog.Exitf("Same private key used by more than one log") + } + case ed25519.PublicKey: + if pub.Equal(p) { + klog.Exitf("Same private key used by more than one log") + } + case *rsa.PublicKey: + if pub.Equal(p) { + klog.Exitf("Same private key used by more than one log") + } + } + } + publicKeys = append(publicKeys, publicKey) + } + } + + // Return a 200 on the root, for GCE default health checking :/ + corsMux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/" { + resp.WriteHeader(http.StatusOK) + } else { + resp.WriteHeader(http.StatusNotFound) + } + }) + + // Export a healthz target. + corsMux.HandleFunc("/healthz", func(resp http.ResponseWriter, req *http.Request) { + // TODO(al): Wire this up to tell the truth. + if _, err := resp.Write([]byte("ok")); err != nil { + klog.Errorf("resp.Write(): %v", err) + } + }) + + if metricsAt != *httpEndpoint { + // Run a separate handler for metrics. + go func() { + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + metricsServer := http.Server{Addr: metricsAt, Handler: mux} + err := metricsServer.ListenAndServe() + klog.Warningf("Metrics server exited: %v", err) + }() + } else { + // Handle metrics on the DefaultServeMux. + http.Handle("/metrics", promhttp.Handler()) + } + + // If we're enabling tracing we need to use an instrumented http.Handler. + var handler http.Handler + if *tracing { + handler, err = opencensus.EnableHTTPServerTracing(*tracingProjectID, *tracingPercent) + if err != nil { + klog.Exitf("Failed to initialize stackdriver / opencensus tracing: %v", err) + } + } + + // Bring up the HTTP server and serve until we get a signal not to. + srv := http.Server{} + if *tlsCert != "" && *tlsKey != "" { + cert, err := tls.LoadX509KeyPair(*tlsCert, *tlsKey) + if err != nil { + klog.Errorf("failed to load TLS certificate/key: %v", err) + } + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } + srv = http.Server{Addr: *httpEndpoint, Handler: handler, TLSConfig: tlsConfig} + } else { + srv = http.Server{Addr: *httpEndpoint, Handler: handler} + } + shutdownWG := new(sync.WaitGroup) + go awaitSignal(func() { + shutdownWG.Add(1) + defer shutdownWG.Done() + // Allow 60s for any pending requests to finish then terminate any stragglers + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + defer cancel() + klog.Info("Shutting down HTTP server...") + if err := srv.Shutdown(ctx); err != nil { + klog.Errorf("srv.Shutdown(): %v", err) + } + klog.Info("HTTP server shutdown") + }) + + if *tlsCert != "" && *tlsKey != "" { + err = srv.ListenAndServeTLS("", "") + } else { + err = srv.ListenAndServe() + } + if err != http.ErrServerClosed { + klog.Warningf("Server exited: %v", err) + } + // Wait will only block if the function passed to awaitSignal was called, + // in which case it'll block until the HTTP server has gracefully shutdown + shutdownWG.Wait() + klog.Flush() +} + +// awaitSignal waits for standard termination signals, then runs the given +// function; it should be run as a separate goroutine. +func awaitSignal(doneFn func()) { + // Arrange notification for the standard set of signals used to terminate a server + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + // Now block main and wait for a signal + sig := <-sigs + klog.Warningf("Signal received: %v", sig) + klog.Flush() + + doneFn() +} + +func setupAndRegister(ctx context.Context, deadline time.Duration, vCfg *sctfe.ValidatedLogConfig, mux *http.ServeMux, maskInternalErrors bool, cacheType cache.Type, cacheOption cache.Option) (*sctfe.Instance, error) { + opts := sctfe.InstanceOptions{ + Validated: vCfg, + Deadline: deadline, + MetricFactory: prometheus.MetricFactory{}, + RequestLog: new(sctfe.DefaultRequestLog), + MaskInternalErrors: maskInternalErrors, + CacheType: cacheType, + CacheOption: cacheOption, + } + if *quotaRemote { + klog.Info("Enabling quota for requesting IP") + opts.RemoteQuotaUser = func(r *http.Request) string { + var remoteUser = realip.FromRequest(r) + if len(remoteUser) == 0 { + return unknownRemoteUser + } + return remoteUser + } + } + if *quotaIntermediate { + klog.Info("Enabling quota for intermediate certificates") + opts.CertificateQuotaUser = sctfe.QuotaUserForCert + } + + switch vCfg.Config.StorageConfig.(type) { + case *configpb.LogConfig_Gcp: + storage, err := newGCPStorage(ctx, vCfg.Config.GetGcp()) + if err != nil { + return nil, fmt.Errorf("failed to initialize GCP storage: %v", err) + } + opts.Storage = storage + default: + return nil, fmt.Errorf("unrecognized storage config") + } + + inst, err := sctfe.SetUpInstance(ctx, opts) + if err != nil { + return nil, err + } + for path, handler := range inst.Handlers { + mux.Handle(path, handler) + } + return inst, nil +} + +func newGCPStorage(ctx context.Context, cfg *configpb.GCPConfig) (*sctfe.CTStorage, error) { + gcpCfg := gcp.Config{ + ProjectID: cfg.ProjectId, + Bucket: cfg.Bucket, + Spanner: cfg.SpannerDbPath, + } + storage, err := gcp.New(ctx, gcpCfg) + if err != nil { + return nil, fmt.Errorf("Failed to initialize GCP storage: %v", err) + } + return sctfe.NewCTSTorage(storage) +} diff --git a/personalities/sctfe/doc.go b/personalities/sctfe/doc.go new file mode 100644 index 00000000..b5059a24 --- /dev/null +++ b/personalities/sctfe/doc.go @@ -0,0 +1,22 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe contains a usage example by providing an implementation of a ct-static-api +personality using Trillian Tessera as a backend storage. + +It is a port of the ctfe package from +https://github.com/google/certificate-transparency-go +*/ +package sctfe diff --git a/personalities/sctfe/handlers.go b/personalities/sctfe/handlers.go new file mode 100644 index 00000000..2fe94f08 --- /dev/null +++ b/personalities/sctfe/handlers.go @@ -0,0 +1,501 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "context" + "crypto" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/google/certificate-transparency-go/asn1" + "github.com/google/certificate-transparency-go/tls" + "github.com/google/certificate-transparency-go/trillian/util" + "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" + "github.com/google/trillian/monitoring" + "github.com/transparency-dev/trillian-tessera/ctonly" + "k8s.io/klog/v2" + + ct "github.com/google/certificate-transparency-go" +) + +const ( + // HTTP content type header + contentTypeHeader string = "Content-Type" + // MIME content type for JSON + contentTypeJSON string = "application/json" +) + +// EntrypointName identifies a CT entrypoint as defined in section 4 of RFC 6962. +type EntrypointName string + +// Constants for entrypoint names, as exposed in statistics/logging. +const ( + AddChainName = EntrypointName("AddChain") + AddPreChainName = EntrypointName("AddPreChain") +) + +var ( + // Metrics are all per-log (label "origin"), but may also be + // per-entrypoint (label "ep") or per-return-code (label "rc"). + once sync.Once + knownLogs monitoring.Gauge // origin => value (always 1.0) + maxMergeDelay monitoring.Gauge // origin => value + expMergeDelay monitoring.Gauge // origin => value + lastSCTTimestamp monitoring.Gauge // origin => value + reqsCounter monitoring.Counter // origin, ep => value + rspsCounter monitoring.Counter // origin, ep, rc => value + rspLatency monitoring.Histogram // origin, ep, rc => value +) + +// setupMetrics initializes all the exported metrics. +func setupMetrics(mf monitoring.MetricFactory) { + knownLogs = mf.NewGauge("known_logs", "Set to 1 for known logs", "logid") + maxMergeDelay = mf.NewGauge("max_merge_delay", "Maximum Merge Delay in seconds", "logid") + expMergeDelay = mf.NewGauge("expected_merge_delay", "Expected Merge Delay in seconds", "logid") + lastSCTTimestamp = mf.NewGauge("last_sct_timestamp", "Time of last SCT in ms since epoch", "logid") + reqsCounter = mf.NewCounter("http_reqs", "Number of requests", "logid", "ep") + rspsCounter = mf.NewCounter("http_rsps", "Number of responses", "logid", "ep", "rc") + rspLatency = mf.NewHistogram("http_latency", "Latency of responses in seconds", "logid", "ep", "rc") +} + +// Entrypoints is a list of entrypoint names as exposed in statistics/logging. +var Entrypoints = []EntrypointName{AddChainName, AddPreChainName} + +// PathHandlers maps from a path to the relevant AppHandler instance. +type PathHandlers map[string]AppHandler + +// AppHandler holds a logInfo and a handler function that uses it, and is +// an implementation of the http.Handler interface. +type AppHandler struct { + Info *logInfo + Handler func(context.Context, *logInfo, http.ResponseWriter, *http.Request) (int, error) + Name EntrypointName + Method string // http.MethodGet or http.MethodPost +} + +// ServeHTTP for an AppHandler invokes the underlying handler function but +// does additional common error and stats processing. +func (a AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var statusCode int + label0 := a.Info.LogOrigin + label1 := string(a.Name) + reqsCounter.Inc(label0, label1) + startTime := a.Info.TimeSource.Now() + logCtx := a.Info.RequestLog.Start(r.Context()) + a.Info.RequestLog.LogOrigin(logCtx, a.Info.LogOrigin) + defer func() { + latency := a.Info.TimeSource.Now().Sub(startTime).Seconds() + rspLatency.Observe(latency, label0, label1, strconv.Itoa(statusCode)) + }() + klog.V(2).Infof("%s: request %v %q => %s", a.Info.LogOrigin, r.Method, r.URL, a.Name) + if r.Method != a.Method { + klog.Warningf("%s: %s wrong HTTP method: %v", a.Info.LogOrigin, a.Name, r.Method) + a.Info.SendHTTPError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed: %s", r.Method)) + a.Info.RequestLog.Status(logCtx, http.StatusMethodNotAllowed) + return + } + + // For GET requests all params come as form encoded so we might as well parse them now. + // POSTs will decode the raw request body as JSON later. + if r.Method == http.MethodGet { + if err := r.ParseForm(); err != nil { + a.Info.SendHTTPError(w, http.StatusBadRequest, fmt.Errorf("failed to parse form data: %s", err)) + a.Info.RequestLog.Status(logCtx, http.StatusBadRequest) + return + } + } + + // Many/most of the handlers forward the request on to the Log RPC server; impose a deadline + // on this onward request. + ctx, cancel := context.WithDeadline(logCtx, getRPCDeadlineTime(a.Info)) + defer cancel() + + var err error + statusCode, err = a.Handler(ctx, a.Info, w, r) + a.Info.RequestLog.Status(ctx, statusCode) + klog.V(2).Infof("%s: %s <= st=%d", a.Info.LogOrigin, a.Name, statusCode) + rspsCounter.Inc(label0, label1, strconv.Itoa(statusCode)) + if err != nil { + klog.Warningf("%s: %s handler error: %v", a.Info.LogOrigin, a.Name, err) + a.Info.SendHTTPError(w, statusCode, err) + return + } + + // Additional check, for consistency the handler must return an error for non-200 st + if statusCode != http.StatusOK { + klog.Warningf("%s: %s handler non 200 without error: %d %v", a.Info.LogOrigin, a.Name, statusCode, err) + a.Info.SendHTTPError(w, http.StatusInternalServerError, fmt.Errorf("http handler misbehaved, st: %d", statusCode)) + return + } +} + +// CertValidationOpts contains various parameters for certificate chain validation +type CertValidationOpts struct { + // trustedRoots is a pool of certificates that defines the roots the CT log will accept + trustedRoots *x509util.PEMCertPool + // currentTime is the time used for checking a certificate's validity period + // against. If it's zero then time.Now() is used. Only for testing. + currentTime time.Time + // rejectExpired indicates that expired certificates will be rejected. + rejectExpired bool + // rejectUnexpired indicates that certificates that are currently valid or not yet valid will be rejected. + rejectUnexpired bool + // notAfterStart is the earliest notAfter date which will be accepted. + // nil means no lower bound on the accepted range. + notAfterStart *time.Time + // notAfterLimit defines the cut off point of notAfter dates - only notAfter + // dates strictly *before* notAfterLimit will be accepted. + // nil means no upper bound on the accepted range. + notAfterLimit *time.Time + // acceptOnlyCA will reject any certificate without the CA bit set. + acceptOnlyCA bool + // extKeyUsages contains the list of EKUs to use during chain verification + extKeyUsages []x509.ExtKeyUsage + // rejectExtIds contains a list of X.509 extension IDs to reject during chain verification. + rejectExtIds []asn1.ObjectIdentifier +} + +// NewCertValidationOpts builds validation options based on parameters. +func NewCertValidationOpts(trustedRoots *x509util.PEMCertPool, currentTime time.Time, rejectExpired bool, rejectUnexpired bool, notAfterStart *time.Time, notAfterLimit *time.Time, acceptOnlyCA bool, extKeyUsages []x509.ExtKeyUsage) CertValidationOpts { + var vOpts CertValidationOpts + vOpts.trustedRoots = trustedRoots + vOpts.currentTime = currentTime + vOpts.rejectExpired = rejectExpired + vOpts.rejectUnexpired = rejectUnexpired + vOpts.notAfterStart = notAfterStart + vOpts.notAfterLimit = notAfterLimit + vOpts.acceptOnlyCA = acceptOnlyCA + vOpts.extKeyUsages = extKeyUsages + return vOpts +} + +// logInfo holds information for a specific log instance. +type logInfo struct { + // LogOrigin identifies the log, as per https://c2sp.org/static-ct-api + LogOrigin string + // TimeSource is a util.TimeSource that can be injected for testing + TimeSource util.TimeSource + // RequestLog is a logger for various request / processing / response debug + // information. + RequestLog RequestLog + + // Instance-wide options + instanceOpts InstanceOptions + // validationOpts contains the certificate chain validation parameters + validationOpts CertValidationOpts + // storage stores log data + storage Storage + // signer signs objects (e.g. STHs, SCTs) for regular logs + signer crypto.Signer +} + +// newLogInfo creates a new instance of logInfo. +func newLogInfo( + instanceOpts InstanceOptions, + validationOpts CertValidationOpts, + signer crypto.Signer, + timeSource util.TimeSource, +) *logInfo { + vCfg := instanceOpts.Validated + cfg := vCfg.Config + + li := &logInfo{ + LogOrigin: cfg.Origin, + storage: instanceOpts.Storage, + signer: signer, + TimeSource: timeSource, + instanceOpts: instanceOpts, + validationOpts: validationOpts, + RequestLog: instanceOpts.RequestLog, + } + + once.Do(func() { setupMetrics(instanceOpts.MetricFactory) }) + label := cfg.Origin + knownLogs.Set(1.0, cfg.Origin) + + maxMergeDelay.Set(float64(cfg.MaxMergeDelaySec), label) + expMergeDelay.Set(float64(cfg.ExpectedMergeDelaySec), label) + + return li +} + +// Handlers returns a map from URL paths (with the given prefix) and AppHandler instances +// to handle those entrypoints. +func (li *logInfo) Handlers(prefix string) PathHandlers { + if !strings.HasPrefix(prefix, "/") { + prefix = "/" + prefix + } + prefix = strings.TrimRight(prefix, "/") + + // Bind the logInfo instance to give an AppHandler instance for each endpoint. + ph := PathHandlers{ + prefix + ct.AddChainPath: AppHandler{Info: li, Handler: addChain, Name: AddChainName, Method: http.MethodPost}, + prefix + ct.AddPreChainPath: AppHandler{Info: li, Handler: addPreChain, Name: AddPreChainName, Method: http.MethodPost}, + } + + return ph +} + +// SendHTTPError generates a custom error page to give more information on why something didn't work +func (li *logInfo) SendHTTPError(w http.ResponseWriter, statusCode int, err error) { + errorBody := http.StatusText(statusCode) + if !li.instanceOpts.MaskInternalErrors || statusCode != http.StatusInternalServerError { + errorBody += fmt.Sprintf("\n%v", err) + } + http.Error(w, errorBody, statusCode) +} + +// ParseBodyAsJSONChain tries to extract cert-chain out of request. +func ParseBodyAsJSONChain(r *http.Request) (ct.AddChainRequest, error) { + body, err := io.ReadAll(r.Body) + if err != nil { + klog.V(1).Infof("Failed to read request body: %v", err) + return ct.AddChainRequest{}, err + } + + var req ct.AddChainRequest + if err := json.Unmarshal(body, &req); err != nil { + klog.V(1).Infof("Failed to parse request body: %v", err) + return ct.AddChainRequest{}, err + } + + // The cert chain is not allowed to be empty. We'll defer other validation for later + if len(req.Chain) == 0 { + klog.V(1).Infof("Request chain is empty: %q", body) + return ct.AddChainRequest{}, errors.New("cert chain was empty") + } + + return req, nil +} + +// addChainInternal is called by add-chain and add-pre-chain as the logic involved in +// processing these requests is almost identical +func addChainInternal(ctx context.Context, li *logInfo, w http.ResponseWriter, r *http.Request, isPrecert bool) (int, error) { + var method EntrypointName + + // Check the contents of the request and convert to slice of certificates. + addChainReq, err := ParseBodyAsJSONChain(r) + if err != nil { + return http.StatusBadRequest, fmt.Errorf("%s: failed to parse add-chain body: %s", li.LogOrigin, err) + } + // Log the DERs now because they might not parse as valid X.509. + for _, der := range addChainReq.Chain { + li.RequestLog.AddDERToChain(ctx, der) + } + chain, err := verifyAddChain(li, addChainReq, isPrecert) + if err != nil { + return http.StatusBadRequest, fmt.Errorf("failed to verify add-chain contents: %s", err) + } + for _, cert := range chain { + li.RequestLog.AddCertToChain(ctx, cert) + } + // Get the current time in the form used throughout RFC6962, namely milliseconds since Unix + // epoch, and use this throughout. + timeMillis := uint64(li.TimeSource.Now().UnixNano() / millisPerNano) + + entry, err := entryFromChain(chain, isPrecert, timeMillis) + if err != nil { + return http.StatusBadRequest, fmt.Errorf("failed to build MerkleTreeLeaf: %s", err) + } + + klog.V(2).Infof("%s: %s => storage.Add", li.LogOrigin, method) + idx, err := li.storage.Add(ctx, entry) + if err != nil { + return http.StatusInternalServerError, fmt.Errorf("couldn't store the leaf") + } + + // Always use the returned leaf as the basis for an SCT. + var loggedLeaf ct.MerkleTreeLeaf + leafValue := entry.MerkleTreeLeaf(idx) + if rest, err := tls.Unmarshal(leafValue, &loggedLeaf); err != nil { + return http.StatusInternalServerError, fmt.Errorf("failed to reconstruct MerkleTreeLeaf: %s", err) + } else if len(rest) > 0 { + return http.StatusInternalServerError, fmt.Errorf("extra data (%d bytes) on reconstructing MerkleTreeLeaf", len(rest)) + } + + // As the Log server has definitely got the Merkle tree leaf, we can + // generate an SCT and respond with it. + // TODO(phboneff): this should work, but double check + sct, err := buildV1SCT(li.signer, &loggedLeaf) + if err != nil { + return http.StatusInternalServerError, fmt.Errorf("failed to generate SCT: %s", err) + } + sctBytes, err := tls.Marshal(*sct) + if err != nil { + return http.StatusInternalServerError, fmt.Errorf("failed to marshall SCT: %s", err) + } + // We could possibly fail to issue the SCT after this but it's v. unlikely. + li.RequestLog.IssueSCT(ctx, sctBytes) + err = marshalAndWriteAddChainResponse(sct, li.signer, w) + if err != nil { + // reason is logged and http status is already set + return http.StatusInternalServerError, fmt.Errorf("failed to write response: %s", err) + } + klog.V(3).Infof("%s: %s <= SCT", li.LogOrigin, method) + if sct.Timestamp == timeMillis { + lastSCTTimestamp.Set(float64(sct.Timestamp), li.LogOrigin) + } + + return http.StatusOK, nil +} + +func addChain(ctx context.Context, li *logInfo, w http.ResponseWriter, r *http.Request) (int, error) { + return addChainInternal(ctx, li, w, r, false) +} + +func addPreChain(ctx context.Context, li *logInfo, w http.ResponseWriter, r *http.Request) (int, error) { + return addChainInternal(ctx, li, w, r, true) +} + +// getRPCDeadlineTime calculates the future time an RPC should expire based on our config +func getRPCDeadlineTime(li *logInfo) time.Time { + return li.TimeSource.Now().Add(li.instanceOpts.Deadline) +} + +// verifyAddChain is used by add-chain and add-pre-chain. It does the checks that the supplied +// cert is of the correct type and chains to a trusted root. +func verifyAddChain(li *logInfo, req ct.AddChainRequest, expectingPrecert bool) ([]*x509.Certificate, error) { + // We already checked that the chain is not empty so can move on to verification + validPath, err := validateChain(req.Chain, li.validationOpts) + if err != nil { + // We rejected it because the cert failed checks or we could not find a path to a root etc. + // Lots of possible causes for errors + return nil, fmt.Errorf("chain failed to verify: %s", err) + } + + isPrecert, err := isPrecertificate(validPath[0]) + if err != nil { + return nil, fmt.Errorf("precert test failed: %s", err) + } + + // The type of the leaf must match the one the handler expects + if isPrecert != expectingPrecert { + if expectingPrecert { + klog.Warningf("%s: Cert (or precert with invalid CT ext) submitted as precert chain: %q", li.LogOrigin, req.Chain) + } else { + klog.Warningf("%s: Precert (or cert with invalid CT ext) submitted as cert chain: %q", li.LogOrigin, req.Chain) + } + return nil, fmt.Errorf("cert / precert mismatch: %T", expectingPrecert) + } + + return validPath, nil +} + +// marshalAndWriteAddChainResponse is used by add-chain and add-pre-chain to create and write +// the JSON response to the client +func marshalAndWriteAddChainResponse(sct *ct.SignedCertificateTimestamp, signer crypto.Signer, w http.ResponseWriter) error { + logID, err := GetCTLogID(signer.Public()) + if err != nil { + return fmt.Errorf("failed to marshal logID: %s", err) + } + sig, err := tls.Marshal(sct.Signature) + if err != nil { + return fmt.Errorf("failed to marshal signature: %s", err) + } + + rsp := ct.AddChainResponse{ + SCTVersion: sct.SCTVersion, + Timestamp: sct.Timestamp, + ID: logID[:], + Extensions: base64.StdEncoding.EncodeToString(sct.Extensions), + Signature: sig, + } + + w.Header().Set(contentTypeHeader, contentTypeJSON) + jsonData, err := json.Marshal(&rsp) + if err != nil { + return fmt.Errorf("failed to marshal add-chain: %s", err) + } + + _, err = w.Write(jsonData) + if err != nil { + return fmt.Errorf("failed to write add-chain resp: %s", err) + } + + return nil +} + +// entryFromChain generates an Entry from a chain and timestamp. +// copied from certificate-transparency-go/serialization.go +// TODO(phboneff): move in a different file maybe? +func entryFromChain(chain []*x509.Certificate, isPrecert bool, timestamp uint64) (*ctonly.Entry, error) { + leaf := ctonly.Entry{ + IsPrecert: isPrecert, + Timestamp: timestamp, + } + if !isPrecert { + leaf.Certificate = chain[0].Raw + return &leaf, nil + } + + // Pre-certs are more complicated. First, parse the leaf pre-cert and its + // putative issuer. + if len(chain) < 2 { + return nil, fmt.Errorf("no issuer cert available for precert leaf building") + } + issuer := chain[1] + cert := chain[0] + + var preIssuer *x509.Certificate + if IsPreIssuer(issuer) { + // Replace the cert's issuance information with details from the pre-issuer. + preIssuer = issuer + + // The issuer of the pre-cert is not going to be the issuer of the final + // cert. Change to use the final issuer's key hash. + if len(chain) < 3 { + return nil, fmt.Errorf("no issuer cert available for pre-issuer") + } + issuer = chain[2] + } + + // Next, post-process the DER-encoded TBSCertificate, to remove the CT poison + // extension and possibly update the issuer field. + defangedTBS, err := x509.BuildPrecertTBS(cert.RawTBSCertificate, preIssuer) + if err != nil { + return nil, fmt.Errorf("failed to remove poison extension: %v", err) + } + + leaf.Precertificate = cert.Raw + leaf.PrecertSigningCert = issuer.Raw + leaf.Certificate = defangedTBS + + issuerKeyHash := sha256.Sum256(issuer.RawSubjectPublicKeyInfo) + leaf.IssuerKeyHash = issuerKeyHash[:] + return &leaf, nil +} + +// IsPreIssuer indicates whether a certificate is a pre-cert issuer with the specific +// certificate transparency extended key usage. +// copied form certificate-transparency-go/serialization.go +func IsPreIssuer(issuer *x509.Certificate) bool { + for _, eku := range issuer.ExtKeyUsage { + if eku == x509.ExtKeyUsageCertificateTransparency { + return true + } + } + return false +} diff --git a/personalities/sctfe/instance.go b/personalities/sctfe/instance.go new file mode 100644 index 00000000..a164b92c --- /dev/null +++ b/personalities/sctfe/instance.go @@ -0,0 +1,169 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "context" + "crypto" + "crypto/ecdsa" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/google/certificate-transparency-go/asn1" + "github.com/google/certificate-transparency-go/trillian/ctfe/cache" + "github.com/google/certificate-transparency-go/trillian/util" + "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" + "github.com/google/trillian/crypto/keys" + "github.com/google/trillian/monitoring" +) + +// InstanceOptions describes the options for a log instance. +type InstanceOptions struct { + // Validated holds the original configuration options for the log, and some + // of its fields parsed as a result of validating it. + Validated *ValidatedLogConfig + // Storage is a corresponding Tessera storage implementation. + Storage Storage + // Deadline is a timeout for Tessera requests. + Deadline time.Duration + // MetricFactory allows creating metrics. + MetricFactory monitoring.MetricFactory + // ErrorMapper converts an error from an RPC request to an HTTP status, plus + // a boolean to indicate whether the conversion succeeded. + ErrorMapper func(error) (int, bool) + // RequestLog provides structured logging of CTFE requests. + RequestLog RequestLog + // RemoteUser returns a string representing the originating host for the + // given request. This string will be used as a User quota key. + // If unset, no quota will be requested for remote users. + RemoteQuotaUser func(*http.Request) string + // CertificateQuotaUser returns a string representing the passed in + // intermediate certificate. This string will be user as a User quota key for + // the cert. Quota will be requested for each intermediate in an + // add-[pre]-chain request so as to allow individual issuers to be rate + // limited. If unset, no quota will be requested for intermediate + // certificates. + CertificateQuotaUser func(*x509.Certificate) string + // MaskInternalErrors indicates if internal server errors should be masked + // or returned to the user containing the full error message. + MaskInternalErrors bool + // CacheType is the CTFE cache type. + CacheType cache.Type + // CacheOption includes the cache size and time-to-live (TTL). + CacheOption cache.Option +} + +// Instance is a set up log/mirror instance. It must be created with the +// SetUpInstance call. +type Instance struct { + Handlers PathHandlers + li *logInfo +} + +// GetPublicKey returns the public key from the instance's signer. +func (i *Instance) GetPublicKey() crypto.PublicKey { + if i.li != nil && i.li.signer != nil { + return i.li.signer.Public() + } + return nil +} + +// SetUpInstance sets up a log (or log mirror) instance using the provided +// configuration, and returns an object containing a set of handlers for this +// log, and an STH getter. +func SetUpInstance(ctx context.Context, opts InstanceOptions) (*Instance, error) { + logInfo, err := setUpLogInfo(ctx, opts) + if err != nil { + return nil, err + } + handlers := logInfo.Handlers(opts.Validated.Config.Origin) + return &Instance{Handlers: handlers, li: logInfo}, nil +} + +func setUpLogInfo(ctx context.Context, opts InstanceOptions) (*logInfo, error) { + vCfg := opts.Validated + cfg := vCfg.Config + + // Check config validity. + if len(cfg.RootsPemFile) == 0 { + return nil, errors.New("need to specify RootsPemFile") + } + + // Load the trusted roots. + roots := x509util.NewPEMCertPool() + for _, pemFile := range cfg.RootsPemFile { + if err := roots.AppendCertsFromPEMFile(pemFile); err != nil { + return nil, fmt.Errorf("failed to read trusted roots: %v", err) + } + } + + var signer crypto.Signer + var err error + if signer, err = keys.NewSigner(ctx, vCfg.PrivKey); err != nil { + return nil, fmt.Errorf("failed to load private key: %v", err) + } + + // TODO(phboneff): are pub keys actually used? If not, remove + // If a public key has been configured for a log, check that it is consistent with the private key. + if vCfg.PubKey != nil { + switch pub := vCfg.PubKey.(type) { + case *ecdsa.PublicKey: + if !pub.Equal(signer.Public()) { + return nil, errors.New("public key is not consistent with private key") + } + default: + return nil, errors.New("failed to verify consistency of public key with private key") + } + } + + validationOpts := CertValidationOpts{ + trustedRoots: roots, + rejectExpired: cfg.RejectExpired, + rejectUnexpired: cfg.RejectUnexpired, + notAfterStart: vCfg.NotAfterStart, + notAfterLimit: vCfg.NotAfterLimit, + acceptOnlyCA: cfg.AcceptOnlyCa, + extKeyUsages: vCfg.KeyUsages, + } + validationOpts.rejectExtIds, err = parseOIDs(cfg.RejectExtensions) + if err != nil { + return nil, fmt.Errorf("failed to parse RejectExtensions: %v", err) + } + + logInfo := newLogInfo(opts, validationOpts, signer, new(util.SystemTimeSource)) + return logInfo, nil +} + +func parseOIDs(oids []string) ([]asn1.ObjectIdentifier, error) { + ret := make([]asn1.ObjectIdentifier, 0, len(oids)) + for _, s := range oids { + bits := strings.Split(s, ".") + var oid asn1.ObjectIdentifier + for _, n := range bits { + p, err := strconv.Atoi(n) + if err != nil { + return nil, err + } + oid = append(oid, p) + } + ret = append(ret, oid) + } + return ret, nil +} diff --git a/personalities/sctfe/instance_test.go b/personalities/sctfe/instance_test.go new file mode 100644 index 00000000..09b32b03 --- /dev/null +++ b/personalities/sctfe/instance_test.go @@ -0,0 +1,281 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "context" + "errors" + "fmt" + "net/http/httptest" + "strings" + "testing" + "time" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/trillian/ctfe/cache" + "github.com/google/trillian/crypto/keys" + "github.com/google/trillian/crypto/keys/pem" + "github.com/google/trillian/crypto/keyspb" + "github.com/google/trillian/monitoring" + "github.com/transparency-dev/trillian-tessera/personalities/sctfe/configpb" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func init() { + keys.RegisterHandler(&keyspb.PEMKeyFile{}, pem.FromProto) +} + +func TestSetUpInstance(t *testing.T) { + ctx := context.Background() + + privKey := mustMarshalAny(&keyspb.PEMKeyFile{Path: "./testdata/ct-http-server.privkey.pem", Password: "dirk"}) + missingPrivKey := mustMarshalAny(&keyspb.PEMKeyFile{Path: "./testdata/bogus.privkey.pem", Password: "dirk"}) + wrongPassPrivKey := mustMarshalAny(&keyspb.PEMKeyFile{Path: "./testdata/ct-http-server.privkey.pem", Password: "dirkly"}) + + var tests = []struct { + desc string + cfg *configpb.LogConfig + wantErr string + }{ + { + desc: "valid", + cfg: &configpb.LogConfig{ + Origin: "log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "no-roots", + cfg: &configpb.LogConfig{ + Origin: "log", + PrivateKey: privKey, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + wantErr: "specify RootsPemFile", + }, + { + desc: "missing-root-cert", + cfg: &configpb.LogConfig{ + Origin: "log", + RootsPemFile: []string{"../testdata/bogus.cert"}, + PrivateKey: privKey, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + wantErr: "failed to read trusted roots", + }, + { + desc: "missing-privkey", + cfg: &configpb.LogConfig{ + Origin: "log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: missingPrivKey, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + wantErr: "failed to load private key", + }, + { + desc: "privkey-wrong-password", + cfg: &configpb.LogConfig{ + Origin: "log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: wrongPassPrivKey, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + wantErr: "failed to load private key", + }, + { + desc: "valid-ekus-1", + cfg: &configpb.LogConfig{ + Origin: "log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + ExtKeyUsages: []string{"Any"}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "valid-ekus-2", + cfg: &configpb.LogConfig{ + Origin: "log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + ExtKeyUsages: []string{"Any", "ServerAuth", "TimeStamping"}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "valid-reject-ext", + cfg: &configpb.LogConfig{ + Origin: "log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + RejectExtensions: []string{"1.2.3.4", "5.6.7.8"}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "invalid-reject-ext", + cfg: &configpb.LogConfig{ + Origin: "log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + RejectExtensions: []string{"1.2.3.4", "one.banana.two.bananas"}, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + wantErr: "one", + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + vCfg, err := validateLogConfig(test.cfg) + if err != nil { + t.Fatalf("ValidateLogConfig(): %v", err) + } + opts := InstanceOptions{Validated: vCfg, Deadline: time.Second, MetricFactory: monitoring.InertMetricFactory{}} + + if _, err := SetUpInstance(ctx, opts); err != nil { + if test.wantErr == "" { + t.Errorf("SetUpInstance()=_,%v; want _,nil", err) + } else if !strings.Contains(err.Error(), test.wantErr) { + t.Errorf("SetUpInstance()=_,%v; want err containing %q", err, test.wantErr) + } + return + } + if test.wantErr != "" { + t.Errorf("SetUpInstance()=_,nil; want err containing %q", test.wantErr) + } + }) + } +} + +func equivalentTimes(a *time.Time, b *timestamppb.Timestamp) bool { + if a == nil && b == nil { + return true + } + if a == nil { + // b can't be nil as it would have returned above. + return false + } + tsA := timestamppb.New(*a) + return tsA.AsTime().Format(time.RFC3339Nano) == b.AsTime().Format(time.RFC3339Nano) +} + +func TestSetUpInstanceSetsValidationOpts(t *testing.T) { + ctx := context.Background() + + start := timestamppb.New(time.Unix(10000, 0)) + limit := timestamppb.New(time.Unix(12000, 0)) + + privKey, err := anypb.New(&keyspb.PEMKeyFile{Path: "./testdata/ct-http-server.privkey.pem", Password: "dirk"}) + if err != nil { + t.Fatalf("Could not marshal private key proto: %v", err) + } + var tests = []struct { + desc string + cfg *configpb.LogConfig + }{ + { + desc: "no validation opts", + cfg: &configpb.LogConfig{ + Origin: "/log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "notAfterStart only", + cfg: &configpb.LogConfig{ + Origin: "/log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + NotAfterStart: start, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "notAfter range", + cfg: &configpb.LogConfig{ + Origin: "/log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + NotAfterStart: start, + NotAfterLimit: limit, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + { + desc: "caOnly", + cfg: &configpb.LogConfig{ + Origin: "/log", + RootsPemFile: []string{"./testdata/fake-ca.cert"}, + PrivateKey: privKey, + AcceptOnlyCa: true, + StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}}, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + vCfg, err := validateLogConfig(test.cfg) + if err != nil { + t.Fatalf("ValidateLogConfig(): %v", err) + } + opts := InstanceOptions{Validated: vCfg, Deadline: time.Second, MetricFactory: monitoring.InertMetricFactory{}, CacheType: cache.NOOP, CacheOption: cache.Option{}} + + inst, err := SetUpInstance(ctx, opts) + if err != nil { + t.Fatalf("%v: SetUpInstance() = %v, want no error", test.desc, err) + } + addChainHandler, ok := inst.Handlers[test.cfg.Origin+ct.AddChainPath] + if !ok { + t.Fatal("Couldn't find AddChain handler") + } + gotOpts := addChainHandler.Info.validationOpts + if got, want := gotOpts.notAfterStart, test.cfg.NotAfterStart; want != nil && !equivalentTimes(got, want) { + t.Errorf("%v: handler notAfterStart %v, want %v", test.desc, got, want) + } + if got, want := gotOpts.notAfterLimit, test.cfg.NotAfterLimit; want != nil && !equivalentTimes(got, want) { + t.Errorf("%v: handler notAfterLimit %v, want %v", test.desc, got, want) + } + if got, want := gotOpts.acceptOnlyCA, test.cfg.AcceptOnlyCa; got != want { + t.Errorf("%v: handler acceptOnlyCA %v, want %v", test.desc, got, want) + } + }) + } +} + +func TestErrorMasking(t *testing.T) { + info := logInfo{} + w := httptest.NewRecorder() + prefix := "Internal Server Error" + err := errors.New("well that's bad") + info.SendHTTPError(w, 500, err) + if got, want := w.Body.String(), fmt.Sprintf("%s\n%v\n", prefix, err); got != want { + t.Errorf("SendHTTPError: got %s, want %s", got, want) + } + info.instanceOpts.MaskInternalErrors = true + w = httptest.NewRecorder() + info.SendHTTPError(w, 500, err) + if got, want := w.Body.String(), prefix+"\n"; got != want { + t.Errorf("SendHTTPError: got %s, want %s", got, want) + } + +} diff --git a/personalities/sctfe/proto_gen.go b/personalities/sctfe/proto_gen.go new file mode 100644 index 00000000..53e5231e --- /dev/null +++ b/personalities/sctfe/proto_gen.go @@ -0,0 +1,6 @@ +// See the License for the specific language governing permissions and +// limitations under the License. + +package sctfe + +//go:generate sh -c "protoc -I=. -I$(go list -f '{{ .Dir }}' github.com/google/trillian) -I$(go list -f '{{ .Dir }}' github.com/transparency-dev/trillian-tessera/personalities/sctfe) --go_out=paths=source_relative:. configpb/config.proto" diff --git a/personalities/sctfe/requestlog.go b/personalities/sctfe/requestlog.go new file mode 100644 index 00000000..4ef41d02 --- /dev/null +++ b/personalities/sctfe/requestlog.go @@ -0,0 +1,103 @@ +// Copyright 2017 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "context" + "encoding/hex" + "time" + + "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" + "k8s.io/klog/v2" +) + +const vLevel = 9 + +// RequestLog allows implementations to do structured logging of CTFE +// request parameters, submitted chains and other internal details that +// are useful for log operators when debugging issues. CTFE handlers will +// call the appropriate methods during request processing. The implementation +// is responsible for collating and storing the resulting logging information. +type RequestLog interface { + // Start will be called once at the beginning of handling each request. + // The supplied context will be the one used for request processing and + // can be used by the logger to set values on the returned context. + // The returned context should be used in all the following calls to + // this API. This is normally arranged by the request handler code. + Start(context.Context) context.Context + // LogOrigin will be called once per request to set the log prefix. + LogOrigin(context.Context, string) + // AddDERToChain will be called once for each certificate in a submitted + // chain. It's called early in request processing so the supplied bytes + // have not been checked for validity. Calls will be in order of the + // certificates as presented in the request with the root last. + AddDERToChain(context.Context, []byte) + // AddCertToChain will be called once for each certificate in the chain + // after it has been parsed and verified. Calls will be in order of the + // certificates as presented in the request with the root last. + AddCertToChain(context.Context, *x509.Certificate) + // IssueSCT will be called once when the server is about to issue an SCT to a + // client. This should not be called if the submission process fails before an + // SCT could be presented to a client, even if this is unrelated to + // the validity of the submitted chain. The SCT bytes will be in TLS + // serialized format. + IssueSCT(context.Context, []byte) + // Status will be called once to set the HTTP status code that was the + // the result after the request has been handled. + Status(context.Context, int) +} + +// DefaultRequestLog is an implementation of RequestLog that does nothing +// except log the calls at a high level of verbosity. +type DefaultRequestLog struct { +} + +// Start logs the start of request processing. +func (dlr *DefaultRequestLog) Start(ctx context.Context) context.Context { + klog.V(vLevel).Info("RL: Start") + return ctx +} + +// LogOrigin logs the origin of the CT log that this request is for. +func (dlr *DefaultRequestLog) LogOrigin(_ context.Context, p string) { + klog.V(vLevel).Infof("RL: LogOrigin: %s", p) +} + +// AddDERToChain logs the raw bytes of a submitted certificate. +func (dlr *DefaultRequestLog) AddDERToChain(_ context.Context, d []byte) { + // Explicit hex encoding below to satisfy CodeQL: + klog.V(vLevel).Infof("RL: Cert DER: %s", hex.EncodeToString(d)) +} + +// AddCertToChain logs some issuer / subject / timing fields from a +// certificate that is part of a submitted chain. +func (dlr *DefaultRequestLog) AddCertToChain(_ context.Context, cert *x509.Certificate) { + klog.V(vLevel).Infof("RL: Cert: Sub: %s Iss: %s notBef: %s notAft: %s", + x509util.NameToString(cert.Subject), + x509util.NameToString(cert.Issuer), + cert.NotBefore.Format(time.RFC1123Z), + cert.NotAfter.Format(time.RFC1123Z)) +} + +// IssueSCT logs an SCT that will be issued to a client. +func (dlr *DefaultRequestLog) IssueSCT(_ context.Context, sct []byte) { + klog.V(vLevel).Infof("RL: Issuing SCT: %x", sct) +} + +// Status logs the response HTTP status code after processing completes. +func (dlr *DefaultRequestLog) Status(_ context.Context, s int) { + klog.V(vLevel).Infof("RL: Status: %d", s) +} diff --git a/personalities/sctfe/serialize.go b/personalities/sctfe/serialize.go new file mode 100644 index 00000000..28b9526a --- /dev/null +++ b/personalities/sctfe/serialize.go @@ -0,0 +1,66 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "crypto" + "crypto/rand" + "crypto/sha256" + "fmt" + + "github.com/google/certificate-transparency-go/tls" + + ct "github.com/google/certificate-transparency-go" +) + +func buildV1SCT(signer crypto.Signer, leaf *ct.MerkleTreeLeaf) (*ct.SignedCertificateTimestamp, error) { + // Serialize SCT signature input to get the bytes that need to be signed + sctInput := ct.SignedCertificateTimestamp{ + SCTVersion: ct.V1, + Timestamp: leaf.TimestampedEntry.Timestamp, + Extensions: leaf.TimestampedEntry.Extensions, + } + data, err := ct.SerializeSCTSignatureInput(sctInput, ct.LogEntry{Leaf: *leaf}) + if err != nil { + return nil, fmt.Errorf("failed to serialize SCT data: %v", err) + } + + h := sha256.Sum256(data) + signature, err := signer.Sign(rand.Reader, h[:], crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("failed to sign SCT data: %v", err) + } + + digitallySigned := ct.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.SignatureAlgorithmFromPubKey(signer.Public()), + }, + Signature: signature, + } + + logID, err := GetCTLogID(signer.Public()) + if err != nil { + return nil, fmt.Errorf("failed to get logID for signing: %v", err) + } + + return &ct.SignedCertificateTimestamp{ + SCTVersion: ct.V1, + LogID: ct.LogID{KeyID: logID}, + Timestamp: sctInput.Timestamp, + Extensions: sctInput.Extensions, + Signature: digitallySigned, + }, nil +} diff --git a/personalities/sctfe/serialize_test.go b/personalities/sctfe/serialize_test.go new file mode 100644 index 00000000..18e30366 --- /dev/null +++ b/personalities/sctfe/serialize_test.go @@ -0,0 +1,143 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "bytes" + "crypto/sha256" + "testing" + + "github.com/google/certificate-transparency-go/tls" + "github.com/google/certificate-transparency-go/trillian/ctfe/testonly" + "github.com/google/certificate-transparency-go/x509" + "github.com/google/certificate-transparency-go/x509util" + "github.com/kylelemons/godebug/pretty" + + ct "github.com/google/certificate-transparency-go" +) + +func TestBuildV1MerkleTreeLeafForCert(t *testing.T) { + cert, err := x509util.CertificateFromPEM([]byte(testonly.LeafSignedByFakeIntermediateCertPEM)) + if x509.IsFatal(err) { + t.Fatalf("failed to set up test cert: %v", err) + } + + signer, err := setupSigner(fakeSignature) + if err != nil { + t.Fatalf("could not create signer: %v", err) + } + + leaf, err := ct.MerkleTreeLeafFromChain([]*x509.Certificate{cert}, ct.X509LogEntryType, fixedTimeMillis) + if err != nil { + t.Fatalf("buildV1MerkleTreeLeafForCert()=nil,%v; want _,nil", err) + } + got, err := buildV1SCT(signer, leaf) + if err != nil { + t.Fatalf("buildV1SCT()=nil,%v; want _,nil", err) + } + + expected := ct.SignedCertificateTimestamp{ + SCTVersion: 0, + LogID: ct.LogID{KeyID: demoLogID}, + Timestamp: fixedTimeMillis, + Extensions: ct.CTExtensions{}, + Signature: ct.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.ECDSA}, + Signature: fakeSignature, + }, + } + + if diff := pretty.Compare(*got, expected); diff != "" { + t.Fatalf("Mismatched SCT (cert), diff:\n%v", diff) + } + + // Additional checks that the MerkleTreeLeaf we built is correct + if got, want := leaf.Version, ct.V1; got != want { + t.Fatalf("Got a %v leaf, expected a %v leaf", got, want) + } + if got, want := leaf.LeafType, ct.TimestampedEntryLeafType; got != want { + t.Fatalf("Got leaf type %v, expected %v", got, want) + } + if got, want := leaf.TimestampedEntry.EntryType, ct.X509LogEntryType; got != want { + t.Fatalf("Got entry type %v, expected %v", got, want) + } + if got, want := leaf.TimestampedEntry.Timestamp, got.Timestamp; got != want { + t.Fatalf("Entry / sct timestamp mismatch; got %v, expected %v", got, want) + } + if got, want := leaf.TimestampedEntry.X509Entry.Data, cert.Raw; !bytes.Equal(got, want) { + t.Fatalf("Cert bytes mismatch, got %x, expected %x", got, want) + } +} + +func TestSignV1SCTForPrecertificate(t *testing.T) { + cert, err := x509util.CertificateFromPEM([]byte(testonly.PrecertPEMValid)) + if x509.IsFatal(err) { + t.Fatalf("failed to set up test precert: %v", err) + } + + signer, err := setupSigner(fakeSignature) + if err != nil { + t.Fatalf("could not create signer: %v", err) + } + + // Use the same cert as the issuer for convenience. + leaf, err := ct.MerkleTreeLeafFromChain([]*x509.Certificate{cert, cert}, ct.PrecertLogEntryType, fixedTimeMillis) + if err != nil { + t.Fatalf("buildV1MerkleTreeLeafForCert()=nil,%v; want _,nil", err) + } + got, err := buildV1SCT(signer, leaf) + if err != nil { + t.Fatalf("buildV1SCT()=nil,%v; want _,nil", err) + } + + expected := ct.SignedCertificateTimestamp{ + SCTVersion: 0, + LogID: ct.LogID{KeyID: demoLogID}, + Timestamp: fixedTimeMillis, + Extensions: ct.CTExtensions{}, + Signature: ct.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.ECDSA}, + Signature: fakeSignature}} + + if diff := pretty.Compare(*got, expected); diff != "" { + t.Fatalf("Mismatched SCT (precert), diff:\n%v", diff) + } + + // Additional checks that the MerkleTreeLeaf we built is correct + if got, want := leaf.Version, ct.V1; got != want { + t.Fatalf("Got a %v leaf, expected a %v leaf", got, want) + } + if got, want := leaf.LeafType, ct.TimestampedEntryLeafType; got != want { + t.Fatalf("Got leaf type %v, expected %v", got, want) + } + if got, want := leaf.TimestampedEntry.EntryType, ct.PrecertLogEntryType; got != want { + t.Fatalf("Got entry type %v, expected %v", got, want) + } + if got, want := got.Timestamp, leaf.TimestampedEntry.Timestamp; got != want { + t.Fatalf("Entry / sct timestamp mismatch; got %v, expected %v", got, want) + } + keyHash := sha256.Sum256(cert.RawSubjectPublicKeyInfo) + if got, want := keyHash[:], leaf.TimestampedEntry.PrecertEntry.IssuerKeyHash[:]; !bytes.Equal(got, want) { + t.Fatalf("Issuer key hash bytes mismatch, got %v, expected %v", got, want) + } + defangedTBS, _ := x509.RemoveCTPoison(cert.RawTBSCertificate) + if got, want := leaf.TimestampedEntry.PrecertEntry.TBSCertificate, defangedTBS; !bytes.Equal(got, want) { + t.Fatalf("TBS cert mismatch, got %v, expected %v", got, want) + } +} diff --git a/personalities/sctfe/storage.go b/personalities/sctfe/storage.go new file mode 100644 index 00000000..8bf78b5c --- /dev/null +++ b/personalities/sctfe/storage.go @@ -0,0 +1,49 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "context" + + tessera "github.com/transparency-dev/trillian-tessera" + "github.com/transparency-dev/trillian-tessera/ctonly" +) + +// Storage provides all the storage primitives necessary to write to a ct-static-api log. +type Storage interface { + // Add assign an index to the provided Entry, stages the entry for integration, and return it the assigned index. + Add(context.Context, *ctonly.Entry) (uint64, error) +} + +// CTStorage implements Storage. +type CTStorage struct { + storeData func(context.Context, *ctonly.Entry) (uint64, error) + // TODO(phboneff): add storeExtraData + // TODO(phboneff): add dedupe +} + +// NewCTStorage instantiates a CTStorage object. +func NewCTSTorage(logStorage tessera.Storage) (*CTStorage, error) { + ctStorage := &CTStorage{ + storeData: tessera.NewCertificateTransparencySequencedWriter(logStorage), + } + return ctStorage, nil +} + +// Add stores CT entries. +func (cts CTStorage) Add(ctx context.Context, entry *ctonly.Entry) (uint64, error) { + // TODO(phboneff): add deduplication and chain storage + return cts.storeData(ctx, entry) +} diff --git a/personalities/sctfe/structures.go b/personalities/sctfe/structures.go new file mode 100644 index 00000000..87a61d79 --- /dev/null +++ b/personalities/sctfe/structures.go @@ -0,0 +1,37 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +// Code to handle encoding / decoding various data structures used in RFC 6962. Does not +// contain the low level serialization. + +import ( + "crypto" + "crypto/sha256" + + "github.com/google/certificate-transparency-go/x509" +) + +const millisPerNano int64 = 1000 * 1000 + +// GetCTLogID takes the key manager for a log and returns the LogID. (see RFC 6962 S3.2) +// In CT V1 the log id is a hash of the public key. +func GetCTLogID(pk crypto.PublicKey) ([sha256.Size]byte, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(pk) + if err != nil { + return [sha256.Size]byte{}, err + } + return sha256.Sum256(pubBytes), nil +} diff --git a/personalities/sctfe/structures_test.go b/personalities/sctfe/structures_test.go new file mode 100644 index 00000000..f537e091 --- /dev/null +++ b/personalities/sctfe/structures_test.go @@ -0,0 +1,61 @@ +// Copyright 2016 Google LLC. All Rights Reserved. +// +// 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 sctfe + +import ( + "crypto" + "crypto/x509" + "encoding/pem" + "testing" + "time" + + "github.com/google/certificate-transparency-go/trillian/testdata" +) + +var ( + fixedTime = time.Date(2017, 9, 7, 12, 15, 23, 0, time.UTC) + fixedTimeMillis = uint64(fixedTime.UnixNano() / millisPerNano) + demoLogID = [32]byte{19, 56, 222, 93, 229, 36, 102, 128, 227, 214, 3, 121, 93, 175, 126, 236, 97, 217, 34, 32, 40, 233, 98, 27, 46, 179, 164, 251, 84, 10, 60, 57} + fakeSignature = []byte("signed") +) + +func TestGetCTLogID(t *testing.T) { + block, _ := pem.Decode([]byte(testdata.DemoPublicKey)) + pk, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + t.Fatalf("unexpected error loading public key: %v", err) + } + + got, err := GetCTLogID(pk) + if err != nil { + t.Fatalf("error getting logid: %v", err) + } + + if want := demoLogID; got != want { + t.Errorf("logID: \n%v want \n%v", got, want) + } +} + +// Creates a fake signer for use in interaction tests. +// It will always return fakeSig when asked to sign something. +func setupSigner(fakeSig []byte) (crypto.Signer, error) { + block, _ := pem.Decode([]byte(testdata.DemoPublicKey)) + key, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + return testdata.NewSignerWithFixedSig(key, fakeSig), nil +} diff --git a/personalities/sctfe/testdata/ct-http-server.privkey.pem b/personalities/sctfe/testdata/ct-http-server.privkey.pem new file mode 100644 index 00000000..d5d0e372 --- /dev/null +++ b/personalities/sctfe/testdata/ct-http-server.privkey.pem @@ -0,0 +1,12 @@ +# This key is for test purposes only and must not be used for +# production under any circumstances. The key password is +# 'dirk' + +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-CBC,46EB0095C2BEFB56 + +PmIaXRJaKvO09uZuKWqTqotLbH1lWVghuvju4s9BT1AaVmS8BtgPyVTQXwUTXE4Y +bEAk2mWZJySTBUXAUg/NNQjz75TfId96IcFfv/PI0G3CZv4Nvf4IjiVYO3QrCHqr +JZzCxc82/wZmtEntvtof9fXgVoWv1Bk87WE65T5cl0U= +-----END EC PRIVATE KEY----- diff --git a/personalities/sctfe/testdata/fake-ca.cert b/personalities/sctfe/testdata/fake-ca.cert new file mode 100644 index 00000000..5755fe4c --- /dev/null +++ b/personalities/sctfe/testdata/fake-ca.cert @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICHDCCAcGgAwIBAgIEBAbK/jAKBggqhkjOPQQDAjBxMQswCQYDVQQGEwJHQjEP +MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdvb2ds +ZTEMMAoGA1UECxMDRW5nMSEwHwYDVQQDExhGYWtlQ2VydGlmaWNhdGVBdXRob3Jp +dHkwHhcNMTYxMjA3MTUxMzM2WhcNMjYxMjA1MTUxMzM2WjBxMQswCQYDVQQGEwJH +QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdv +b2dsZTEMMAoGA1UECxMDRW5nMSEwHwYDVQQDExhGYWtlQ2VydGlmaWNhdGVBdXRo +b3JpdHkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATy0wfvft/PzvT0Clu8nj/L +HP0MRtyF+8H207K6HVHxmGxIqBVGRWPK39bJrM9gO8dO3bjSFqugCSQdCWYeTeuh +o0cwRTANBgNVHQ4EBgQEAQIDBDAPBgNVHSMECDAGgAQBAgMEMBIGA1UdEwEB/wQI +MAYBAf8CAQowDwYDVR0PAQH/BAUDAwf/gDAKBggqhkjOPQQDAgNJADBGAiEApihJ +OUNvgORDph47qolewiVgKuE5vVVDrk1cqabvrGUCIQDJxQjGWZO0hnCla1QrW/wM +iGuwIwcrxwwn3octloDVVg== +-----END CERTIFICATE----- diff --git a/personalities/sctfe/testdata/subleaf.chain b/personalities/sctfe/testdata/subleaf.chain new file mode 100644 index 00000000..e55831e8 --- /dev/null +++ b/personalities/sctfe/testdata/subleaf.chain @@ -0,0 +1,55 @@ +-----BEGIN CERTIFICATE----- +MIICAjCCAaigAwIBAgIFAN6tvu8wCgYIKoZIzj0EAwIwdTELMAkGA1UEBhMCR0Ix +DzANBgNVBAgTBkxvbmRvbjEPMA0GA1UEBxMGTG9uZG9uMQ8wDQYDVQQKEwZHb29n +bGUxDDAKBgNVBAsTA0VuZzElMCMGA1UEAxMcRmFrZVN1YkludGVybWVkaWF0ZUF1 +dGhvcml0eTAeFw0xODA0MDYxMTAxMDFaFw0yNTA1MTkxMTAxMDFaMFcxCzAJBgNV +BAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAoMBkdvb2dsZTEMMAoGA1UE +CwwDRW5nMRgwFgYDVQQDDA9zdWJsZWFmLmNzci5wZW0wWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAATrN05SRZxG1ai4xe1YuTAppnCKaaAmXJ4vbrhrI2yE4UY6mDaC +RKWKF4tBgjL0LeAIW34HOFL8R1YoJ5vtYIuso0MwQTAdBgNVHQ4EFgQUP7IvQfwR +mtONpoWAhIaufnMuaV0wDwYDVR0jBAgwBoAECgsMDTAPBgNVHQ8BAf8EBQMDB/mA +MAoGCCqGSM49BAMCA0gAMEUCIQDlNWeY4ia5oU7JhTPoDubhuORdfqgVwr0MFENf +NLDQ/gIgOPLRom6vbqZC1EuT234zn1csFnSkDp9EFOS1z7URJPk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPDCCAeKgAwIBAgIEEhISEjAKBggqhkjOPQQDAjByMQswCQYDVQQGEwJHQjEP +MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdvb2ds +ZTEMMAoGA1UECxMDRW5nMSIwIAYDVQQDExlGYWtlSW50ZXJtZWRpYXRlQXV0aG9y +aXR5MB4XDTE4MDQwNjExMDEwMVoXDTI4MDIxMzExMDEwMVowdTELMAkGA1UEBhMC +R0IxDzANBgNVBAgTBkxvbmRvbjEPMA0GA1UEBxMGTG9uZG9uMQ8wDQYDVQQKEwZH +b29nbGUxDDAKBgNVBAsTA0VuZzElMCMGA1UEAxMcRmFrZVN1YkludGVybWVkaWF0 +ZUF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGViTbXYtpdRZ0f4 +QhdHo39C9s8zCtWAmWsnHub3d0OlhJxazVV1uHTgJGDE7Euff4vyH0D2j7xnjsmV +GGenafOjYzBhMA0GA1UdDgQGBAQKCwwNMA8GA1UdIwQIMAaABAoLDA0wDwYDVR0T +AQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDB/+AMB0GA1UdJQQWMBQGCCsGAQUFBwMB +BggrBgEFBQcDAjAKBggqhkjOPQQDAgNIADBFAiBRmlMEQHyEenjH9eft8K/9Aj0s +Q5TMzI8domdMGSTktwIhAKIJWSuJsn9C0QXKFAxlOVJsjlgIIuLuyUvrrbKKlz45 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICODCCAd6gAwIBAgIEAQEBATAKBggqhkjOPQQDAjBxMQswCQYDVQQGEwJHQjEP +MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdvb2ds +ZTEMMAoGA1UECxMDRW5nMSEwHwYDVQQDExhGYWtlQ2VydGlmaWNhdGVBdXRob3Jp +dHkwHhcNMTgwNDA2MTEwMTAxWhcNMjgwMjEzMTEwMTAxWjByMQswCQYDVQQGEwJH +QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdv +b2dsZTEMMAoGA1UECxMDRW5nMSIwIAYDVQQDExlGYWtlSW50ZXJtZWRpYXRlQXV0 +aG9yaXR5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEgjivMuD8A9GCkFMrYcP +xWS7aFP5VKV4Bm46cibTzCMuQZVBgSxgkZ3nMbPFo0fif2f84yrcum4ntUHVX3uV +bKNjMGEwDQYDVR0OBAYEBAoLDA0wDwYDVR0jBAgwBoAEAQIDBDAPBgNVHRMBAf8E +BTADAQH/MA8GA1UdDwEB/wQFAwMH/4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAoGCCqGSM49BAMCA0gAMEUCIEEDVMxuFamEthOVmELENUvfWIzwxKft +5vO/TNsFG/rMAiEAtKzSTGMqtqKeR5Nnj9vOSPWYnj2R6Dmdvcw3fbcMvyQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHDCCAcGgAwIBAgIEBAbK/jAKBggqhkjOPQQDAjBxMQswCQYDVQQGEwJHQjEP +MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdvb2ds +ZTEMMAoGA1UECxMDRW5nMSEwHwYDVQQDExhGYWtlQ2VydGlmaWNhdGVBdXRob3Jp +dHkwHhcNMTYxMjA3MTUxMzM2WhcNMjYxMjA1MTUxMzM2WjBxMQswCQYDVQQGEwJH +QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdv +b2dsZTEMMAoGA1UECxMDRW5nMSEwHwYDVQQDExhGYWtlQ2VydGlmaWNhdGVBdXRo +b3JpdHkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATy0wfvft/PzvT0Clu8nj/L +HP0MRtyF+8H207K6HVHxmGxIqBVGRWPK39bJrM9gO8dO3bjSFqugCSQdCWYeTeuh +o0cwRTANBgNVHQ4EBgQEAQIDBDAPBgNVHSMECDAGgAQBAgMEMBIGA1UdEwEB/wQI +MAYBAf8CAQowDwYDVR0PAQH/BAUDAwf/gDAKBggqhkjOPQQDAgNJADBGAiEApihJ +OUNvgORDph47qolewiVgKuE5vVVDrk1cqabvrGUCIQDJxQjGWZO0hnCla1QrW/wM +iGuwIwcrxwwn3octloDVVg== +-----END CERTIFICATE----- diff --git a/personalities/sctfe/testdata/subleaf.misordered.chain b/personalities/sctfe/testdata/subleaf.misordered.chain new file mode 100644 index 00000000..2f047d48 --- /dev/null +++ b/personalities/sctfe/testdata/subleaf.misordered.chain @@ -0,0 +1,55 @@ +-----BEGIN CERTIFICATE----- +MIICAjCCAaigAwIBAgIFAN6tvu8wCgYIKoZIzj0EAwIwdTELMAkGA1UEBhMCR0Ix +DzANBgNVBAgTBkxvbmRvbjEPMA0GA1UEBxMGTG9uZG9uMQ8wDQYDVQQKEwZHb29n +bGUxDDAKBgNVBAsTA0VuZzElMCMGA1UEAxMcRmFrZVN1YkludGVybWVkaWF0ZUF1 +dGhvcml0eTAeFw0xODA0MDYxMTAxMDFaFw0yNTA1MTkxMTAxMDFaMFcxCzAJBgNV +BAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAoMBkdvb2dsZTEMMAoGA1UE +CwwDRW5nMRgwFgYDVQQDDA9zdWJsZWFmLmNzci5wZW0wWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAATrN05SRZxG1ai4xe1YuTAppnCKaaAmXJ4vbrhrI2yE4UY6mDaC +RKWKF4tBgjL0LeAIW34HOFL8R1YoJ5vtYIuso0MwQTAdBgNVHQ4EFgQUP7IvQfwR +mtONpoWAhIaufnMuaV0wDwYDVR0jBAgwBoAECgsMDTAPBgNVHQ8BAf8EBQMDB/mA +MAoGCCqGSM49BAMCA0gAMEUCIQDlNWeY4ia5oU7JhTPoDubhuORdfqgVwr0MFENf +NLDQ/gIgOPLRom6vbqZC1EuT234zn1csFnSkDp9EFOS1z7URJPk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICODCCAd6gAwIBAgIEAQEBATAKBggqhkjOPQQDAjBxMQswCQYDVQQGEwJHQjEP +MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdvb2ds +ZTEMMAoGA1UECxMDRW5nMSEwHwYDVQQDExhGYWtlQ2VydGlmaWNhdGVBdXRob3Jp +dHkwHhcNMTgwNDA2MTEwMTAxWhcNMjgwMjEzMTEwMTAxWjByMQswCQYDVQQGEwJH +QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdv +b2dsZTEMMAoGA1UECxMDRW5nMSIwIAYDVQQDExlGYWtlSW50ZXJtZWRpYXRlQXV0 +aG9yaXR5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEgjivMuD8A9GCkFMrYcP +xWS7aFP5VKV4Bm46cibTzCMuQZVBgSxgkZ3nMbPFo0fif2f84yrcum4ntUHVX3uV +bKNjMGEwDQYDVR0OBAYEBAoLDA0wDwYDVR0jBAgwBoAEAQIDBDAPBgNVHRMBAf8E +BTADAQH/MA8GA1UdDwEB/wQFAwMH/4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAoGCCqGSM49BAMCA0gAMEUCIEEDVMxuFamEthOVmELENUvfWIzwxKft +5vO/TNsFG/rMAiEAtKzSTGMqtqKeR5Nnj9vOSPWYnj2R6Dmdvcw3fbcMvyQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPDCCAeKgAwIBAgIEEhISEjAKBggqhkjOPQQDAjByMQswCQYDVQQGEwJHQjEP +MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdvb2ds +ZTEMMAoGA1UECxMDRW5nMSIwIAYDVQQDExlGYWtlSW50ZXJtZWRpYXRlQXV0aG9y +aXR5MB4XDTE4MDQwNjExMDEwMVoXDTI4MDIxMzExMDEwMVowdTELMAkGA1UEBhMC +R0IxDzANBgNVBAgTBkxvbmRvbjEPMA0GA1UEBxMGTG9uZG9uMQ8wDQYDVQQKEwZH +b29nbGUxDDAKBgNVBAsTA0VuZzElMCMGA1UEAxMcRmFrZVN1YkludGVybWVkaWF0 +ZUF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGViTbXYtpdRZ0f4 +QhdHo39C9s8zCtWAmWsnHub3d0OlhJxazVV1uHTgJGDE7Euff4vyH0D2j7xnjsmV +GGenafOjYzBhMA0GA1UdDgQGBAQKCwwNMA8GA1UdIwQIMAaABAoLDA0wDwYDVR0T +AQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDB/+AMB0GA1UdJQQWMBQGCCsGAQUFBwMB +BggrBgEFBQcDAjAKBggqhkjOPQQDAgNIADBFAiBRmlMEQHyEenjH9eft8K/9Aj0s +Q5TMzI8domdMGSTktwIhAKIJWSuJsn9C0QXKFAxlOVJsjlgIIuLuyUvrrbKKlz45 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHDCCAcGgAwIBAgIEBAbK/jAKBggqhkjOPQQDAjBxMQswCQYDVQQGEwJHQjEP +MA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdvb2ds +ZTEMMAoGA1UECxMDRW5nMSEwHwYDVQQDExhGYWtlQ2VydGlmaWNhdGVBdXRob3Jp +dHkwHhcNMTYxMjA3MTUxMzM2WhcNMjYxMjA1MTUxMzM2WjBxMQswCQYDVQQGEwJH +QjEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDzANBgNVBAoTBkdv +b2dsZTEMMAoGA1UECxMDRW5nMSEwHwYDVQQDExhGYWtlQ2VydGlmaWNhdGVBdXRo +b3JpdHkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATy0wfvft/PzvT0Clu8nj/L +HP0MRtyF+8H207K6HVHxmGxIqBVGRWPK39bJrM9gO8dO3bjSFqugCSQdCWYeTeuh +o0cwRTANBgNVHQ4EBgQEAQIDBDAPBgNVHSMECDAGgAQBAgMEMBIGA1UdEwEB/wQI +MAYBAf8CAQowDwYDVR0PAQH/BAUDAwf/gDAKBggqhkjOPQQDAgNJADBGAiEApihJ +OUNvgORDph47qolewiVgKuE5vVVDrk1cqabvrGUCIQDJxQjGWZO0hnCla1QrW/wM +iGuwIwcrxwwn3octloDVVg== +-----END CERTIFICATE-----