Skip to content

Commit

Permalink
Update ctclient to support SCT extensions
Browse files Browse the repository at this point in the history
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 <aaronly.me@gmail.com>
  • Loading branch information
liweitianux committed Jan 24, 2025
1 parent 8a203c2 commit e9832e5
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 10 deletions.
3 changes: 3 additions & 0 deletions client/ctclient/cmd/get_entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 34 additions & 9 deletions client/ctclient/cmd/get_inclusion_proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package cmd
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/pem"
"fmt"
"os"
Expand All @@ -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),
Expand All @@ -54,13 +56,22 @@ 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(&timestamp, "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)
}

// 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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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
}
8 changes: 7 additions & 1 deletion client/ctclient/cmd/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions client/logclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit e9832e5

Please sign in to comment.