Skip to content

Commit 85ce9e2

Browse files
committed
cmd/witnessctl: add pull-logs command
Fixes #31
1 parent 242a202 commit 85ce9e2

5 files changed

Lines changed: 267 additions & 8 deletions

File tree

cmd/litewitness/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ the origin and key from a 32-byte hex-encoded Ed25519 public key.
8888

8989
witnessctl list-logs -db <path>
9090

91+
The `pull-logs` command fetches a list of known logs from [a witness network log
92+
list](https://github.com/transparency-dev/witness-network/blob/main/site/content/participate.md#automate-discovery-of-logs-in-the-selected-lists)
93+
URL or file path. It only adds new logs, it does not remove or change the keys
94+
of existing ones. It is designed to run in a cronjob, and by default (without
95+
`-verbose`) it only prints output if there are logs with keys different from
96+
what is already in the database, suggesting they may need to be rotated.
97+
98+
witnessctl pull-logs -db <path> -source <log list url> -verbose
99+
91100
The `list-logs` command lists known logs, in JSON lines like the following.
92101

93102
{"origin":"sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562","size":5,"root_hash":"QrtXrQZCCvpIgsSmOsah7HdICzMLLyDfxToMql9WTjY=","keys":["sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562+5202289b+Af/cLU2Y5BJNP+r3iMDC+av9eWCD0fBJVDfzAux5zxAP"]}

cmd/litewitness/testdata/gentest/sigsum.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"log"
1717
"net/url"
1818

19-
"github.com/caarlos0/sshmarshal"
2019
"golang.org/x/crypto/hkdf"
2120
"golang.org/x/crypto/ssh"
2221
"golang.org/x/mod/sumdb/note"
@@ -55,8 +54,11 @@ func main() {
5554
fmt.Printf("- origin URL-encoded: %s\n", url.QueryEscape(origin))
5655

5756
const algEd25519 = 1
58-
skey := fmt.Sprintf("PRIVATE+KEY+%s+%08x+%s", origin, noteKeyHash(origin, append([]byte{algEd25519}, publicKey...)), base64.StdEncoding.EncodeToString(append([]byte{algEd25519}, privateKey.Seed()...)))
57+
kh := noteKeyHash(origin, append([]byte{algEd25519}, publicKey...))
58+
vkey := fmt.Sprintf("%s+%08x+%s", origin, kh, base64.StdEncoding.EncodeToString(append([]byte{algEd25519}, publicKey...)))
59+
skey := fmt.Sprintf("PRIVATE+KEY+%s+%08x+%s", origin, kh, base64.StdEncoding.EncodeToString(append([]byte{algEd25519}, privateKey.Seed()...)))
5960
s, _ := note.NewSigner(skey)
61+
fmt.Printf("- log note vkey: %s\n", vkey)
6062
fmt.Printf("- log note key: %s\n", skey)
6163

6264
witSeed := make([]byte, ed25519.SeedSize)
@@ -69,7 +71,7 @@ func main() {
6971
pkHash := sigsum.HashBytes(ss.PublicKey().(ssh.CryptoPublicKey).CryptoPublicKey().(ed25519.PublicKey))
7072
fmt.Printf("- witness key hash: %s\n", hex.EncodeToString(pkHash[:]))
7173
fmt.Printf("- witness key: %x\n", witKey)
72-
pemKey, err := sshmarshal.MarshalPrivateKey(witKey, "")
74+
pemKey, err := ssh.MarshalPrivateKey(witKey, "")
7375
if err != nil {
7476
log.Fatal(err)
7577
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# pull logs
2+
exec witnessctl pull-logs -source=log_list.0
3+
! stdout .
4+
! stderr .
5+
6+
# pull logs again, with no changes
7+
exec witnessctl pull-logs -source=log_list.1
8+
! stdout .
9+
! stderr .
10+
11+
# pull logs again, with a key change
12+
exec witnessctl pull-logs -source=log_list.2
13+
! stdout .
14+
stderr 'listed with a different key'
15+
16+
# check state
17+
exec witnessctl list-logs
18+
cmp stdout list-logs.jsonl
19+
! stderr .
20+
21+
22+
-- log_list.0 --
23+
#
24+
# List: 10qps-100klogs
25+
# Revision: 123
26+
# Generated: YYYY-MM-DD HH:MM:SS UTC
27+
# Other undefined debug information.
28+
#
29+
logs/v0
30+
31+
# gentest seed 467e9782a4eaa2bca9c9f5bcb067fb7bce1bfcae7b18f27bac0900ad93825300
32+
vkey sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702+9bc54c7f+AVv6q3xDaHxI2aTemqEb7W6ZcbO7QbTqTr20thOqfqsw
33+
qpd 86400
34+
contact https://tlog.foo.org/contact
35+
unknown
36+
foo bar
37+
38+
# gentest seed 07210b16c5409251a63305371bb0e86bbe9e005c41ca1963f9ed3d89931a8a16
39+
vkey sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h
40+
origin example.com/foo
41+
qpd 24
42+
contact sysadmin (at) bar.org
43+
44+
-- log_list.1 --
45+
logs/v0
46+
47+
# new log
48+
# gentest seed 92540ba271142b0011c96531067b8f1f7695fd16ff7d92833767f821ab983c4d
49+
vkey sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120+7f693d84+AUlxeri80AO7/4j/+OGo+5M2Sud0ktFg34uZl2fZnjJT
50+
51+
# existing log, same key
52+
vkey sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h
53+
origin example.com/foo
54+
55+
-- log_list.2 --
56+
logs/v0
57+
58+
# new log
59+
# gentest seed 92540ba271142b0011c96531067b8f1f7695fd16ff7d92833767f821ab983c4d
60+
vkey sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120+7f693d84+AUlxeri80AO7/4j/+OGo+5M2Sud0ktFg34uZl2fZnjJT
61+
62+
# existing log, same key
63+
vkey sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h
64+
origin example.com/foo
65+
66+
# known log, different key
67+
# gentest seed cf8477f7158ac47b018a55fda4bd1af29b0e864cdd3fd495efa477639d0b690c
68+
vkey sigsum.org/v1/tree/80f121ea8f6364ca8c19212d7caf4e235d00aa34a98d86b60204c24f981a4933+a05ec139+ASZ0biMUCMjOMmt0NyD8g6Mt6RGZGXJU/ZpM5Oy0otex
69+
origin sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702
70+
71+
-- list-logs.jsonl --
72+
{"origin":"example.com/foo","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h"]}
73+
{"origin":"sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702+9bc54c7f+AVv6q3xDaHxI2aTemqEb7W6ZcbO7QbTqTr20thOqfqsw"]}
74+
{"origin":"sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120+7f693d84+AUlxeri80AO7/4j/+OGo+5M2Sud0ktFg34uZl2fZnjJT"]}

cmd/witnessctl/loglist.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"strings"
7+
8+
"golang.org/x/mod/sumdb/note"
9+
)
10+
11+
func parseLogList(logList []byte, verbose bool) (map[string]string, error) {
12+
logs := make(map[string]string)
13+
var sawHeader bool
14+
var vkey, origin string
15+
finalizeLogEntry := func() {
16+
if vkey == "" {
17+
// The list may be empty.
18+
return
19+
}
20+
defer func() {
21+
vkey = ""
22+
origin = ""
23+
}()
24+
v, err := note.NewVerifier(vkey)
25+
if err != nil {
26+
if verbose {
27+
log.Printf("Skipping invalid vkey %q: %v", vkey, err)
28+
}
29+
return
30+
}
31+
if origin == "" {
32+
origin = v.Name()
33+
}
34+
if logs[origin] != "" {
35+
if verbose {
36+
log.Printf("Skipping duplicate log entry for %q", origin)
37+
log.Printf(" - %q", logs[origin])
38+
log.Printf(" - %q (skipped)", vkey)
39+
}
40+
return
41+
}
42+
logs[origin] = vkey
43+
}
44+
for line := range strings.Lines(string(logList)) {
45+
line = strings.TrimSpace(line)
46+
if strings.HasPrefix(line, "#") {
47+
// Comment line, skip.
48+
continue
49+
}
50+
if line == "" {
51+
// Empty line, skip.
52+
continue
53+
}
54+
if !sawHeader {
55+
// First non-comment, non-empty line is the header.
56+
if line != "logs/v0" {
57+
return nil, fmt.Errorf("invalid log list header: %q", line)
58+
}
59+
sawHeader = true
60+
continue
61+
}
62+
key, value, _ := strings.Cut(line, " ")
63+
if vkey == "" && key != "vkey" {
64+
return nil, fmt.Errorf("expected vkey entry, got %q", line)
65+
}
66+
switch key {
67+
case "vkey":
68+
finalizeLogEntry()
69+
if value == "" {
70+
return nil, fmt.Errorf("empty vkey entry")
71+
}
72+
vkey = value
73+
case "origin":
74+
origin = value
75+
default:
76+
// Unknown key, ignore.
77+
}
78+
}
79+
finalizeLogEntry()
80+
return logs, nil
81+
}

cmd/witnessctl/witnessctl.go

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import (
55
"encoding/hex"
66
"flag"
77
"fmt"
8+
"io"
89
"log"
10+
"net/http"
911
"os"
12+
"strings"
13+
"time"
1014

1115
"filippo.io/torchwood/internal/witness"
1216
"golang.org/x/mod/sumdb/note"
@@ -23,6 +27,7 @@ func usage() {
2327
fmt.Println(" add-key -db <path> -origin <origin> -key <verifier key>")
2428
fmt.Println(" del-key -db <path> -origin <origin> -key <verifier key>")
2529
fmt.Println(" add-sigsum-log -db <path> -key <hex-encoded key>")
30+
fmt.Println(" pull-logs -db <path> -source <witness url> [-verbose]")
2631
fmt.Println(" list-logs -db <path>")
2732
os.Exit(1)
2833
}
@@ -39,27 +44,38 @@ func main() {
3944
fs.Parse(os.Args[2:])
4045
db := openDB(*dbFlag)
4146
addLog(db, *originFlag)
47+
log.Printf("Added log %q.", *originFlag)
4248

4349
case "add-key":
4450
originFlag := fs.String("origin", "", "log name")
4551
keyFlag := fs.String("key", "", "verifier key")
4652
fs.Parse(os.Args[2:])
4753
db := openDB(*dbFlag)
54+
checkKeyMatches(*originFlag, *keyFlag)
4855
addKey(db, *originFlag, *keyFlag)
56+
log.Printf("Added key %q for log %q.", *keyFlag, *originFlag)
4957

5058
case "del-key":
5159
originFlag := fs.String("origin", "", "log name")
5260
keyFlag := fs.String("key", "", "verifier key")
5361
fs.Parse(os.Args[2:])
5462
db := openDB(*dbFlag)
5563
delKey(db, *originFlag, *keyFlag)
64+
log.Printf("Deleted key %q for log %q.", *keyFlag, *originFlag)
5665

5766
case "add-sigsum-log":
5867
keyFlag := fs.String("key", "", "hex-encoded key")
5968
fs.Parse(os.Args[2:])
6069
db := openDB(*dbFlag)
6170
addSigsumLog(db, *keyFlag)
6271

72+
case "pull-logs":
73+
sourceFlag := fs.String("source", "", "witness network log list URL or file path")
74+
verboseFlag := fs.Bool("verbose", false, "verbose output")
75+
fs.Parse(os.Args[2:])
76+
db := openDB(*dbFlag)
77+
pullLogs(db, *sourceFlag, *verboseFlag)
78+
6379
case "list-logs":
6480
fs.Parse(os.Args[2:])
6581
db := openDB(*dbFlag)
@@ -84,22 +100,23 @@ func addLog(db *sqlite.Conn, origin string) {
84100
nil, origin, base64.StdEncoding.EncodeToString(treeHash[:])); err != nil {
85101
log.Fatalf("Error adding log: %v", err)
86102
}
87-
log.Printf("Added log %q.", origin)
88103
}
89104

90-
func addKey(db *sqlite.Conn, origin string, vk string) {
105+
func checkKeyMatches(origin string, vk string) {
91106
v, err := note.NewVerifier(vk)
92107
if err != nil {
93108
log.Fatalf("Error parsing verifier key: %v", err)
94109
}
95110
if v.Name() != origin {
96111
log.Printf("Warning: verifier key name %q does not match origin %q.", v.Name(), origin)
97112
}
98-
err = sqlitex.Exec(db, "INSERT INTO key (origin, key) VALUES (?, ?)", nil, origin, vk)
113+
}
114+
115+
func addKey(db *sqlite.Conn, origin string, vk string) {
116+
err := sqlitex.Exec(db, "INSERT INTO key (origin, key) VALUES (?, ?)", nil, origin, vk)
99117
if err != nil {
100118
log.Fatalf("Error adding key: %v", err)
101119
}
102-
log.Printf("Added key %q.", vk)
103120
}
104121

105122
func delKey(db *sqlite.Conn, origin string, vk string) {
@@ -110,7 +127,6 @@ func delKey(db *sqlite.Conn, origin string, vk string) {
110127
if db.Changes() == 0 {
111128
log.Fatalf("Key %q not found.", vk)
112129
}
113-
log.Printf("Deleted key %q.", vk)
114130
}
115131

116132
func addSigsumLog(db *sqlite.Conn, keyFlag string) {
@@ -129,6 +145,83 @@ func addSigsumLog(db *sqlite.Conn, keyFlag string) {
129145
}
130146
addLog(db, origin)
131147
addKey(db, origin, vk)
148+
log.Printf("Added Sigsum log %q with key %q.", origin, vk)
149+
}
150+
151+
func pullLogs(db *sqlite.Conn, source string, verbose bool) {
152+
var logList []byte
153+
if strings.HasPrefix(source, "https://") {
154+
client := http.Client{Timeout: 30 * time.Second}
155+
resp, err := client.Get(source)
156+
if err != nil {
157+
log.Fatalf("Error fetching log list: %v", err)
158+
}
159+
defer resp.Body.Close()
160+
if resp.StatusCode != http.StatusOK {
161+
log.Fatalf("Error fetching log list: HTTP %d", resp.StatusCode)
162+
}
163+
logList, err = io.ReadAll(resp.Body)
164+
if err != nil {
165+
log.Fatalf("Error reading log list: %v", err)
166+
}
167+
} else {
168+
var err error
169+
logList, err = os.ReadFile(source)
170+
if err != nil {
171+
log.Fatalf("Error reading log list: %v", err)
172+
}
173+
}
174+
logs, err := parseLogList(logList, verbose)
175+
if err != nil {
176+
log.Fatalf("Error parsing log list: %v", err)
177+
}
178+
for origin, vkey := range logs {
179+
keys, exists := logKeys(db, origin)
180+
if exists {
181+
if keys[vkey] {
182+
// Key already exists, nothing to do.
183+
if verbose {
184+
log.Printf("Log %q with key %q already exists, skipping.", origin, vkey)
185+
}
186+
continue
187+
}
188+
// Key is different, warn.
189+
log.Printf("Warning: log %q is listed with a different key than the one in the database.\n", origin)
190+
log.Printf(" - existing keys:\n")
191+
for k := range keys {
192+
log.Printf(" - %q\n", k)
193+
}
194+
log.Printf(" - new key:\n")
195+
log.Printf(" - %q\n", vkey)
196+
continue
197+
}
198+
// New log, add it.
199+
addLog(db, origin)
200+
addKey(db, origin, vkey)
201+
if verbose {
202+
log.Printf("Added log %q with key %q.", origin, vkey)
203+
}
204+
}
205+
}
206+
207+
func logKeys(db *sqlite.Conn, origin string) (keys map[string]bool, exists bool) {
208+
keys = make(map[string]bool)
209+
if err := sqlitex.Exec(db, "SELECT 1 FROM log WHERE origin = ?", func(stmt *sqlite.Stmt) error {
210+
exists = true
211+
return nil
212+
}, origin); err != nil {
213+
log.Fatalf("Error looking for log %q: %v", origin, err)
214+
}
215+
if !exists {
216+
return keys, false
217+
}
218+
if err := sqlitex.Exec(db, "SELECT key FROM key WHERE origin = ?", func(stmt *sqlite.Stmt) error {
219+
keys[stmt.ColumnText(0)] = true
220+
return nil
221+
}, origin); err != nil {
222+
log.Fatalf("Error querying keys: %v", err)
223+
}
224+
return keys, true
132225
}
133226

134227
func listLogs(db *sqlite.Conn) {

0 commit comments

Comments
 (0)