From e9832e58b95dee4a0adc93b013a4385ba466a5f4 Mon Sep 17 00:00:00 2001 From: Aaron LI Date: Thu, 16 Jan 2025 10:32:08 +0800 Subject: [PATCH] Update ctclient to support SCT extensions According to RFC 6962 (Section 3.4), the 'extensions' field defined for SCT is also included in 'MerkleTreeLeaf.TimestampedEntry'. Since the current MerkleTreeLeaf functions (e.g., CreateX509MerkleTreeLeaf()) don't support to specify this field, we need to manually fill it in order to calculate the correct leaf hash. As suggested, we chose not to change the signatures of those functions to avoid breaking external dependencies. * Update LogClient.VerifySCTSignature() to fill the 'extensions' field from 'sct.Extensions'. * Update ctclient's get-entries command to print the extensions if non-empty, which can be parsed by the 'get-inclusion-proof' command similar to the 'timestamp' field for convenience. * Update ctclient's upload command to calculate the leaf hash with the 'sct.Extensions'; also print the SCT extensions as hex string in the end, which may be used by the 'get-inclusion-proof' command. * Update ctclient's get-inclusion-proof command to add the '--extensions' flag to specify the SCT extensions returned by 'upload' command. Signed-off-by: Aaron LI --- client/ctclient/cmd/get_entries.go | 3 ++ client/ctclient/cmd/get_inclusion_proof.go | 43 +++++++++++++++++----- client/ctclient/cmd/upload.go | 8 +++- client/logclient.go | 1 + 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/client/ctclient/cmd/get_entries.go b/client/ctclient/cmd/get_entries.go index 33e583473b..69b2c4942b 100644 --- a/client/ctclient/cmd/get_entries.go +++ b/client/ctclient/cmd/get_entries.go @@ -80,6 +80,9 @@ func showRawLogEntry(rle *ct.RawLogEntry) { ts := rle.Leaf.TimestampedEntry when := ct.TimestampToTime(ts.Timestamp) fmt.Printf("Index=%d Timestamp=%d (%v) ", rle.Index, ts.Timestamp, when) + if len(ts.Extensions) > 0 { + fmt.Printf("Extensions=%x ", ts.Extensions) + } switch ts.EntryType { case ct.X509LogEntryType: diff --git a/client/ctclient/cmd/get_inclusion_proof.go b/client/ctclient/cmd/get_inclusion_proof.go index 1d2881e6bc..03ca2d3a30 100644 --- a/client/ctclient/cmd/get_inclusion_proof.go +++ b/client/ctclient/cmd/get_inclusion_proof.go @@ -17,6 +17,7 @@ package cmd import ( "context" "crypto/sha256" + "encoding/hex" "encoding/pem" "fmt" "os" @@ -35,15 +36,16 @@ import ( ) var ( - leafHash string - certChain string - timestamp int64 - treeSize uint64 + leafHash string + certChain string + timestamp int64 + treeSize uint64 + extensions string ) func init() { cmd := cobra.Command{ - Use: fmt.Sprintf("get-inclusion-proof %s {--leaf_hash=hash | --cert_chain=file} [--timestamp=ts] [--size=N]", connectionFlags), + Use: fmt.Sprintf("get-inclusion-proof %s {--leaf_hash=hash | --cert_chain=file} [--timestamp=ts] [--size=N] [--extensions=exts]", connectionFlags), Aliases: []string{"getinclusionproof", "inclusion-proof", "inclusion"}, Short: "Fetch and verify the inclusion proof for an entry", Args: cobra.MaximumNArgs(0), @@ -54,6 +56,7 @@ func init() { cmd.Flags().StringVar(&leafHash, "leaf_hash", "", "Leaf hash to retrieve (as hex string or base64)") cmd.Flags().StringVar(&certChain, "cert_chain", "", "Name of file containing certificate chain as concatenated PEM files") cmd.Flags().Int64Var(×tamp, "timestamp", 0, "Timestamp to use for inclusion checking") + cmd.Flags().StringVar(&extensions, "extensions", "", "CT extensions in SCT (as hex string)") cmd.Flags().Uint64Var(&treeSize, "size", 0, "Tree size to query at") rootCmd.AddCommand(&cmd) } @@ -61,6 +64,14 @@ func init() { // runGetInclusionProof runs the get-inclusion-proof command. func runGetInclusionProof(ctx context.Context) { logClient := connect(ctx) + var ctexts ct.CTExtensions + if len(extensions) > 0 { + exts, err := hex.DecodeString(extensions) + if err != nil { + klog.Exitf("Invalid --extensions supplied: %v", err) + } + ctexts = ct.CTExtensions(exts) + } var hash []byte if len(leafHash) > 0 { var err error @@ -70,13 +81,16 @@ func runGetInclusionProof(ctx context.Context) { } } else if len(certChain) > 0 { // Build a leaf hash from the chain and a timestamp. - chain, entryTimestamp := chainFromFile(certChain) + chain, entryTimestamp, entryCTExts := chainFromFile(certChain) if timestamp != 0 { entryTimestamp = timestamp // Use user-specified timestamp. } if entryTimestamp == 0 { klog.Exit("No timestamp available to accompany certificate") } + if len(entryCTExts) == 0 { + entryCTExts = ctexts + } var leafEntry *ct.MerkleTreeLeaf cert, err := x509.ParseCertificate(chain[0].Data) @@ -92,6 +106,7 @@ func runGetInclusionProof(ctx context.Context) { leafEntry = ct.CreateX509MerkleTreeLeaf(chain[0], uint64(entryTimestamp)) } + leafEntry.TimestampedEntry.Extensions = entryCTExts leafHash, err := ct.LeafHashForLeaf(leafEntry) if err != nil { klog.Exitf("Failed to create hash of leaf: %v", err) @@ -139,7 +154,7 @@ func getInclusionProofForHash(ctx context.Context, logClient client.CheckLogClie } } -func chainFromFile(filename string) ([]ct.ASN1Cert, int64) { +func chainFromFile(filename string) ([]ct.ASN1Cert, int64, []byte) { contents, err := os.ReadFile(filename) if err != nil { klog.Exitf("Failed to read certificate file: %v", err) @@ -160,9 +175,12 @@ func chainFromFile(filename string) ([]ct.ASN1Cert, int64) { klog.Exitf("No certificates found in %s", certChain) } - // Also look for something like a text timestamp for convenience. + // Also look for something like a text timestamp and a hex extensions + // for convenience. var timestamp int64 + var extensions []byte tsRE := regexp.MustCompile(`Timestamp[:=](\d+)`) + extsRE := regexp.MustCompile(`Extensions[:=]([a-fA-F0-9]+)`) for _, line := range strings.Split(string(contents), "\n") { x := tsRE.FindStringSubmatch(line) if len(x) > 1 { @@ -171,6 +189,13 @@ func chainFromFile(filename string) ([]ct.ASN1Cert, int64) { break } } + x = extsRE.FindStringSubmatch(line) + if len(x) > 1 { + extensions, err = hex.DecodeString(x[1]) + if err != nil { + break + } + } } - return chain, timestamp + return chain, timestamp, extensions } diff --git a/client/ctclient/cmd/upload.go b/client/ctclient/cmd/upload.go index 95adda9145..743d61863a 100644 --- a/client/ctclient/cmd/upload.go +++ b/client/ctclient/cmd/upload.go @@ -50,7 +50,7 @@ func runUpload(ctx context.Context) { if certChain == "" { klog.Exitf("No certificate chain file specified with -cert_chain") } - chain, _ := chainFromFile(certChain) + chain, _, _ := chainFromFile(certChain) // Examine the leaf to see if it looks like a pre-certificate. isPrecert := false @@ -74,6 +74,7 @@ func runUpload(ctx context.Context) { } // Calculate the leaf hash. leafEntry := ct.CreateX509MerkleTreeLeaf(chain[0], sct.Timestamp) + leafEntry.TimestampedEntry.Extensions = sct.Extensions leafHash, err := ct.LeafHashForLeaf(leafEntry) if err != nil { klog.Exitf("Failed to create hash of leaf: %v", err) @@ -84,6 +85,11 @@ func runUpload(ctx context.Context) { fmt.Printf("Uploaded chain of %d certs to %v log at %v, timestamp: %d (%v)\n", len(chain), sct.SCTVersion, logClient.BaseURI(), sct.Timestamp, when) fmt.Printf("LogID: %x\n", sct.LogID.KeyID[:]) fmt.Printf("LeafHash: %x\n", leafHash) + if len(sct.Extensions) > 0 { + fmt.Printf("Extensions: %x\n", sct.Extensions) + } else { + fmt.Printf("Extensions: (nil)\n") + } fmt.Printf("Signature: %v\n", signatureToString(&sct.Signature)) age := time.Since(when) diff --git a/client/logclient.go b/client/logclient.go index 7842c8e288..0e90c1077f 100644 --- a/client/logclient.go +++ b/client/logclient.go @@ -160,6 +160,7 @@ func (c *LogClient) VerifySCTSignature(sct ct.SignedCertificateTimestamp, ctype if err != nil { return fmt.Errorf("failed to build MerkleTreeLeaf: %v", err) } + leaf.TimestampedEntry.Extensions = sct.Extensions entry := ct.LogEntry{Leaf: *leaf} return c.Verifier.VerifySCTSignature(sct, entry) }