Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ctclient: Use Cobra library for command-line tools #901

Merged
merged 16 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
* `ctfe.PEMCertPool` type has been moved to `x509util.PEMCertPool` to reduce
dependencies (#903).

### Misc
* `ctclient` tool now uses Cobra for better CLI experience (#901).

## v1.1.2

### CTFE
Expand Down
82 changes: 82 additions & 0 deletions client/ctclient/cmd/bisect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2022 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"context"
"fmt"
"sort"

"github.com/golang/glog"
ct "github.com/google/certificate-transparency-go"
"github.com/spf13/cobra"
)

func init() {
cmd := cobra.Command{
Use: fmt.Sprintf("bisect %s --timestamp=ts [--chain] [--text=false]", connectionFlags),
Aliases: []string{"find-timestamp"},
Short: "Find a log entry by timestamp",
Run: func(cmd *cobra.Command, _ []string) {
runBisect(cmd.Context())
},
}
cmd.Flags().Int64Var(&timestamp, "timestamp", 0, "Timestamp to use for inclusion checking")
// TODO(pavelkalinnikov): Don't share these parameters with get-entries.
cmd.Flags().BoolVar(&chainOut, "chain", false, "Display entire certificate chain")
cmd.Flags().BoolVar(&textOut, "text", true, "Display certificates as text")
rootCmd.AddCommand(&cmd)
}

// runBisect runs the bisect command.
func runBisect(ctx context.Context) {
logClient := connect(ctx)
if timestamp == 0 {
glog.Exit("No -timestamp option supplied")
}
target := timestamp
sth, err := logClient.GetSTH(ctx)
if err != nil {
exitWithDetails(err)
}
getEntry := func(idx int64) *ct.RawLogEntry {
entries, err := logClient.GetRawEntries(ctx, idx, idx)
if err != nil {
exitWithDetails(err)
}
if l := len(entries.Entries); l != 1 {
glog.Exitf("Unexpected number (%d) of entries received requesting index %d", l, idx)
}
logEntry, err := ct.RawLogEntryFromLeaf(idx, &entries.Entries[0])
if err != nil {
glog.Exitf("Failed to parse leaf %d: %v", idx, err)
}
return logEntry
}
// Performing a binary search assumes that the timestamps are monotonically
// increasing.
idx := sort.Search(int(sth.TreeSize), func(idx int) bool {
glog.V(1).Infof("check timestamp at index %d", idx)
entry := getEntry(int64(idx))
return entry.Leaf.TimestampedEntry.Timestamp >= uint64(target)
})
when := ct.TimestampToTime(uint64(target))
if idx >= int(sth.TreeSize) {
fmt.Printf("No entry with timestamp>=%d (%v) found up to tree size %d\n", target, when, sth.TreeSize)
return
}
fmt.Printf("First entry with timestamp>=%d (%v) found at index %d\n", target, when, idx)
showRawLogEntry(getEntry(int64(idx)))
}
114 changes: 114 additions & 0 deletions client/ctclient/cmd/get_consistency_proof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2022 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"

"github.com/golang/glog"
"github.com/google/certificate-transparency-go/client"
"github.com/spf13/cobra"
"github.com/transparency-dev/merkle"
"github.com/transparency-dev/merkle/rfc6962"
)

var (
treeHash string
prevSize uint64
prevHash string
)

func init() {
cmd := cobra.Command{
Use: fmt.Sprintf("get-consistency-proof %s --size=N --tree_hash=hash --prev_size=N --prev_hash=hash", connectionFlags),
Aliases: []string{"getconsistencyproof", "consistency-proof", "consistency"},
Short: "Fetch and verify a consistency proof between two tree states",
Run: func(cmd *cobra.Command, _ []string) {
runGetConsistencyProof(cmd.Context())
},
}
// TODO(pavelkalinnikov): Don't share this parameter with get-inclusion-proof.
cmd.Flags().Uint64Var(&treeSize, "size", 0, "Tree size to query at")
cmd.Flags().StringVar(&treeHash, "tree_hash", "", "Tree hash to check against (as hex string or base64)")
cmd.Flags().Uint64Var(&prevSize, "prev_size", 0, "Previous tree size to get consistency against")
cmd.Flags().StringVar(&prevHash, "prev_hash", "", "Previous tree hash to check against (as hex string or base64)")
rootCmd.AddCommand(&cmd)
}

// runGetConsistencyProof runs the get-consistency-proof command.
func runGetConsistencyProof(ctx context.Context) {
logClient := connect(ctx)
if treeSize <= 0 {
glog.Exit("No valid --size supplied")
}
if prevSize <= 0 {
glog.Exit("No valid --prev_size supplied")
}
var hash1, hash2 []byte
if prevHash != "" {
var err error
hash1, err = hashFromString(prevHash)
if err != nil {
glog.Exitf("Invalid --prev_hash: %v", err)
}
}
if treeHash != "" {
var err error
hash2, err = hashFromString(treeHash)
if err != nil {
glog.Exitf("Invalid --tree_hash: %v", err)
}
}
if (hash1 != nil) != (hash2 != nil) {
glog.Exitf("Need both --prev_hash and --tree_hash or neither")
}
getConsistencyProofBetween(ctx, logClient, prevSize, treeSize, hash1, hash2)
}

func getConsistencyProofBetween(ctx context.Context, logClient client.CheckLogClient, first, second uint64, prevHash, treeHash []byte) {
proof, err := logClient.GetSTHConsistency(ctx, uint64(first), uint64(second))
if err != nil {
exitWithDetails(err)
}
fmt.Printf("Consistency proof from size %d to size %d:\n", first, second)
for _, e := range proof {
fmt.Printf(" %x\n", e)
}
if prevHash == nil || treeHash == nil {
return
}
// We have tree hashes so we can verify the proof.
verifier := merkle.NewLogVerifier(rfc6962.DefaultHasher)
if err := verifier.VerifyConsistency(first, second, prevHash, treeHash, proof); err != nil {
glog.Exitf("Failed to VerifyConsistencyProof(%x @size=%d, %x @size=%d): %v", prevHash, first, treeHash, second, err)
}
fmt.Printf("Verified that hash %x @%d + proof = hash %x @%d\n", prevHash, first, treeHash, second)
}

func hashFromString(input string) ([]byte, error) {
hash, err := hex.DecodeString(input)
if err == nil && len(hash) == sha256.Size {
return hash, nil
}
hash, err = base64.StdEncoding.DecodeString(input)
if err == nil && len(hash) == sha256.Size {
return hash, nil
}
return nil, fmt.Errorf("hash value %q failed to parse as 32-byte hex or base64", input)
}
127 changes: 127 additions & 0 deletions client/ctclient/cmd/get_entries.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2022 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"context"
"encoding/pem"
"fmt"
"os"

"github.com/golang/glog"
ct "github.com/google/certificate-transparency-go"
"github.com/google/certificate-transparency-go/x509"
"github.com/google/certificate-transparency-go/x509util"
"github.com/spf13/cobra"
)

var (
getFirst int64
getLast int64
chainOut bool
textOut bool
)

func init() {
cmd := cobra.Command{
Use: fmt.Sprintf("get-entries %s --first=idx [--last=idx]", connectionFlags),
Aliases: []string{"getentries", "entries"},
Short: "Fetch a range of entries in the log",
Run: func(cmd *cobra.Command, _ []string) {
runGetEntries(cmd.Context())
},
}
cmd.Flags().Int64Var(&getFirst, "first", -1, "First entry to get")
cmd.Flags().Int64Var(&getLast, "last", -1, "Last entry to get")
cmd.Flags().BoolVar(&chainOut, "chain", false, "Display entire certificate chain")
cmd.Flags().BoolVar(&textOut, "text", true, "Display certificates as text")
rootCmd.AddCommand(&cmd)
}

// runGetEntries runs the get-entries command.
func runGetEntries(ctx context.Context) {
logClient := connect(ctx)
if getFirst == -1 {
glog.Exit("No -first option supplied")
}
if getLast == -1 {
getLast = getFirst
}
rsp, err := logClient.GetRawEntries(ctx, getFirst, getLast)
if err != nil {
exitWithDetails(err)
}

for i, rawEntry := range rsp.Entries {
index := getFirst + int64(i)
rle, err := ct.RawLogEntryFromLeaf(index, &rawEntry)
if err != nil {
fmt.Printf("Index=%d Failed to unmarshal leaf entry: %v", index, err)
continue
}
showRawLogEntry(rle)
}
}

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)

switch ts.EntryType {
case ct.X509LogEntryType:
fmt.Printf("X.509 certificate:\n")
showRawCert(*ts.X509Entry)
case ct.PrecertLogEntryType:
fmt.Printf("pre-certificate from issuer with keyhash %x:\n", ts.PrecertEntry.IssuerKeyHash)
showRawCert(rle.Cert) // As-submitted: with signature and poison.
default:
fmt.Printf("Unhandled log entry type %d\n", ts.EntryType)
}
if chainOut {
for _, c := range rle.Chain {
showRawCert(c)
}
}
}

func showRawCert(cert ct.ASN1Cert) {
if textOut {
c, err := x509.ParseCertificate(cert.Data)
if err != nil {
glog.Errorf("Error parsing certificate: %q", err.Error())
}
if c == nil {
return
}
showParsedCert(c)
} else {
showPEMData(cert.Data)
}
}

func showParsedCert(cert *x509.Certificate) {
if textOut {
fmt.Printf("%s\n", x509util.CertificateToString(cert))
} else {
showPEMData(cert.Raw)
}
}

func showPEMData(data []byte) {
if err := pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: data}); err != nil {
glog.Errorf("Failed to PEM encode cert: %q", err.Error())
}
}
Loading