From 563934f43af809965e83573770c1601f632f2642 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Wed, 5 Feb 2025 11:48:12 +0000 Subject: [PATCH 1/4] copy types.go from c-t-go --- internal/types/types.go | 576 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 internal/types/types.go diff --git a/internal/types/types.go b/internal/types/types.go new file mode 100644 index 00000000..e89a4229 --- /dev/null +++ b/internal/types/types.go @@ -0,0 +1,576 @@ +package types + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/google/certificate-transparency-go/tls" + "github.com/google/certificate-transparency-go/x509" +) + +/////////////////////////////////////////////////////////////////////////////// +// The following structures represent those outlined in RFC6962; any section +// numbers mentioned refer to that RFC. +/////////////////////////////////////////////////////////////////////////////// + +// LogEntryType represents the LogEntryType enum from section 3.1: +// +// enum { x509_entry(0), precert_entry(1), (65535) } LogEntryType; +type LogEntryType tls.Enum // tls:"maxval:65535" + +// LogEntryType constants from section 3.1. +const ( + X509LogEntryType LogEntryType = 0 + PrecertLogEntryType LogEntryType = 1 +) + +func (e LogEntryType) String() string { + switch e { + case X509LogEntryType: + return "X509LogEntryType" + case PrecertLogEntryType: + return "PrecertLogEntryType" + default: + return fmt.Sprintf("UnknownEntryType(%d)", e) + } +} + +// RFC6962 section 2.1 requires a prefix byte on hash inputs for second preimage resistance. +const ( + TreeLeafPrefix = byte(0x00) + TreeNodePrefix = byte(0x01) +) + +// MerkleLeafType represents the MerkleLeafType enum from section 3.4: +// +// enum { timestamped_entry(0), (255) } MerkleLeafType; +type MerkleLeafType tls.Enum // tls:"maxval:255" + +// TimestampedEntryLeafType is the only defined MerkleLeafType constant from section 3.4. +const TimestampedEntryLeafType MerkleLeafType = 0 // Entry type for an SCT + +func (m MerkleLeafType) String() string { + switch m { + case TimestampedEntryLeafType: + return "TimestampedEntryLeafType" + default: + return fmt.Sprintf("UnknownLeafType(%d)", m) + } +} + +// Version represents the Version enum from section 3.2: +// +// enum { v1(0), (255) } Version; +type Version tls.Enum // tls:"maxval:255" + +// CT Version constants from section 3.2. +const ( + V1 Version = 0 +) + +func (v Version) String() string { + switch v { + case V1: + return "V1" + default: + return fmt.Sprintf("UnknownVersion(%d)", v) + } +} + +// SignatureType differentiates STH signatures from SCT signatures, see section 3.2. +// +// enum { certificate_timestamp(0), tree_hash(1), (255) } SignatureType; +type SignatureType tls.Enum // tls:"maxval:255" + +// SignatureType constants from section 3.2. +const ( + CertificateTimestampSignatureType SignatureType = 0 + TreeHashSignatureType SignatureType = 1 +) + +func (st SignatureType) String() string { + switch st { + case CertificateTimestampSignatureType: + return "CertificateTimestamp" + case TreeHashSignatureType: + return "TreeHash" + default: + return fmt.Sprintf("UnknownSignatureType(%d)", st) + } +} + +// ASN1Cert type for holding the raw DER bytes of an ASN.1 Certificate +// (section 3.1). +type ASN1Cert struct { + Data []byte `tls:"minlen:1,maxlen:16777215"` +} + +// LogID holds the hash of the Log's public key (section 3.2). +// TODO(pphaneuf): Users should be migrated to the one in the logid package. +type LogID struct { + KeyID [sha256.Size]byte +} + +// PreCert represents a Precertificate (section 3.2). +type PreCert struct { + IssuerKeyHash [sha256.Size]byte + TBSCertificate []byte `tls:"minlen:1,maxlen:16777215"` // DER-encoded TBSCertificate +} + +// CTExtensions is a representation of the raw bytes of any CtExtension +// structure (see section 3.2). +// nolint: revive +type CTExtensions []byte // tls:"minlen:0,maxlen:65535"` + +// MerkleTreeNode represents an internal node in the CT tree. +type MerkleTreeNode []byte + +// ConsistencyProof represents a CT consistency proof (see sections 2.1.2 and +// 4.4). +type ConsistencyProof []MerkleTreeNode + +// AuditPath represents a CT inclusion proof (see sections 2.1.1 and 4.5). +type AuditPath []MerkleTreeNode + +// LeafInput represents a serialized MerkleTreeLeaf structure. +type LeafInput []byte + +// DigitallySigned is a local alias for tls.DigitallySigned so that we can +// attach a MarshalJSON method. +type DigitallySigned tls.DigitallySigned + +// FromBase64String populates the DigitallySigned structure from the base64 data passed in. +// Returns an error if the base64 data is invalid. +func (d *DigitallySigned) FromBase64String(b64 string) error { + raw, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + return fmt.Errorf("failed to unbase64 DigitallySigned: %v", err) + } + var ds tls.DigitallySigned + if rest, err := tls.Unmarshal(raw, &ds); err != nil { + return fmt.Errorf("failed to unmarshal DigitallySigned: %v", err) + } else if len(rest) > 0 { + return fmt.Errorf("trailing data (%d bytes) after DigitallySigned", len(rest)) + } + *d = DigitallySigned(ds) + return nil +} + +// Base64String returns the base64 representation of the DigitallySigned struct. +func (d DigitallySigned) Base64String() (string, error) { + b, err := tls.Marshal(d) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(b), nil +} + +// MarshalJSON implements the json.Marshaller interface. +func (d DigitallySigned) MarshalJSON() ([]byte, error) { + b64, err := d.Base64String() + if err != nil { + return []byte{}, err + } + return []byte(`"` + b64 + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *DigitallySigned) UnmarshalJSON(b []byte) error { + var content string + if err := json.Unmarshal(b, &content); err != nil { + return fmt.Errorf("failed to unmarshal DigitallySigned: %v", err) + } + return d.FromBase64String(content) +} + +// RawLogEntry represents the (TLS-parsed) contents of an entry in a CT log. +type RawLogEntry struct { + // Index is a position of the entry in the log. + Index int64 + // Leaf is a parsed Merkle leaf hash input. + Leaf MerkleTreeLeaf + // Cert is: + // - A certificate if Leaf.TimestampedEntry.EntryType is X509LogEntryType. + // - A precertificate if Leaf.TimestampedEntry.EntryType is + // PrecertLogEntryType, in the form of a DER-encoded Certificate as + // originally added (which includes the poison extension and a signature + // generated over the pre-cert by the pre-cert issuer). + // - Empty otherwise. + Cert ASN1Cert + // Chain is the issuing certificate chain starting with the issuer of Cert, + // or an empty slice if Cert is empty. + Chain []ASN1Cert +} + +// LogEntry represents the (parsed) contents of an entry in a CT log. This is described +// in section 3.1, but note that this structure does *not* match the TLS structure +// defined there (the TLS structure is never used directly in RFC6962). +type LogEntry struct { + Index int64 + Leaf MerkleTreeLeaf + // Exactly one of the following three fields should be non-empty. + X509Cert *x509.Certificate // Parsed X.509 certificate + Precert *Precertificate // Extracted precertificate + JSONData []byte + + // Chain holds the issuing certificate chain, starting with the + // issuer of the leaf certificate / pre-certificate. + Chain []ASN1Cert +} + +// PrecertChainEntry holds an precertificate together with a validation chain +// for it; see section 3.1. +type PrecertChainEntry struct { + PreCertificate ASN1Cert `tls:"minlen:1,maxlen:16777215"` + CertificateChain []ASN1Cert `tls:"minlen:0,maxlen:16777215"` +} + +// CertificateChain holds a chain of certificates, as returned as extra data +// for get-entries (section 4.6). +type CertificateChain struct { + Entries []ASN1Cert `tls:"minlen:0,maxlen:16777215"` +} + +// PrecertChainEntryHash is an extended PrecertChainEntry type with the +// IssuanceChainHash field added to store the hash of the +// CertificateChain field of PrecertChainEntry. +type PrecertChainEntryHash struct { + PreCertificate ASN1Cert `tls:"minlen:1,maxlen:16777215"` + IssuanceChainHash []byte `tls:"minlen:0,maxlen:256"` +} + +// CertificateChainHash is an extended CertificateChain type with the +// IssuanceChainHash field added to store the hash of the +// Entries field of CertificateChain. +type CertificateChainHash struct { + IssuanceChainHash []byte `tls:"minlen:0,maxlen:256"` +} + +// JSONDataEntry holds arbitrary data. +type JSONDataEntry struct { + Data []byte `tls:"minlen:0,maxlen:1677215"` +} + +// SHA256Hash represents the output from the SHA256 hash function. +type SHA256Hash [sha256.Size]byte + +// FromBase64String populates the SHA256 struct with the contents of the base64 data passed in. +func (s *SHA256Hash) FromBase64String(b64 string) error { + bs, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + return fmt.Errorf("failed to unbase64 LogID: %v", err) + } + if len(bs) != sha256.Size { + return fmt.Errorf("invalid SHA256 length, expected 32 but got %d", len(bs)) + } + copy(s[:], bs) + return nil +} + +// Base64String returns the base64 representation of this SHA256Hash. +func (s SHA256Hash) Base64String() string { + return base64.StdEncoding.EncodeToString(s[:]) +} + +// MarshalJSON implements the json.Marshaller interface for SHA256Hash. +func (s SHA256Hash) MarshalJSON() ([]byte, error) { + return []byte(`"` + s.Base64String() + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaller interface. +func (s *SHA256Hash) UnmarshalJSON(b []byte) error { + var content string + if err := json.Unmarshal(b, &content); err != nil { + return fmt.Errorf("failed to unmarshal SHA256Hash: %v", err) + } + return s.FromBase64String(content) +} + +// SignedTreeHead represents the structure returned by the get-sth CT method +// after base64 decoding; see sections 3.5 and 4.3. +type SignedTreeHead struct { + Version Version `json:"sth_version"` // The version of the protocol to which the STH conforms + TreeSize uint64 `json:"tree_size"` // The number of entries in the new tree + Timestamp uint64 `json:"timestamp"` // The time at which the STH was created + SHA256RootHash SHA256Hash `json:"sha256_root_hash"` // The root hash of the log's Merkle tree + TreeHeadSignature DigitallySigned `json:"tree_head_signature"` // Log's signature over a TLS-encoded TreeHeadSignature + LogID SHA256Hash `json:"log_id"` // The SHA256 hash of the log's public key +} + +func (s SignedTreeHead) String() string { + sigStr, err := s.TreeHeadSignature.Base64String() + if err != nil { + sigStr = tls.DigitallySigned(s.TreeHeadSignature).String() + } + + // If the LogID field in the SignedTreeHead is empty, don't include it in + // the string. + var logIDStr string + if id, empty := s.LogID, (SHA256Hash{}); id != empty { + logIDStr = fmt.Sprintf("LogID:%s, ", id.Base64String()) + } + + return fmt.Sprintf("{%sTreeSize:%d, Timestamp:%d, SHA256RootHash:%q, TreeHeadSignature:%q}", + logIDStr, s.TreeSize, s.Timestamp, s.SHA256RootHash.Base64String(), sigStr) +} + +// TreeHeadSignature holds the data over which the signature in an STH is +// generated; see section 3.5 +type TreeHeadSignature struct { + Version Version `tls:"maxval:255"` + SignatureType SignatureType `tls:"maxval:255"` // == TreeHashSignatureType + Timestamp uint64 + TreeSize uint64 + SHA256RootHash SHA256Hash +} + +// SignedCertificateTimestamp represents the structure returned by the +// add-chain and add-pre-chain methods after base64 decoding; see sections +// 3.2, 4.1 and 4.2. +type SignedCertificateTimestamp struct { + SCTVersion Version `tls:"maxval:255"` + LogID LogID + Timestamp uint64 + Extensions CTExtensions `tls:"minlen:0,maxlen:65535"` + Signature DigitallySigned // Signature over TLS-encoded CertificateTimestamp +} + +// CertificateTimestamp is the collection of data that the signature in an +// SCT is over; see section 3.2. +type CertificateTimestamp struct { + SCTVersion Version `tls:"maxval:255"` + SignatureType SignatureType `tls:"maxval:255"` + Timestamp uint64 + EntryType LogEntryType `tls:"maxval:65535"` + X509Entry *ASN1Cert `tls:"selector:EntryType,val:0"` + PrecertEntry *PreCert `tls:"selector:EntryType,val:1"` + JSONEntry *JSONDataEntry `tls:"selector:EntryType,val:32768"` + Extensions CTExtensions `tls:"minlen:0,maxlen:65535"` +} + +func (s SignedCertificateTimestamp) String() string { + return fmt.Sprintf("{Version:%d LogId:%s Timestamp:%d Extensions:'%s' Signature:%v}", s.SCTVersion, + base64.StdEncoding.EncodeToString(s.LogID.KeyID[:]), + s.Timestamp, + s.Extensions, + s.Signature) +} + +// TimestampedEntry is part of the MerkleTreeLeaf structure; see section 3.4. +type TimestampedEntry struct { + Timestamp uint64 + EntryType LogEntryType `tls:"maxval:65535"` + X509Entry *ASN1Cert `tls:"selector:EntryType,val:0"` + PrecertEntry *PreCert `tls:"selector:EntryType,val:1"` + JSONEntry *JSONDataEntry `tls:"selector:EntryType,val:32768"` + Extensions CTExtensions `tls:"minlen:0,maxlen:65535"` +} + +// MerkleTreeLeaf represents the deserialized structure of the hash input for the +// leaves of a log's Merkle tree; see section 3.4. +type MerkleTreeLeaf struct { + Version Version `tls:"maxval:255"` + LeafType MerkleLeafType `tls:"maxval:255"` + TimestampedEntry *TimestampedEntry `tls:"selector:LeafType,val:0"` +} + +// Precertificate represents the parsed CT Precertificate structure. +type Precertificate struct { + // DER-encoded pre-certificate as originally added, which includes a + // poison extension and a signature generated over the pre-cert by + // the pre-cert issuer (which might differ from the issuer of the final + // cert, see RFC6962 s3.1). + Submitted ASN1Cert + // SHA256 hash of the issuing key + IssuerKeyHash [sha256.Size]byte + // Parsed TBSCertificate structure, held in an x509.Certificate for convenience. + TBSCertificate *x509.Certificate +} + +// X509Certificate returns the X.509 Certificate contained within the +// MerkleTreeLeaf. +func (m *MerkleTreeLeaf) X509Certificate() (*x509.Certificate, error) { + if m.TimestampedEntry.EntryType != X509LogEntryType { + return nil, fmt.Errorf("cannot call X509Certificate on a MerkleTreeLeaf that is not an X509 entry") + } + return x509.ParseCertificate(m.TimestampedEntry.X509Entry.Data) +} + +// Precertificate returns the X.509 Precertificate contained within the MerkleTreeLeaf. +// +// The returned precertificate is embedded in an x509.Certificate, but is in the +// form stored internally in the log rather than the original submitted form +// (i.e. it does not include the poison extension and any changes to reflect the +// final certificate's issuer have been made; see x509.BuildPrecertTBS). +func (m *MerkleTreeLeaf) Precertificate() (*x509.Certificate, error) { + if m.TimestampedEntry.EntryType != PrecertLogEntryType { + return nil, fmt.Errorf("cannot call Precertificate on a MerkleTreeLeaf that is not a precert entry") + } + return x509.ParseTBSCertificate(m.TimestampedEntry.PrecertEntry.TBSCertificate) +} + +// APIEndpoint is a string that represents one of the Certificate Transparency +// Log API endpoints. +type APIEndpoint string + +// Certificate Transparency Log API endpoints; see section 4. +// WARNING: Should match the URI paths without the "/ct/v1/" prefix. If +// changing these constants, may need to change those too. +const ( + AddChainStr APIEndpoint = "add-chain" + AddPreChainStr APIEndpoint = "add-pre-chain" + GetSTHStr APIEndpoint = "get-sth" + GetEntriesStr APIEndpoint = "get-entries" + GetProofByHashStr APIEndpoint = "get-proof-by-hash" + GetSTHConsistencyStr APIEndpoint = "get-sth-consistency" + GetRootsStr APIEndpoint = "get-roots" + GetEntryAndProofStr APIEndpoint = "get-entry-and-proof" +) + +// URI paths for Log requests; see section 4. +// WARNING: Should match the API endpoints, with the "/ct/v1/" prefix. If +// changing these constants, may need to change those too. +const ( + AddChainPath = "/ct/v1/add-chain" + AddPreChainPath = "/ct/v1/add-pre-chain" + GetSTHPath = "/ct/v1/get-sth" + GetEntriesPath = "/ct/v1/get-entries" + GetProofByHashPath = "/ct/v1/get-proof-by-hash" + GetSTHConsistencyPath = "/ct/v1/get-sth-consistency" + GetRootsPath = "/ct/v1/get-roots" + GetEntryAndProofPath = "/ct/v1/get-entry-and-proof" + + AddJSONPath = "/ct/v1/add-json" // Experimental addition +) + +// AddChainRequest represents the JSON request body sent to the add-chain and +// add-pre-chain POST methods from sections 4.1 and 4.2. +type AddChainRequest struct { + Chain [][]byte `json:"chain"` +} + +// AddChainResponse represents the JSON response to the add-chain and +// add-pre-chain POST methods. +// An SCT represents a Log's promise to integrate a [pre-]certificate into the +// log within a defined period of time. +type AddChainResponse struct { + SCTVersion Version `json:"sct_version"` // SCT structure version + ID []byte `json:"id"` // Log ID + Timestamp uint64 `json:"timestamp"` // Timestamp of issuance + Extensions string `json:"extensions"` // Holder for any CT extensions + Signature []byte `json:"signature"` // Log signature for this SCT +} + +// ToSignedCertificateTimestamp creates a SignedCertificateTimestamp from the +// AddChainResponse. +func (r *AddChainResponse) ToSignedCertificateTimestamp() (*SignedCertificateTimestamp, error) { + sct := SignedCertificateTimestamp{ + SCTVersion: r.SCTVersion, + Timestamp: r.Timestamp, + } + + if len(r.ID) != sha256.Size { + return nil, fmt.Errorf("id is invalid length, expected %d got %d", sha256.Size, len(r.ID)) + } + copy(sct.LogID.KeyID[:], r.ID) + + exts, err := base64.StdEncoding.DecodeString(r.Extensions) + if err != nil { + return nil, fmt.Errorf("invalid base64 data in Extensions (%q): %v", r.Extensions, err) + } + sct.Extensions = CTExtensions(exts) + + var ds DigitallySigned + if rest, err := tls.Unmarshal(r.Signature, &ds); err != nil { + return nil, fmt.Errorf("tls.Unmarshal(): %s", err) + } else if len(rest) > 0 { + return nil, fmt.Errorf("trailing data (%d bytes) after DigitallySigned", len(rest)) + } + sct.Signature = ds + + return &sct, nil +} + +// AddJSONRequest represents the JSON request body sent to the add-json POST method. +// The corresponding response re-uses AddChainResponse. +// This is an experimental addition not covered by RFC6962. +type AddJSONRequest struct { + Data interface{} `json:"data"` +} + +// GetSTHResponse represents the JSON response to the get-sth GET method from section 4.3. +type GetSTHResponse struct { + TreeSize uint64 `json:"tree_size"` // Number of certs in the current tree + Timestamp uint64 `json:"timestamp"` // Time that the tree was created + SHA256RootHash []byte `json:"sha256_root_hash"` // Root hash of the tree + TreeHeadSignature []byte `json:"tree_head_signature"` // Log signature for this STH +} + +// ToSignedTreeHead creates a SignedTreeHead from the GetSTHResponse. +func (r *GetSTHResponse) ToSignedTreeHead() (*SignedTreeHead, error) { + sth := SignedTreeHead{ + TreeSize: r.TreeSize, + Timestamp: r.Timestamp, + } + + if len(r.SHA256RootHash) != sha256.Size { + return nil, fmt.Errorf("sha256_root_hash is invalid length, expected %d got %d", sha256.Size, len(r.SHA256RootHash)) + } + copy(sth.SHA256RootHash[:], r.SHA256RootHash) + + var ds DigitallySigned + if rest, err := tls.Unmarshal(r.TreeHeadSignature, &ds); err != nil { + return nil, fmt.Errorf("tls.Unmarshal(): %s", err) + } else if len(rest) > 0 { + return nil, fmt.Errorf("trailing data (%d bytes) after DigitallySigned", len(rest)) + } + sth.TreeHeadSignature = ds + + return &sth, nil +} + +// GetSTHConsistencyResponse represents the JSON response to the get-sth-consistency +// GET method from section 4.4. (The corresponding GET request has parameters 'first' and +// 'second'.) +type GetSTHConsistencyResponse struct { + Consistency [][]byte `json:"consistency"` +} + +// GetProofByHashResponse represents the JSON response to the get-proof-by-hash GET +// method from section 4.5. (The corresponding GET request has parameters 'hash' +// and 'tree_size'.) +type GetProofByHashResponse struct { + LeafIndex int64 `json:"leaf_index"` // The 0-based index of the end entity corresponding to the "hash" parameter. + AuditPath [][]byte `json:"audit_path"` // An array of base64-encoded Merkle Tree nodes proving the inclusion of the chosen certificate. +} + +// LeafEntry represents a leaf in the Log's Merkle tree, as returned by the get-entries +// GET method from section 4.6. +type LeafEntry struct { + // LeafInput is a TLS-encoded MerkleTreeLeaf + LeafInput []byte `json:"leaf_input"` + // ExtraData holds (unsigned) extra data, normally the cert validation chain. + ExtraData []byte `json:"extra_data"` +} + +// GetEntriesResponse represents the JSON response to the get-entries GET method +// from section 4.6. +type GetEntriesResponse struct { + Entries []LeafEntry `json:"entries"` // the list of returned entries +} + +// GetRootsResponse represents the JSON response to the get-roots GET method from section 4.7. +type GetRootsResponse struct { + Certificates []string `json:"certificates"` +} + +// GetEntryAndProofResponse represents the JSON response to the get-entry-and-proof +// GET method from section 4.8. (The corresponding GET request has parameters 'leaf_index' +// and 'tree_size'.) +type GetEntryAndProofResponse struct { + LeafInput []byte `json:"leaf_input"` // the entry itself + ExtraData []byte `json:"extra_data"` // any chain provided when the entry was added to the log + AuditPath [][]byte `json:"audit_path"` // the corresponding proof +} From e2a8fff8e2d9ed3fe6ee44da38ff18cd50996932 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Wed, 5 Feb 2025 11:55:07 +0000 Subject: [PATCH 2/4] migrate to local and internal types # Conflicts: # ctlog.go # Conflicts: # internal/scti/handlers_test.go # internal/scti/signatures_test.go # Conflicts: # ctlog.go # internal/scti/handlers.go # internal/scti/handlers_test.go # internal/scti/signatures.go --- internal/scti/ctlog.go | 4 +-- internal/scti/handlers.go | 27 +++++++++---------- internal/scti/handlers_test.go | 45 ++++++++++++++++---------------- internal/scti/signatures.go | 31 +++++++++++----------- internal/scti/signatures_test.go | 35 ++++++++++++------------- 5 files changed, 69 insertions(+), 73 deletions(-) diff --git a/internal/scti/ctlog.go b/internal/scti/ctlog.go index f24b148c..b7dc7c7c 100644 --- a/internal/scti/ctlog.go +++ b/internal/scti/ctlog.go @@ -7,8 +7,8 @@ import ( "errors" "fmt" - ct "github.com/google/certificate-transparency-go" "github.com/google/certificate-transparency-go/x509" + "github.com/transparency-dev/static-ct/internal/types" "github.com/transparency-dev/static-ct/modules/dedup" "github.com/transparency-dev/static-ct/storage" tessera "github.com/transparency-dev/trillian-tessera" @@ -64,7 +64,7 @@ func NewLog(ctx context.Context, origin string, signer crypto.Signer, cvOpts Cha return nil, fmt.Errorf("unsupported key type: %v", keyType) } - log.signSCT = func(leaf *ct.MerkleTreeLeaf) (*ct.SignedCertificateTimestamp, error) { + log.signSCT = func(leaf *types.MerkleTreeLeaf) (*types.SignedCertificateTimestamp, error) { return buildV1SCT(signer, leaf) } diff --git a/internal/scti/handlers.go b/internal/scti/handlers.go index 8d82433f..80c501e1 100644 --- a/internal/scti/handlers.go +++ b/internal/scti/handlers.go @@ -32,12 +32,11 @@ import ( "github.com/google/certificate-transparency-go/x509" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/transparency-dev/static-ct/internal/types" "github.com/transparency-dev/static-ct/modules/dedup" tessera "github.com/transparency-dev/trillian-tessera" "github.com/transparency-dev/trillian-tessera/ctonly" "k8s.io/klog/v2" - - ct "github.com/google/certificate-transparency-go" ) const ( @@ -206,9 +205,9 @@ func NewPathHandlers(opts *HandlerOptions, log *log) pathHandlers { // Bind each endpoint to an appHandler instance. // TODO(phboneff): try and get rid of PathHandlers and appHandler ph := pathHandlers{ - prefix + ct.AddChainPath: appHandler{opts: opts, log: log, handler: addChain, name: addChainName, method: http.MethodPost}, - prefix + ct.AddPreChainPath: appHandler{opts: opts, log: log, handler: addPreChain, name: addPreChainName, method: http.MethodPost}, - prefix + ct.GetRootsPath: appHandler{opts: opts, log: log, handler: getRoots, name: getRootsName, method: http.MethodGet}, + prefix + types.AddChainPath: appHandler{opts: opts, log: log, handler: addChain, name: addChainName, method: http.MethodPost}, + prefix + types.AddPreChainPath: appHandler{opts: opts, log: log, handler: addPreChain, name: addPreChainName, method: http.MethodPost}, + prefix + types.GetRootsPath: appHandler{opts: opts, log: log, handler: getRoots, name: getRootsName, method: http.MethodGet}, } return ph @@ -224,23 +223,23 @@ func (opts *HandlerOptions) sendHTTPError(w http.ResponseWriter, statusCode int, } // parseBodyAsJSONChain tries to extract cert-chain out of request. -func parseBodyAsJSONChain(r *http.Request) (ct.AddChainRequest, error) { +func parseBodyAsJSONChain(r *http.Request) (types.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 + return types.AddChainRequest{}, err } - var req ct.AddChainRequest + var req types.AddChainRequest if err := json.Unmarshal(body, &req); err != nil { klog.V(1).Infof("Failed to parse request body: %v", err) - return ct.AddChainRequest{}, err + return types.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 types.AddChainRequest{}, errors.New("cert chain was empty") } return req, nil @@ -318,7 +317,7 @@ func addChainInternal(ctx context.Context, opts *HandlerOptions, log *log, w htt } // Always use the returned leaf as the basis for an SCT. - var loggedLeaf ct.MerkleTreeLeaf + var loggedLeaf types.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) @@ -387,7 +386,7 @@ func deadlineTime(opts *HandlerOptions) time.Time { // 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(log *log, req ct.AddChainRequest, expectingPrecert bool) ([]*x509.Certificate, error) { +func verifyAddChain(log *log, req types.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, log.chainValidationOpts) if err != nil { @@ -416,13 +415,13 @@ func verifyAddChain(log *log, req ct.AddChainRequest, expectingPrecert bool) ([] // 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, w http.ResponseWriter) error { +func marshalAndWriteAddChainResponse(sct *types.SignedCertificateTimestamp, w http.ResponseWriter) error { sig, err := tls.Marshal(sct.Signature) if err != nil { return fmt.Errorf("failed to marshal signature: %s", err) } - rsp := ct.AddChainResponse{ + rsp := types.AddChainResponse{ SCTVersion: sct.SCTVersion, Timestamp: sct.Timestamp, ID: sct.LogID.KeyID[:], diff --git a/internal/scti/handlers_test.go b/internal/scti/handlers_test.go index ef52719a..7aa70936 100644 --- a/internal/scti/handlers_test.go +++ b/internal/scti/handlers_test.go @@ -35,6 +35,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/transparency-dev/static-ct/internal/testdata" + "github.com/transparency-dev/static-ct/internal/types" "github.com/transparency-dev/static-ct/mockstorage" "github.com/transparency-dev/static-ct/modules/dedup" "github.com/transparency-dev/trillian-tessera/ctonly" @@ -42,8 +43,6 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "k8s.io/klog/v2" - - ct "github.com/google/certificate-transparency-go" ) // Arbitrary time for use in tests @@ -57,7 +56,7 @@ var origin = "example.com" var fakeDeadlineTime = time.Date(2016, 7, 22, 11, 01, 13, 500*1000*1000, time.UTC) var fakeTimeSource = newFixedTimeSource(fakeTime) -var entrypaths = []string{origin + ct.AddChainPath, origin + ct.AddPreChainPath, origin + ct.GetRootsPath} +var entrypaths = []string{origin + types.AddChainPath, origin + types.AddPreChainPath, origin + types.GetRootsPath} type handlerTestInfo struct { mockCtrl *gomock.Controller @@ -99,7 +98,7 @@ func setupTest(t *testing.T, pemRoots []string, signer crypto.Signer) handlerTes RequestLog: new(DefaultRequestLog), TimeSource: fakeTimeSource, } - signSCT := func(leaf *ct.MerkleTreeLeaf) (*ct.SignedCertificateTimestamp, error) { + signSCT := func(leaf *types.MerkleTreeLeaf) (*types.SignedCertificateTimestamp, error) { return buildV1SCT(signer, leaf) } log := log{ @@ -121,27 +120,27 @@ func setupTest(t *testing.T, pemRoots []string, signer crypto.Signer) handlerTes func (info handlerTestInfo) getHandlers(t *testing.T) pathHandlers { t.Helper() - handler, ok := info.handlers[origin+ct.GetRootsPath] + handler, ok := info.handlers[origin+types.GetRootsPath] if !ok { - t.Fatalf("%q path not registered", ct.GetRootsPath) + t.Fatalf("%q path not registered", types.GetRootsPath) } - return pathHandlers{origin + ct.GetRootsPath: handler} + return pathHandlers{origin + types.GetRootsPath: handler} } func (info handlerTestInfo) postHandlers(t *testing.T) pathHandlers { t.Helper() - addChainHandler, ok := info.handlers[origin+ct.AddChainPath] + addChainHandler, ok := info.handlers[origin+types.AddChainPath] if !ok { - t.Fatalf("%q path not registered", ct.AddPreChainStr) + t.Fatalf("%q path not registered", types.AddPreChainStr) } - addPreChainHandler, ok := info.handlers[origin+ct.AddPreChainPath] + addPreChainHandler, ok := info.handlers[origin+types.AddPreChainPath] if !ok { - t.Fatalf("%q path not registered", ct.AddPreChainStr) + t.Fatalf("%q path not registered", types.AddPreChainStr) } return map[string]appHandler{ - origin + ct.AddChainPath: addChainHandler, - origin + ct.AddPreChainPath: addPreChainHandler, + origin + types.AddChainPath: addChainHandler, + origin + types.AddPreChainPath: addPreChainHandler, } } @@ -339,7 +338,7 @@ func TestAddChainWhitespace(t *testing.T) { recorder := httptest.NewRecorder() handler, ok := info.handlers["example.com/ct/v1/add-chain"] if !ok { - t.Fatalf("%q path not registered", ct.AddChainStr) + t.Fatalf("%q path not registered", types.AddChainStr) } req, err := http.NewRequest(http.MethodPost, "http://example.com/ct/v1/add-chain", strings.NewReader(test.body)) if err != nil { @@ -422,12 +421,12 @@ func TestAddChain(t *testing.T) { t.Fatalf("addChain()=%d (body:%v); want %dv", recorder.Code, recorder.Body, test.want) } if test.want == http.StatusOK { - var resp ct.AddChainResponse + var resp types.AddChainResponse if err := json.NewDecoder(recorder.Body).Decode(&resp); err != nil { t.Fatalf("json.Decode(%s)=%v; want nil", recorder.Body.Bytes(), err) } - if got, want := ct.Version(resp.SCTVersion), ct.V1; got != want { + if got, want := types.Version(resp.SCTVersion), types.V1; got != want { t.Errorf("resp.SCTVersion=%v; want %v", got, want) } if got, want := resp.ID, demoLogID[:]; !bytes.Equal(got, want) { @@ -519,12 +518,12 @@ func TestAddPrechain(t *testing.T) { t.Fatalf("addPrechain()=%d (body:%v); want %d", recorder.Code, recorder.Body, test.want) } if test.want == http.StatusOK { - var resp ct.AddChainResponse + var resp types.AddChainResponse if err := json.NewDecoder(recorder.Body).Decode(&resp); err != nil { t.Fatalf("json.Decode(%s)=%v; want nil", recorder.Body.Bytes(), err) } - if got, want := ct.Version(resp.SCTVersion), ct.V1; got != want { + if got, want := types.Version(resp.SCTVersion), types.V1; got != want { t.Errorf("resp.SCTVersion=%v; want %v", got, want) } if got, want := resp.ID, demoLogID[:]; !bytes.Equal(got, want) { @@ -543,7 +542,7 @@ func TestAddPrechain(t *testing.T) { func createJSONChain(t *testing.T, p x509util.PEMCertPool) io.Reader { t.Helper() - var req ct.AddChainRequest + var req types.AddChainRequest for _, rawCert := range p.RawCertificates() { req.Chain = append(req.Chain, rawCert.Raw) } @@ -590,18 +589,18 @@ func (d dlMatcher) String() string { func makeAddPrechainRequest(t *testing.T, handlers pathHandlers, body io.Reader) *httptest.ResponseRecorder { t.Helper() - handler, ok := handlers[origin+ct.AddPreChainPath] + handler, ok := handlers[origin+types.AddPreChainPath] if !ok { - t.Fatalf("%q path not registered", ct.AddPreChainStr) + t.Fatalf("%q path not registered", types.AddPreChainStr) } return makeAddChainRequestInternal(t, handler, "add-pre-chain", body) } func makeAddChainRequest(t *testing.T, handlers pathHandlers, body io.Reader) *httptest.ResponseRecorder { t.Helper() - handler, ok := handlers[origin+ct.AddChainPath] + handler, ok := handlers[origin+types.AddChainPath] if !ok { - t.Fatalf("%q path not registered", ct.AddChainStr) + t.Fatalf("%q path not registered", types.AddChainStr) } return makeAddChainRequestInternal(t, handler, "add-chain", body) } diff --git a/internal/scti/signatures.go b/internal/scti/signatures.go index ef092948..0c6e0100 100644 --- a/internal/scti/signatures.go +++ b/internal/scti/signatures.go @@ -25,25 +25,24 @@ import ( "github.com/google/certificate-transparency-go/tls" "github.com/google/certificate-transparency-go/x509" tfl "github.com/transparency-dev/formats/log" + "github.com/transparency-dev/static-ct/internal/types" "golang.org/x/mod/sumdb/note" - - ct "github.com/google/certificate-transparency-go" ) const nanosPerMilli int64 = int64(time.Millisecond / time.Nanosecond) // signSCT builds an SCT for a leaf. -type signSCT func(leaf *ct.MerkleTreeLeaf) (*ct.SignedCertificateTimestamp, error) +type signSCT func(leaf *types.MerkleTreeLeaf) (*types.SignedCertificateTimestamp, error) // TODO(phboneff): create an SCTSigner object -func buildV1SCT(signer crypto.Signer, leaf *ct.MerkleTreeLeaf) (*ct.SignedCertificateTimestamp, error) { +func buildV1SCT(signer crypto.Signer, leaf *types.MerkleTreeLeaf) (*types.SignedCertificateTimestamp, error) { // Serialize SCT signature input to get the bytes that need to be signed - sctInput := ct.SignedCertificateTimestamp{ - SCTVersion: ct.V1, + sctInput := types.SignedCertificateTimestamp{ + SCTVersion: types.V1, Timestamp: leaf.TimestampedEntry.Timestamp, Extensions: leaf.TimestampedEntry.Extensions, } - data, err := ct.SerializeSCTSignatureInput(sctInput, ct.LogEntry{Leaf: *leaf}) + data, err := types.SerializeSCTSignatureInput(sctInput, types.LogEntry{Leaf: *leaf}) if err != nil { return nil, fmt.Errorf("failed to serialize SCT data: %v", err) } @@ -54,7 +53,7 @@ func buildV1SCT(signer crypto.Signer, leaf *ct.MerkleTreeLeaf) (*ct.SignedCertif return nil, fmt.Errorf("failed to sign SCT data: %v", err) } - digitallySigned := ct.DigitallySigned{ + digitallySigned := types.DigitallySigned{ Algorithm: tls.SignatureAndHashAlgorithm{ Hash: tls.SHA256, Signature: tls.SignatureAlgorithmFromPubKey(signer.Public()), @@ -67,9 +66,9 @@ func buildV1SCT(signer crypto.Signer, leaf *ct.MerkleTreeLeaf) (*ct.SignedCertif return nil, fmt.Errorf("failed to get logID for signing: %v", err) } - return &ct.SignedCertificateTimestamp{ - SCTVersion: ct.V1, - LogID: ct.LogID{KeyID: logID}, + return &types.SignedCertificateTimestamp{ + SCTVersion: types.V1, + LogID: types.LogID{KeyID: logID}, Timestamp: sctInput.Timestamp, Extensions: sctInput.Extensions, Signature: digitallySigned, @@ -78,20 +77,20 @@ func buildV1SCT(signer crypto.Signer, leaf *ct.MerkleTreeLeaf) (*ct.SignedCertif type rfc6962NoteSignature struct { timestamp uint64 - signature ct.DigitallySigned + signature types.DigitallySigned } // buildCp builds a https://c2sp.org/static-ct-api checkpoint. // TODO(phboneff): add tests func buildCp(signer crypto.Signer, size uint64, timeMilli uint64, hash []byte) ([]byte, error) { - sth := ct.SignedTreeHead{ - Version: ct.V1, + sth := types.SignedTreeHead{ + Version: types.V1, TreeSize: size, Timestamp: timeMilli, } copy(sth.SHA256RootHash[:], hash) - sthBytes, err := ct.SerializeSTHSignatureInput(sth) + sthBytes, err := types.SerializeSTHSignatureInput(sth) if err != nil { return nil, fmt.Errorf("ct.SerializeSTHSignatureInput(): %v", err) } @@ -104,7 +103,7 @@ func buildCp(signer crypto.Signer, size uint64, timeMilli uint64, hash []byte) ( rfc6962Note := rfc6962NoteSignature{ timestamp: sth.Timestamp, - signature: ct.DigitallySigned{ + signature: types.DigitallySigned{ Algorithm: tls.SignatureAndHashAlgorithm{ Hash: tls.SHA256, Signature: tls.SignatureAlgorithmFromPubKey(signer.Public()), diff --git a/internal/scti/signatures_test.go b/internal/scti/signatures_test.go index 3c4d61d6..f9a2c5c3 100644 --- a/internal/scti/signatures_test.go +++ b/internal/scti/signatures_test.go @@ -27,8 +27,7 @@ import ( "github.com/google/certificate-transparency-go/x509util" "github.com/kylelemons/godebug/pretty" "github.com/transparency-dev/static-ct/internal/testdata" - - ct "github.com/google/certificate-transparency-go" + "github.com/transparency-dev/static-ct/internal/types" ) var ( @@ -49,7 +48,7 @@ func TestBuildV1MerkleTreeLeafForCert(t *testing.T) { t.Fatalf("could not create signer: %v", err) } - leaf, err := ct.MerkleTreeLeafFromChain([]*x509.Certificate{cert}, ct.X509LogEntryType, fixedTimeMillis) + leaf, err := types.MerkleTreeLeafFromChain([]*x509.Certificate{cert}, types.X509LogEntryType, fixedTimeMillis) if err != nil { t.Fatalf("buildV1MerkleTreeLeafForCert()=nil,%v; want _,nil", err) } @@ -58,12 +57,12 @@ func TestBuildV1MerkleTreeLeafForCert(t *testing.T) { t.Fatalf("buildV1SCT()=nil,%v; want _,nil", err) } - expected := ct.SignedCertificateTimestamp{ + expected := types.SignedCertificateTimestamp{ SCTVersion: 0, - LogID: ct.LogID{KeyID: demoLogID}, + LogID: types.LogID{KeyID: demoLogID}, Timestamp: fixedTimeMillis, - Extensions: ct.CTExtensions{}, - Signature: ct.DigitallySigned{ + Extensions: types.CTExtensions{}, + Signature: types.DigitallySigned{ Algorithm: tls.SignatureAndHashAlgorithm{ Hash: tls.SHA256, Signature: tls.ECDSA}, @@ -76,13 +75,13 @@ func TestBuildV1MerkleTreeLeafForCert(t *testing.T) { } // Additional checks that the MerkleTreeLeaf we built is correct - if got, want := leaf.Version, ct.V1; got != want { + if got, want := leaf.Version, types.V1; got != want { t.Fatalf("Got a %v leaf, expected a %v leaf", got, want) } - if got, want := leaf.LeafType, ct.TimestampedEntryLeafType; got != want { + if got, want := leaf.LeafType, types.TimestampedEntryLeafType; got != want { t.Fatalf("Got leaf type %v, expected %v", got, want) } - if got, want := leaf.TimestampedEntry.EntryType, ct.X509LogEntryType; got != want { + if got, want := leaf.TimestampedEntry.EntryType, types.X509LogEntryType; got != want { t.Fatalf("Got entry type %v, expected %v", got, want) } if got, want := leaf.TimestampedEntry.Timestamp, got.Timestamp; got != want { @@ -105,7 +104,7 @@ func TestSignV1SCTForPrecertificate(t *testing.T) { } // Use the same cert as the issuer for convenience. - leaf, err := ct.MerkleTreeLeafFromChain([]*x509.Certificate{cert, cert}, ct.PrecertLogEntryType, fixedTimeMillis) + leaf, err := types.MerkleTreeLeafFromChain([]*x509.Certificate{cert, cert}, types.PrecertLogEntryType, fixedTimeMillis) if err != nil { t.Fatalf("buildV1MerkleTreeLeafForCert()=nil,%v; want _,nil", err) } @@ -114,12 +113,12 @@ func TestSignV1SCTForPrecertificate(t *testing.T) { t.Fatalf("buildV1SCT()=nil,%v; want _,nil", err) } - expected := ct.SignedCertificateTimestamp{ + expected := types.SignedCertificateTimestamp{ SCTVersion: 0, - LogID: ct.LogID{KeyID: demoLogID}, + LogID: types.LogID{KeyID: demoLogID}, Timestamp: fixedTimeMillis, - Extensions: ct.CTExtensions{}, - Signature: ct.DigitallySigned{ + Extensions: types.CTExtensions{}, + Signature: types.DigitallySigned{ Algorithm: tls.SignatureAndHashAlgorithm{ Hash: tls.SHA256, Signature: tls.ECDSA}, @@ -130,13 +129,13 @@ func TestSignV1SCTForPrecertificate(t *testing.T) { } // Additional checks that the MerkleTreeLeaf we built is correct - if got, want := leaf.Version, ct.V1; got != want { + if got, want := leaf.Version, types.V1; got != want { t.Fatalf("Got a %v leaf, expected a %v leaf", got, want) } - if got, want := leaf.LeafType, ct.TimestampedEntryLeafType; got != want { + if got, want := leaf.LeafType, types.TimestampedEntryLeafType; got != want { t.Fatalf("Got leaf type %v, expected %v", got, want) } - if got, want := leaf.TimestampedEntry.EntryType, ct.PrecertLogEntryType; got != want { + if got, want := leaf.TimestampedEntry.EntryType, types.PrecertLogEntryType; got != want { t.Fatalf("Got entry type %v, expected %v", got, want) } if got, want := got.Timestamp, leaf.TimestampedEntry.Timestamp; got != want { From 822f581afb59b20052aee14ad5b85c3819c63d4a Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Wed, 5 Feb 2025 12:15:00 +0000 Subject: [PATCH 3/4] move SerializeSTHSignatureInput and serializeSCTSignatureInput from c-t-fo # Conflicts: # internal/scti/signatures_test.go --- internal/scti/signatures.go | 55 ++++++++- internal/scti/signatures_test.go | 203 +++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 2 deletions(-) diff --git a/internal/scti/signatures.go b/internal/scti/signatures.go index 0c6e0100..a3518a77 100644 --- a/internal/scti/signatures.go +++ b/internal/scti/signatures.go @@ -34,6 +34,35 @@ const nanosPerMilli int64 = int64(time.Millisecond / time.Nanosecond) // signSCT builds an SCT for a leaf. type signSCT func(leaf *types.MerkleTreeLeaf) (*types.SignedCertificateTimestamp, error) +// serializeSCTSignatureInput serializes the passed in sct and log entry into +// the correct format for signing. +func serializeSCTSignatureInput(sct types.SignedCertificateTimestamp, entry types.LogEntry) ([]byte, error) { + switch sct.SCTVersion { + case types.V1: + input := types.CertificateTimestamp{ + SCTVersion: sct.SCTVersion, + SignatureType: types.CertificateTimestampSignatureType, + Timestamp: sct.Timestamp, + EntryType: entry.Leaf.TimestampedEntry.EntryType, + Extensions: sct.Extensions, + } + switch entry.Leaf.TimestampedEntry.EntryType { + case types.X509LogEntryType: + input.X509Entry = entry.Leaf.TimestampedEntry.X509Entry + case types.PrecertLogEntryType: + input.PrecertEntry = &types.PreCert{ + IssuerKeyHash: entry.Leaf.TimestampedEntry.PrecertEntry.IssuerKeyHash, + TBSCertificate: entry.Leaf.TimestampedEntry.PrecertEntry.TBSCertificate, + } + default: + return nil, fmt.Errorf("unsupported entry type %s", entry.Leaf.TimestampedEntry.EntryType) + } + return tls.Marshal(input) + default: + return nil, fmt.Errorf("unknown SCT version %d", sct.SCTVersion) + } +} + // TODO(phboneff): create an SCTSigner object func buildV1SCT(signer crypto.Signer, leaf *types.MerkleTreeLeaf) (*types.SignedCertificateTimestamp, error) { // Serialize SCT signature input to get the bytes that need to be signed @@ -42,7 +71,7 @@ func buildV1SCT(signer crypto.Signer, leaf *types.MerkleTreeLeaf) (*types.Signed Timestamp: leaf.TimestampedEntry.Timestamp, Extensions: leaf.TimestampedEntry.Extensions, } - data, err := types.SerializeSCTSignatureInput(sctInput, types.LogEntry{Leaf: *leaf}) + data, err := serializeSCTSignatureInput(sctInput, types.LogEntry{Leaf: *leaf}) if err != nil { return nil, fmt.Errorf("failed to serialize SCT data: %v", err) } @@ -80,6 +109,28 @@ type rfc6962NoteSignature struct { signature types.DigitallySigned } +// serializeSTHSignatureInput serializes the passed in STH into the correct +// format for signing. +func serializeSTHSignatureInput(sth types.SignedTreeHead) ([]byte, error) { + switch sth.Version { + case types.V1: + if len(sth.SHA256RootHash) != crypto.SHA256.Size() { + return nil, fmt.Errorf("invalid TreeHash length, got %d expected %d", len(sth.SHA256RootHash), crypto.SHA256.Size()) + } + + input := types.TreeHeadSignature{ + Version: sth.Version, + SignatureType: types.TreeHashSignatureType, + Timestamp: sth.Timestamp, + TreeSize: sth.TreeSize, + SHA256RootHash: sth.SHA256RootHash, + } + return tls.Marshal(input) + default: + return nil, fmt.Errorf("unsupported STH version %d", sth.Version) + } +} + // buildCp builds a https://c2sp.org/static-ct-api checkpoint. // TODO(phboneff): add tests func buildCp(signer crypto.Signer, size uint64, timeMilli uint64, hash []byte) ([]byte, error) { @@ -90,7 +141,7 @@ func buildCp(signer crypto.Signer, size uint64, timeMilli uint64, hash []byte) ( } copy(sth.SHA256RootHash[:], hash) - sthBytes, err := types.SerializeSTHSignatureInput(sth) + sthBytes, err := serializeSTHSignatureInput(sth) if err != nil { return nil, fmt.Errorf("ct.SerializeSTHSignatureInput(): %v", err) } diff --git a/internal/scti/signatures_test.go b/internal/scti/signatures_test.go index f9a2c5c3..9720aa70 100644 --- a/internal/scti/signatures_test.go +++ b/internal/scti/signatures_test.go @@ -18,6 +18,7 @@ import ( "bytes" "crypto" "crypto/sha256" + "encoding/hex" "encoding/pem" "testing" "time" @@ -37,6 +38,208 @@ var ( fakeSignature = []byte("signed") ) +const ( + defaultSCTLogIDString string = "iamapublickeyshatwofivesixdigest" + defaultSCTTimestamp uint64 = 1234 + defaultSCTSignatureString string = "\x04\x03\x00\x09signature" + defaultCertifictateString string = "certificate" + defaultPrecertIssuerHashString string = "iamapublickeyshatwofivesixdigest" + defaultPrecertTBSString string = "tbs" + + defaultCertificateSCTSignatureInputHexString string = + // version, 1 byte + "00" + + // signature type, 1 byte + "00" + + // timestamp, 8 bytes + "00000000000004d2" + + // entry type, 2 bytes + "0000" + + // leaf certificate length, 3 bytes + "00000b" + + // leaf certificate, 11 bytes + "6365727469666963617465" + + // extensions length, 2 bytes + "0000" + + // extensions, 0 bytes + "" + + defaultPrecertSCTSignatureInputHexString string = + // version, 1 byte + "00" + + // signature type, 1 byte + "00" + + // timestamp, 8 bytes + "00000000000004d2" + + // entry type, 2 bytes + "0001" + + // issuer key hash, 32 bytes + "69616d617075626c69636b657973686174776f66697665736978646967657374" + + // tbs certificate length, 3 bytes + "000003" + + // tbs certificate, 3 bytes + "746273" + + // extensions length, 2 bytes + "0000" + + // extensions, 0 bytes + "" + + defaultSTHSignedHexString string = + // version, 1 byte + "00" + + // signature type, 1 byte + "01" + + // timestamp, 8 bytes + "0000000000000929" + + // tree size, 8 bytes + "0000000000000006" + + // root hash, 32 bytes + "696d757374626565786163746c7974686972747974776f62797465736c6f6e67" +) + +func defaultSCTLogID() types.LogID { + var id types.LogID + copy(id.KeyID[:], defaultSCTLogIDString) + return id +} + +func defaultSCTSignature() types.DigitallySigned { + var ds types.DigitallySigned + if _, err := tls.Unmarshal([]byte(defaultSCTSignatureString), &ds); err != nil { + panic(err) + } + return ds +} + +func defaultSCT() types.SignedCertificateTimestamp { + return types.SignedCertificateTimestamp{ + SCTVersion: types.V1, + LogID: defaultSCTLogID(), + Timestamp: defaultSCTTimestamp, + Extensions: []byte{}, + Signature: defaultSCTSignature()} +} + +func defaultCertificate() []byte { + return []byte(defaultCertifictateString) +} + +func defaultCertificateSCTSignatureInput(t *testing.T) []byte { + t.Helper() + r, err := hex.DecodeString(defaultCertificateSCTSignatureInputHexString) + if err != nil { + t.Fatalf("failed to decode defaultCertificateSCTSignatureInputHexString: %v", err) + } + return r +} + +func defaultCertificateLogEntry() types.LogEntry { + return types.LogEntry{ + Index: 1, + Leaf: types.MerkleTreeLeaf{ + Version: types.V1, + LeafType: types.TimestampedEntryLeafType, + TimestampedEntry: &types.TimestampedEntry{ + Timestamp: defaultSCTTimestamp, + EntryType: types.X509LogEntryType, + X509Entry: &types.ASN1Cert{Data: defaultCertificate()}, + }, + }, + } +} + +func defaultPrecertSCTSignatureInput(t *testing.T) []byte { + t.Helper() + r, err := hex.DecodeString(defaultPrecertSCTSignatureInputHexString) + if err != nil { + t.Fatalf("failed to decode defaultPrecertSCTSignatureInputHexString: %v", err) + } + return r +} + +func defaultPrecertTBS() []byte { + return []byte(defaultPrecertTBSString) +} + +func defaultPrecertIssuerHash() [32]byte { + var b [32]byte + copy(b[:], []byte(defaultPrecertIssuerHashString)) + return b +} + +func defaultPrecertLogEntry() types.LogEntry { + return types.LogEntry{ + Index: 1, + Leaf: types.MerkleTreeLeaf{ + Version: types.V1, + LeafType: types.TimestampedEntryLeafType, + TimestampedEntry: &types.TimestampedEntry{ + Timestamp: defaultSCTTimestamp, + EntryType: types.PrecertLogEntryType, + PrecertEntry: &types.PreCert{ + IssuerKeyHash: defaultPrecertIssuerHash(), + TBSCertificate: defaultPrecertTBS(), + }, + }, + }, + } +} + +func defaultSTH() types.SignedTreeHead { + var root types.SHA256Hash + copy(root[:], "imustbeexactlythirtytwobyteslong") + return types.SignedTreeHead{ + TreeSize: 6, + Timestamp: 2345, + SHA256RootHash: root, + TreeHeadSignature: types.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.ECDSA}, + Signature: []byte("tree_signature"), + }, + } +} + +func mustDehex(t *testing.T, h string) []byte { + t.Helper() + r, err := hex.DecodeString(h) + if err != nil { + t.Fatalf("Failed to decode hex string (%s): %v", h, err) + } + return r +} + +func TestSerializeV1SCTSignatureInputForCertificateKAT(t *testing.T) { + serialized, err := serializeSCTSignatureInput(defaultSCT(), defaultCertificateLogEntry()) + if err != nil { + t.Fatalf("Failed to serialize SCT for signing: %v", err) + } + if !bytes.Equal(serialized, defaultCertificateSCTSignatureInput(t)) { + t.Fatalf("Serialized certificate signature input doesn't match expected answer:\n%v\n%v", serialized, defaultCertificateSCTSignatureInput(t)) + } +} + +func TestSerializeV1SCTSignatureInputForPrecertKAT(t *testing.T) { + serialized, err := serializeSCTSignatureInput(defaultSCT(), defaultPrecertLogEntry()) + if err != nil { + t.Fatalf("Failed to serialize SCT for signing: %v", err) + } + if !bytes.Equal(serialized, defaultPrecertSCTSignatureInput(t)) { + t.Fatalf("Serialized precertificate signature input doesn't match expected answer:\n%v\n%v", serialized, defaultPrecertSCTSignatureInput(t)) + } +} + +func TestSerializeV1STHSignatureKAT(t *testing.T) { + b, err := serializeSTHSignatureInput(defaultSTH()) + if err != nil { + t.Fatalf("Failed to serialize defaultSTH: %v", err) + } + if !bytes.Equal(b, mustDehex(t, defaultSTHSignedHexString)) { + t.Fatalf("defaultSTH incorrectly serialized, expected:\n%v\ngot:\n%v", mustDehex(t, defaultSTHSignedHexString), b) + } +} + func TestBuildV1MerkleTreeLeafForCert(t *testing.T) { cert, err := x509util.CertificateFromPEM([]byte(testdata.LeafSignedByFakeIntermediateCertPEM)) if x509.IsFatal(err) { From 68363904ca709968d310a8b3de7c80c21cb5cf49 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Tue, 11 Feb 2025 10:33:39 +0000 Subject: [PATCH 4/4] copy move MerkleTreeLeafFromChain --- internal/scti/signatures.go | 55 ++++++++++++++++++++++++++++++++ internal/scti/signatures_test.go | 4 +-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/internal/scti/signatures.go b/internal/scti/signatures.go index a3518a77..b5681f32 100644 --- a/internal/scti/signatures.go +++ b/internal/scti/signatures.go @@ -131,6 +131,61 @@ func serializeSTHSignatureInput(sth types.SignedTreeHead) ([]byte, error) { } } +// MerkleTreeLeafFromChain generates a MerkleTreeLeaf from a chain and timestamp. +// TODO(phboneff): delete this function and use entryFromChain instead. +func MerkleTreeLeafFromChain(chain []*x509.Certificate, etype types.LogEntryType, timestamp uint64) (*types.MerkleTreeLeaf, error) { + leaf := types.MerkleTreeLeaf{ + Version: types.V1, + LeafType: types.TimestampedEntryLeafType, + TimestampedEntry: &types.TimestampedEntry{ + EntryType: etype, + Timestamp: timestamp, + }, + } + if etype == types.X509LogEntryType { + leaf.TimestampedEntry.X509Entry = &types.ASN1Cert{Data: chain[0].Raw} + return &leaf, nil + } + if etype != types.PrecertLogEntryType { + return nil, fmt.Errorf("unknown LogEntryType %d", etype) + } + + // 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.TimestampedEntry.EntryType = types.PrecertLogEntryType + leaf.TimestampedEntry.PrecertEntry = &types.PreCert{ + IssuerKeyHash: sha256.Sum256(issuer.RawSubjectPublicKeyInfo), + TBSCertificate: defangedTBS, + } + return &leaf, nil +} + // buildCp builds a https://c2sp.org/static-ct-api checkpoint. // TODO(phboneff): add tests func buildCp(signer crypto.Signer, size uint64, timeMilli uint64, hash []byte) ([]byte, error) { diff --git a/internal/scti/signatures_test.go b/internal/scti/signatures_test.go index 9720aa70..59abb7b9 100644 --- a/internal/scti/signatures_test.go +++ b/internal/scti/signatures_test.go @@ -251,7 +251,7 @@ func TestBuildV1MerkleTreeLeafForCert(t *testing.T) { t.Fatalf("could not create signer: %v", err) } - leaf, err := types.MerkleTreeLeafFromChain([]*x509.Certificate{cert}, types.X509LogEntryType, fixedTimeMillis) + leaf, err := MerkleTreeLeafFromChain([]*x509.Certificate{cert}, types.X509LogEntryType, fixedTimeMillis) if err != nil { t.Fatalf("buildV1MerkleTreeLeafForCert()=nil,%v; want _,nil", err) } @@ -307,7 +307,7 @@ func TestSignV1SCTForPrecertificate(t *testing.T) { } // Use the same cert as the issuer for convenience. - leaf, err := types.MerkleTreeLeafFromChain([]*x509.Certificate{cert, cert}, types.PrecertLogEntryType, fixedTimeMillis) + leaf, err := MerkleTreeLeafFromChain([]*x509.Certificate{cert, cert}, types.PrecertLogEntryType, fixedTimeMillis) if err != nil { t.Fatalf("buildV1MerkleTreeLeafForCert()=nil,%v; want _,nil", err) }