From e52ed6d3c814fcdfa93d1cdef4eaeb46f24b96a0 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Thu, 11 Sep 2025 13:43:13 +0000 Subject: [PATCH 1/5] Draft of ctlog client. --- cmd/client/main.go | 148 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 cmd/client/main.go diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 00000000..085af9f5 --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,148 @@ +package main + +import ( + "context" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "flag" + "fmt" + "net/http" + "net/url" + "os" + "strconv" + "time" + + "github.com/transparency-dev/formats/log" + tdnote "github.com/transparency-dev/formats/note" + "github.com/transparency-dev/tessera/api/layout" + "github.com/transparency-dev/tesseract/internal/client" + "github.com/transparency-dev/tesseract/internal/types/staticct" + "golang.org/x/mod/sumdb/note" + "k8s.io/klog/v2" +) + +var ( + monitoringURL = flag.String("monitoring_url", "", "Base tlog-tiles URL") + leafIndex = flag.String("leaf_index", "", "The index of the leaf to fetch") + origin = flag.String("origin", os.Getenv("CT_LOG_ORIGIN"), "Origin of the log, for checkpoints and the monitoring prefix. This is defaulted to the environment variable CT_LOG_ORIGIN") + logPubKey = flag.String("log_public_key", os.Getenv("CT_LOG_PUBLIC_KEY"), "Public key for the log. This is defaulted to the environment variable CT_LOG_PUBLIC_KEY") +) + +func main() { + klog.InitFlags(nil) + flag.Parse() + + if *monitoringURL == "" { + klog.Exitf("--monitoring_url must be set") + } + if *leafIndex == "" { + klog.Exitf("--leaf_index must be set") + } + li, err := strconv.ParseUint(*leafIndex, 10, 64) + if err != nil { + klog.Exitf("Invalid --leaf_index: %v", err) + } + + logURL, err := url.Parse(*monitoringURL) + if err != nil { + klog.Exitf("Invalid --monitoring_url %q: %v", *monitoringURL, err) + } + hc := &http.Client{ + Timeout: 30 * time.Second, + } + fetcher, err := client.NewHTTPFetcher(logURL, hc) + if err != nil { + klog.Exitf("Failed to create HTTP fetcher: %v", err) + } + + ctx := context.Background() + + cpRaw, err := fetcher.ReadCheckpoint(ctx) + if err != nil { + klog.Exitf("Failed to fetch checkpoint: %v", err) + } + logSigV, err := logSigVerifier(*origin, *logPubKey) + if err != nil { + klog.Exitf("Failed to create verifier: %v", err) + } + cp, _, _, err := log.ParseCheckpoint(cpRaw, *origin, logSigV) + if err != nil { + klog.Exitf("Failed to parse checkpoint: %v", err) + } + + if li >= cp.Size { + klog.Exitf("Leaf index %d is out of range for log size %d", li, cp.Size) + } + + bundleIndex := li / uint64(layout.EntryBundleWidth) + indexInBundle := li % uint64(layout.EntryBundleWidth) + + bundle, err := client.GetEntryBundle(ctx, fetcher.ReadEntryBundle, bundleIndex, cp.Size) + if err != nil { + klog.Exitf("Failed to get entry bundle: %v", err) + } + + if int(indexInBundle) >= len(bundle.Entries) { + klog.Exitf("Index %d is out of range for bundle of size %d", indexInBundle, len(bundle.Entries)) + } + entryData := bundle.Entries[indexInBundle] + + var entry staticct.Entry + if err := entry.UnmarshalText(entryData); err != nil { + klog.Exitf("Failed to unmarshal entry: %v", err) + } + + certBytes := entry.Certificate + if entry.IsPrecert { + // For precertificates, the `Certificate` field holds the TBSCertificate. + // We need to wrap this in a `Certificate` structure to be able to parse it. + // This is a bit of a hack, but it's what the `x509` package expects. + cert, err := x509.ParseCertificate(entry.Precertificate) + if err != nil { + klog.Exitf("Failed to parse precertificate: %v", err) + } + certBytes = cert.Raw + } + + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + } + + if err := pem.Encode(os.Stdout, pemBlock); err != nil { + klog.Exitf("Failed to encode PEM: %v", err) + } +} + +// logSigVerifier creates a note.Verifier for the Static CT API log by taking +// an origin string and a base64-encoded public key. +func logSigVerifier(origin, b64PubKey string) (note.Verifier, error) { + if origin == "" { + return nil, errors.New("origin cannot be empty") + } + if b64PubKey == "" { + return nil, errors.New("log public key cannot be empty") + } + + derBytes, err := base64.StdEncoding.DecodeString(b64PubKey) + if err != nil { + return nil, fmt.Errorf("error decoding public key: %s", err) + } + pub, err := x509.ParsePKIXPublicKey(derBytes) + if err != nil { + return nil, fmt.Errorf("error parsing public key: %v", err) + } + + verifierKey, err := tdnote.RFC6962VerifierString(origin, pub) + if err != nil { + return nil, fmt.Errorf("error creating RFC6962 verifier string: %v", err) + } + logSigV, err := tdnote.NewVerifier(verifierKey) + if err != nil { + return nil, fmt.Errorf("error creating verifier: %v", err) + } + + return logSigV, nil +} From 569ccf63871912730c5a5a5ed292ba8dec05cfc7 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Thu, 11 Sep 2025 18:41:08 +0000 Subject: [PATCH 2/5] verify that leaves have been built properly --- cmd/client/main.go | 130 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 115 insertions(+), 15 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index 085af9f5..a6f75f03 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -1,9 +1,11 @@ package main import ( + "bytes" "context" "crypto/x509" "encoding/base64" + "encoding/hex" "encoding/pem" "errors" "flag" @@ -16,30 +18,41 @@ import ( "github.com/transparency-dev/formats/log" tdnote "github.com/transparency-dev/formats/note" + "github.com/transparency-dev/merkle/proof" + "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api/layout" + "github.com/transparency-dev/tessera/ctonly" "github.com/transparency-dev/tesseract/internal/client" "github.com/transparency-dev/tesseract/internal/types/staticct" + "github.com/transparency-dev/tesseract/internal/x509util" "golang.org/x/mod/sumdb/note" "k8s.io/klog/v2" ) var ( - monitoringURL = flag.String("monitoring_url", "", "Base tlog-tiles URL") - leafIndex = flag.String("leaf_index", "", "The index of the leaf to fetch") - origin = flag.String("origin", os.Getenv("CT_LOG_ORIGIN"), "Origin of the log, for checkpoints and the monitoring prefix. This is defaulted to the environment variable CT_LOG_ORIGIN") - logPubKey = flag.String("log_public_key", os.Getenv("CT_LOG_PUBLIC_KEY"), "Public key for the log. This is defaulted to the environment variable CT_LOG_PUBLIC_KEY") + monitoringURL = flag.String("monitoring_url", "", "Log monitoring URL.") + leafIndex = flag.String("leaf_index", "", "The index of the leaf to fetch.") + origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.") + logPubKey = flag.String("log_public_key", "", "Public key for the log, base64 encoded.") +) + +var ( + hasher = rfc6962.DefaultHasher ) func main() { + // Verify Flags klog.InitFlags(nil) flag.Parse() if *monitoringURL == "" { klog.Exitf("--monitoring_url must be set") } + if *leafIndex == "" { klog.Exitf("--leaf_index must be set") } + li, err := strconv.ParseUint(*leafIndex, 10, 64) if err != nil { klog.Exitf("Invalid --leaf_index: %v", err) @@ -49,6 +62,8 @@ func main() { if err != nil { klog.Exitf("Invalid --monitoring_url %q: %v", *monitoringURL, err) } + + // Create client hc := &http.Client{ Timeout: 30 * time.Second, } @@ -56,9 +71,9 @@ func main() { if err != nil { klog.Exitf("Failed to create HTTP fetcher: %v", err) } - ctx := context.Background() + // Read Checkpoint cpRaw, err := fetcher.ReadCheckpoint(ctx) if err != nil { klog.Exitf("Failed to fetch checkpoint: %v", err) @@ -71,11 +86,11 @@ func main() { if err != nil { klog.Exitf("Failed to parse checkpoint: %v", err) } - if li >= cp.Size { klog.Exitf("Leaf index %d is out of range for log size %d", li, cp.Size) } + // Fetch entry bundleIndex := li / uint64(layout.EntryBundleWidth) indexInBundle := li % uint64(layout.EntryBundleWidth) @@ -94,21 +109,106 @@ func main() { klog.Exitf("Failed to unmarshal entry: %v", err) } - certBytes := entry.Certificate - if entry.IsPrecert { - // For precertificates, the `Certificate` field holds the TBSCertificate. - // We need to wrap this in a `Certificate` structure to be able to parse it. - // This is a bit of a hack, but it's what the `x509` package expects. - cert, err := x509.ParseCertificate(entry.Precertificate) + // Check that the entry has been built properly + e := ctonly.Entry{ + Timestamp: entry.Timestamp, + IsPrecert: entry.IsPrecert, + Certificate: entry.Certificate, + Precertificate: entry.Precertificate, + IssuerKeyHash: entry.IssuerKeyHash, + FingerprintsChain: entry.FingerprintsChain, + } + var chain []*x509.Certificate + if e.IsPrecert { + cert, err := x509.ParseCertificate(e.Precertificate) if err != nil { klog.Exitf("Failed to parse precertificate: %v", err) } - certBytes = cert.Raw + chain = append(chain, cert) + } else { + cert, err := x509.ParseCertificate(e.Certificate) + if err != nil { + klog.Exitf("Failed to parse precertificate: %v", err) + } + chain = append(chain, cert) + } + for i, hash := range entry.FingerprintsChain { + iss, err := fetcher.ReadIssuer(ctx, hash[:]) + if err != nil { + klog.Exitf("Failed to fetch issuer number %d: %v", i, err) + } + cert, err := x509.ParseCertificate(iss) + if err != nil { + klog.Exitf("Failed ot parse issuer number %d: %v", i, err) + } + chain = append(chain, cert) + } + ee, err := x509util.EntryFromChain(chain, entry.IsPrecert, entry.Timestamp) + if err != nil { + klog.Exitf("Failed to reconstruct entry from the leaf and issuers: %v", err) + } + + var errs []error + if e.Timestamp != ee.Timestamp { + errs = append(errs, fmt.Errorf("timestamp don't match: %d, %d", e.Timestamp, ee.Timestamp)) + } + if e.IsPrecert != ee.IsPrecert { + errs = append(errs, fmt.Errorf("IsPrecert don't match: %t, %t", e.IsPrecert, ee.IsPrecert)) + } + if !bytes.Equal(e.Certificate, ee.Certificate) { + if e.IsPrecert { + errs = append(errs, fmt.Errorf("TBSCertificates don't match")) + } else { + errs = append(errs, fmt.Errorf("certificates don't match")) + } + } + if !bytes.Equal(e.Precertificate, ee.Precertificate) { + errs = append(errs, fmt.Errorf("precertificates don't match")) + } + if !bytes.Equal(e.IssuerKeyHash, ee.IssuerKeyHash) { + errs = append(errs, fmt.Errorf("IssuerKeyHashes don't match, got %q, want %q", hex.EncodeToString(e.IssuerKeyHash), hex.EncodeToString(ee.IssuerKeyHash))) + } + if len(e.FingerprintsChain) != len(ee.FingerprintsChain) { + errs = append(errs, fmt.Errorf("lengths of fingerprints chains don't match: got %d, want %d", len(e.FingerprintsChain), len(ee.FingerprintsChain))) + } else { + for i := range e.FingerprintsChain { + if !bytes.Equal(e.FingerprintsChain[i][:], ee.FingerprintsChain[i][:]) { + errs = append(errs, fmt.Errorf("fingerprints %d don't match, got %q, want %q", i, hex.EncodeToString(e.FingerprintsChain[i][:]), hex.EncodeToString(ee.FingerprintsChain[i][:]))) + } + } + } + if len(errs) > 0 { + klog.Exitf("Leaf entry not built properly: %v", errors.Join(errs...)) + } + + // TODO(phboneff): check that the chain is valid + // TODO(phboneff): if this is an end cert and it has an SCT from this very log, check that SCT + + // Build inclusion proof + proofBuilder, err := client.NewProofBuilder(ctx, log.Checkpoint{ + Origin: *origin, + Size: cp.Size, + Hash: cp.Hash}, fetcher.ReadTile) + if err != nil { + klog.Exitf("Failed to create proofBuilder: %v", err) + } + mlh := e.MerkleLeafHash(entry.LeafIndex) + ip, err := proofBuilder.InclusionProof(ctx, li) + if err != nil { + klog.Exitf("Failed to build InclusionProof %v", err) + } + if err := proof.VerifyInclusion(hasher, li, cp.Size, mlh, ip, cp.Hash); err != nil { + klog.Exitf("Failed to verify inclusion of leaf %d in tree of size %d: %v", li, cp.Size, err) } pemBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: certBytes, + Type: "CERTIFICATE", + Bytes: func() []byte { + if entry.IsPrecert { + return entry.Precertificate + } + return entry.Certificate + }(), } if err := pem.Encode(os.Stdout, pemBlock); err != nil { From c0ed15edf23868f6acbb0e8341ba1b0fb0825cec Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Fri, 19 Sep 2025 16:32:53 +0000 Subject: [PATCH 3/5] improvements --- cmd/client/main.go | 200 ++++++++++++++++++++++----------------------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index a6f75f03..d639d2cd 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -1,11 +1,9 @@ package main import ( - "bytes" "context" "crypto/x509" "encoding/base64" - "encoding/hex" "encoding/pem" "errors" "flag" @@ -16,6 +14,7 @@ import ( "strconv" "time" + "github.com/google/go-cmp/cmp" "github.com/transparency-dev/formats/log" tdnote "github.com/transparency-dev/formats/note" "github.com/transparency-dev/merkle/proof" @@ -73,19 +72,7 @@ func main() { } ctx := context.Background() - // Read Checkpoint - cpRaw, err := fetcher.ReadCheckpoint(ctx) - if err != nil { - klog.Exitf("Failed to fetch checkpoint: %v", err) - } - logSigV, err := logSigVerifier(*origin, *logPubKey) - if err != nil { - klog.Exitf("Failed to create verifier: %v", err) - } - cp, _, _, err := log.ParseCheckpoint(cpRaw, *origin, logSigV) - if err != nil { - klog.Exitf("Failed to parse checkpoint: %v", err) - } + cp, nil := readCheckpoint(ctx, fetcher) if li >= cp.Size { klog.Exitf("Leaf index %d is out of range for log size %d", li, cp.Size) } @@ -109,7 +96,76 @@ func main() { klog.Exitf("Failed to unmarshal entry: %v", err) } + if errs := verify(ctx, &entry, cp, li, fetcher); len(errs) != 0 { + klog.Exitf("Failed to verify leaf entry: %s", errors.Join(errs...)) + } + + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: func() []byte { + if entry.IsPrecert { + return entry.Precertificate + } + return entry.Certificate + }(), + } + + if err := pem.Encode(os.Stdout, pemBlock); err != nil { + klog.Exitf("Failed to encode PEM: %v", err) + } +} + +// logSigVerifier creates a note.Verifier for the Static CT API log by taking +// an origin string and a base64-encoded public key. +func logSigVerifier(origin, b64PubKey string) (note.Verifier, error) { + if origin == "" { + return nil, errors.New("origin cannot be empty") + } + if b64PubKey == "" { + return nil, errors.New("log public key cannot be empty") + } + + derBytes, err := base64.StdEncoding.DecodeString(b64PubKey) + if err != nil { + return nil, fmt.Errorf("error decoding public key: %s", err) + } + pub, err := x509.ParsePKIXPublicKey(derBytes) + if err != nil { + return nil, fmt.Errorf("error parsing public key: %v", err) + } + + verifierKey, err := tdnote.RFC6962VerifierString(origin, pub) + if err != nil { + return nil, fmt.Errorf("error creating RFC6962 verifier string: %v", err) + } + logSigV, err := tdnote.NewVerifier(verifierKey) + if err != nil { + return nil, fmt.Errorf("error creating verifier: %v", err) + } + + return logSigV, nil +} + +func readCheckpoint(ctx context.Context, fetcher *client.HTTPFetcher) (*log.Checkpoint, error) { + // Read Checkpoint + cpRaw, err := fetcher.ReadCheckpoint(ctx) + if err != nil { + return nil, fmt.Errorf("Failed to fetch checkpoint: %v", err) + } + logSigV, err := logSigVerifier(*origin, *logPubKey) + if err != nil { + return nil, fmt.Errorf("Failed to create verifier: %v", err) + } + cp, _, _, err := log.ParseCheckpoint(cpRaw, *origin, logSigV) + if err != nil { + return nil, fmt.Errorf("Failed to parse checkpoint: %v", err) + } + return cp, nil +} + +func verify(ctx context.Context, entry *staticct.Entry, cp *log.Checkpoint, li uint64, fetcher *client.HTTPFetcher) []error { // Check that the entry has been built properly + var errs []error e := ctonly.Entry{ Timestamp: entry.Timestamp, IsPrecert: entry.IsPrecert, @@ -118,131 +174,75 @@ func main() { IssuerKeyHash: entry.IssuerKeyHash, FingerprintsChain: entry.FingerprintsChain, } + + if li != entry.LeafIndex { + errs = append(errs, fmt.Errorf("leaf_index in leaf's %d SCT: got %d, want %d", li, li, entry.LeafIndex)) + } + var chain []*x509.Certificate if e.IsPrecert { cert, err := x509.ParseCertificate(e.Precertificate) if err != nil { - klog.Exitf("Failed to parse precertificate: %v", err) + errs = append(errs, fmt.Errorf("Failed to parse precertificate: %v", err)) } chain = append(chain, cert) } else { cert, err := x509.ParseCertificate(e.Certificate) if err != nil { - klog.Exitf("Failed to parse precertificate: %v", err) + errs = append(errs, fmt.Errorf("Failed to parse precertificate: %v", err)) } chain = append(chain, cert) } for i, hash := range entry.FingerprintsChain { iss, err := fetcher.ReadIssuer(ctx, hash[:]) if err != nil { - klog.Exitf("Failed to fetch issuer number %d: %v", i, err) + errs = append(errs, fmt.Errorf("Failed to fetch issuer number %d: %v", i, err)) } cert, err := x509.ParseCertificate(iss) if err != nil { - klog.Exitf("Failed ot parse issuer number %d: %v", i, err) + errs = append(errs, fmt.Errorf("Failed to parse issuer number %d: %v", i, err)) } chain = append(chain, cert) } + + // TODO(phboneff): check that the chain is valid + // TODO(phboneff): check that the last element of the chain is a root + // TODO(phboneff): check that the chain validates with the log's rootset + ee, err := x509util.EntryFromChain(chain, entry.IsPrecert, entry.Timestamp) if err != nil { - klog.Exitf("Failed to reconstruct entry from the leaf and issuers: %v", err) - } - - var errs []error - if e.Timestamp != ee.Timestamp { - errs = append(errs, fmt.Errorf("timestamp don't match: %d, %d", e.Timestamp, ee.Timestamp)) + errs = append(errs, fmt.Errorf("Failed to reconstruct entry from the leaf and issuers: %v", err)) } - if e.IsPrecert != ee.IsPrecert { - errs = append(errs, fmt.Errorf("IsPrecert don't match: %t, %t", e.IsPrecert, ee.IsPrecert)) + eee := ctonly.Entry{ + Timestamp: ee.Timestamp, + IsPrecert: ee.IsPrecert, + Certificate: ee.Certificate, + Precertificate: ee.Precertificate, + IssuerKeyHash: ee.IssuerKeyHash, + FingerprintsChain: ee.FingerprintsChain, } - if !bytes.Equal(e.Certificate, ee.Certificate) { - if e.IsPrecert { - errs = append(errs, fmt.Errorf("TBSCertificates don't match")) - } else { - errs = append(errs, fmt.Errorf("certificates don't match")) - } - } - if !bytes.Equal(e.Precertificate, ee.Precertificate) { - errs = append(errs, fmt.Errorf("precertificates don't match")) - } - if !bytes.Equal(e.IssuerKeyHash, ee.IssuerKeyHash) { - errs = append(errs, fmt.Errorf("IssuerKeyHashes don't match, got %q, want %q", hex.EncodeToString(e.IssuerKeyHash), hex.EncodeToString(ee.IssuerKeyHash))) - } - if len(e.FingerprintsChain) != len(ee.FingerprintsChain) { - errs = append(errs, fmt.Errorf("lengths of fingerprints chains don't match: got %d, want %d", len(e.FingerprintsChain), len(ee.FingerprintsChain))) - } else { - for i := range e.FingerprintsChain { - if !bytes.Equal(e.FingerprintsChain[i][:], ee.FingerprintsChain[i][:]) { - errs = append(errs, fmt.Errorf("fingerprints %d don't match, got %q, want %q", i, hex.EncodeToString(e.FingerprintsChain[i][:]), hex.EncodeToString(ee.FingerprintsChain[i][:]))) - } - } - } - if len(errs) > 0 { - klog.Exitf("Leaf entry not built properly: %v", errors.Join(errs...)) + if diff := cmp.Diff(e, eee); len(diff) != 0 { + errs = append(errs, fmt.Errorf("Leaf entry not built properly (- fetched leaf data, + expected value): \n%s", diff)) } - // TODO(phboneff): check that the chain is valid // TODO(phboneff): if this is an end cert and it has an SCT from this very log, check that SCT - // Build inclusion proof + // Inclusion proof proofBuilder, err := client.NewProofBuilder(ctx, log.Checkpoint{ Origin: *origin, Size: cp.Size, Hash: cp.Hash}, fetcher.ReadTile) if err != nil { - klog.Exitf("Failed to create proofBuilder: %v", err) + errs = append(errs, fmt.Errorf("Failed to create proofBuilder: %v", err)) } mlh := e.MerkleLeafHash(entry.LeafIndex) ip, err := proofBuilder.InclusionProof(ctx, li) if err != nil { - klog.Exitf("Failed to build InclusionProof %v", err) + errs = append(errs, fmt.Errorf("Failed to build InclusionProof %v", err)) } if err := proof.VerifyInclusion(hasher, li, cp.Size, mlh, ip, cp.Hash); err != nil { - klog.Exitf("Failed to verify inclusion of leaf %d in tree of size %d: %v", li, cp.Size, err) - } - - pemBlock := &pem.Block{ - Type: "CERTIFICATE", - Bytes: func() []byte { - if entry.IsPrecert { - return entry.Precertificate - } - return entry.Certificate - }(), - } - - if err := pem.Encode(os.Stdout, pemBlock); err != nil { - klog.Exitf("Failed to encode PEM: %v", err) + errs = append(errs, fmt.Errorf("Failed to verify inclusion of leaf %d in tree of size %d: %v", li, cp.Size, err)) } -} -// logSigVerifier creates a note.Verifier for the Static CT API log by taking -// an origin string and a base64-encoded public key. -func logSigVerifier(origin, b64PubKey string) (note.Verifier, error) { - if origin == "" { - return nil, errors.New("origin cannot be empty") - } - if b64PubKey == "" { - return nil, errors.New("log public key cannot be empty") - } - - derBytes, err := base64.StdEncoding.DecodeString(b64PubKey) - if err != nil { - return nil, fmt.Errorf("error decoding public key: %s", err) - } - pub, err := x509.ParsePKIXPublicKey(derBytes) - if err != nil { - return nil, fmt.Errorf("error parsing public key: %v", err) - } - - verifierKey, err := tdnote.RFC6962VerifierString(origin, pub) - if err != nil { - return nil, fmt.Errorf("error creating RFC6962 verifier string: %v", err) - } - logSigV, err := tdnote.NewVerifier(verifierKey) - if err != nil { - return nil, fmt.Errorf("error creating verifier: %v", err) - } - - return logSigV, nil + return errs } From 090063f5ced24c2954a527d619ee35061735e8e6 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Fri, 19 Sep 2025 16:54:26 +0000 Subject: [PATCH 4/5] add verify flag --- cmd/client/main.go | 47 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index d639d2cd..b6f55dbc 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "crypto/x509" "encoding/base64" @@ -31,8 +32,9 @@ import ( var ( monitoringURL = flag.String("monitoring_url", "", "Log monitoring URL.") leafIndex = flag.String("leaf_index", "", "The index of the leaf to fetch.") - origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix.") - logPubKey = flag.String("log_public_key", "", "Public key for the log, base64 encoded.") + origin = flag.String("origin", "", "Origin of the log, for checkpoints and the monitoring prefix. MUST be provided if verify=true.") + logPubKey = flag.String("log_public_key", "", "Public key for the log, base64 encoded. MUST be provided if verify=true.") + verify = flag.Bool("verify", true, "Whether or not to verify the leaf entry.") ) var ( @@ -62,6 +64,15 @@ func main() { klog.Exitf("Invalid --monitoring_url %q: %v", *monitoringURL, err) } + if *verify { + if *logPubKey == "" { + klog.Exitf("log_public_key MUST be provided when verify=true") + } + if *origin == "" { + klog.Exitf("origin MUST be provided when verify=true") + } + } + // Create client hc := &http.Client{ Timeout: 30 * time.Second, @@ -96,8 +107,10 @@ func main() { klog.Exitf("Failed to unmarshal entry: %v", err) } - if errs := verify(ctx, &entry, cp, li, fetcher); len(errs) != 0 { - klog.Exitf("Failed to verify leaf entry: %s", errors.Join(errs...)) + if *verify { + if errs := verifyLeafEntry(ctx, &entry, cp, li, fetcher); len(errs) != 0 { + klog.Exitf("Failed to verify leaf entry: %s", errors.Join(errs...)) + } } pemBlock := &pem.Block{ @@ -152,18 +165,30 @@ func readCheckpoint(ctx context.Context, fetcher *client.HTTPFetcher) (*log.Chec if err != nil { return nil, fmt.Errorf("Failed to fetch checkpoint: %v", err) } - logSigV, err := logSigVerifier(*origin, *logPubKey) - if err != nil { - return nil, fmt.Errorf("Failed to create verifier: %v", err) + if *verify { + logSigV, err := logSigVerifier(*origin, *logPubKey) + if err != nil { + return nil, fmt.Errorf("Failed to create verifier: %v", err) + } + cp, _, _, err := log.ParseCheckpoint(cpRaw, *origin, logSigV) + if err != nil { + return nil, fmt.Errorf("Failed to parse checkpoint: %v", err) + } + return cp, nil + } + // A https://c2sp.org/static-ct-api logsize is on the second line + l := bytes.SplitN(cpRaw, []byte("\n"), 3) + if len(l) < 2 { + return nil, errors.New("invalid checkpoint - no size") } - cp, _, _, err := log.ParseCheckpoint(cpRaw, *origin, logSigV) + size, err := strconv.ParseUint(string(l[1]), 10, 64) if err != nil { - return nil, fmt.Errorf("Failed to parse checkpoint: %v", err) + return nil, fmt.Errorf("invalid checkpoint - can't extract size: %v", err) } - return cp, nil + return &log.Checkpoint{Size: size}, nil } -func verify(ctx context.Context, entry *staticct.Entry, cp *log.Checkpoint, li uint64, fetcher *client.HTTPFetcher) []error { +func verifyLeafEntry(ctx context.Context, entry *staticct.Entry, cp *log.Checkpoint, li uint64, fetcher *client.HTTPFetcher) []error { // Check that the entry has been built properly var errs []error e := ctonly.Entry{ From 63fd586abdaa69f9308d33c364577f1c638dd2a7 Mon Sep 17 00:00:00 2001 From: Philippe Boneff Date: Fri, 19 Sep 2025 17:00:28 +0000 Subject: [PATCH 5/5] fix msgs --- cmd/client/main.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index b6f55dbc..d498d912 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -163,16 +163,16 @@ func readCheckpoint(ctx context.Context, fetcher *client.HTTPFetcher) (*log.Chec // Read Checkpoint cpRaw, err := fetcher.ReadCheckpoint(ctx) if err != nil { - return nil, fmt.Errorf("Failed to fetch checkpoint: %v", err) + return nil, fmt.Errorf("failed to fetch checkpoint: %v", err) } if *verify { logSigV, err := logSigVerifier(*origin, *logPubKey) if err != nil { - return nil, fmt.Errorf("Failed to create verifier: %v", err) + return nil, fmt.Errorf("failed to create verifier: %v", err) } cp, _, _, err := log.ParseCheckpoint(cpRaw, *origin, logSigV) if err != nil { - return nil, fmt.Errorf("Failed to parse checkpoint: %v", err) + return nil, fmt.Errorf("failed to parse checkpoint: %v", err) } return cp, nil } @@ -208,24 +208,24 @@ func verifyLeafEntry(ctx context.Context, entry *staticct.Entry, cp *log.Checkpo if e.IsPrecert { cert, err := x509.ParseCertificate(e.Precertificate) if err != nil { - errs = append(errs, fmt.Errorf("Failed to parse precertificate: %v", err)) + errs = append(errs, fmt.Errorf("failed to parse precertificate: %v", err)) } chain = append(chain, cert) } else { cert, err := x509.ParseCertificate(e.Certificate) if err != nil { - errs = append(errs, fmt.Errorf("Failed to parse precertificate: %v", err)) + errs = append(errs, fmt.Errorf("failed to parse precertificate: %v", err)) } chain = append(chain, cert) } for i, hash := range entry.FingerprintsChain { iss, err := fetcher.ReadIssuer(ctx, hash[:]) if err != nil { - errs = append(errs, fmt.Errorf("Failed to fetch issuer number %d: %v", i, err)) + errs = append(errs, fmt.Errorf("failed to fetch issuer number %d: %v", i, err)) } cert, err := x509.ParseCertificate(iss) if err != nil { - errs = append(errs, fmt.Errorf("Failed to parse issuer number %d: %v", i, err)) + errs = append(errs, fmt.Errorf("failed to parse issuer number %d: %v", i, err)) } chain = append(chain, cert) } @@ -236,7 +236,7 @@ func verifyLeafEntry(ctx context.Context, entry *staticct.Entry, cp *log.Checkpo ee, err := x509util.EntryFromChain(chain, entry.IsPrecert, entry.Timestamp) if err != nil { - errs = append(errs, fmt.Errorf("Failed to reconstruct entry from the leaf and issuers: %v", err)) + errs = append(errs, fmt.Errorf("failed to reconstruct entry from the leaf and issuers: %v", err)) } eee := ctonly.Entry{ Timestamp: ee.Timestamp, @@ -247,7 +247,7 @@ func verifyLeafEntry(ctx context.Context, entry *staticct.Entry, cp *log.Checkpo FingerprintsChain: ee.FingerprintsChain, } if diff := cmp.Diff(e, eee); len(diff) != 0 { - errs = append(errs, fmt.Errorf("Leaf entry not built properly (- fetched leaf data, + expected value): \n%s", diff)) + errs = append(errs, fmt.Errorf("leaf entry not built properly (- fetched leaf data, + expected value): \n%s", diff)) } // TODO(phboneff): if this is an end cert and it has an SCT from this very log, check that SCT @@ -258,15 +258,15 @@ func verifyLeafEntry(ctx context.Context, entry *staticct.Entry, cp *log.Checkpo Size: cp.Size, Hash: cp.Hash}, fetcher.ReadTile) if err != nil { - errs = append(errs, fmt.Errorf("Failed to create proofBuilder: %v", err)) + errs = append(errs, fmt.Errorf("failed to create proofBuilder: %v", err)) } mlh := e.MerkleLeafHash(entry.LeafIndex) ip, err := proofBuilder.InclusionProof(ctx, li) if err != nil { - errs = append(errs, fmt.Errorf("Failed to build InclusionProof %v", err)) + errs = append(errs, fmt.Errorf("failed to build InclusionProof %v", err)) } if err := proof.VerifyInclusion(hasher, li, cp.Size, mlh, ip, cp.Hash); err != nil { - errs = append(errs, fmt.Errorf("Failed to verify inclusion of leaf %d in tree of size %d: %v", li, cp.Size, err)) + errs = append(errs, fmt.Errorf("failed to verify inclusion of leaf %d in tree of size %d: %v", li, cp.Size, err)) } return errs