Skip to content

Commit

Permalink
Port certificate-transparency-go's Trillian CTFE to Trillian Tessera (#…
Browse files Browse the repository at this point in the history
…105)

* Copy CTFE from certificate-transparency-go

* Port CTFE to Tessera

* refactor MerkleLeafHash

* address comments

* simplify configs methods

* unexport a few methods, and add comments

* edit logorigin comment

* drop support for non ECDSA public keys

* fix copyright

* create newctstorage more beautifuler

* s/ct-static-api/sctfe
  • Loading branch information
phbnf authored Aug 2, 2024
1 parent 71ae375 commit af78c5e
Show file tree
Hide file tree
Showing 27 changed files with 4,023 additions and 18 deletions.
26 changes: 17 additions & 9 deletions ctonly/ct.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 24 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
)
66 changes: 59 additions & 7 deletions go.sum

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions personalities/sctfe/cert_checker.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit af78c5e

Please sign in to comment.