Skip to content

Commit be6a777

Browse files
committed
torchwood: implement c2sp.org/tlog-proof formatting and verification
1 parent 072ed25 commit be6a777

2 files changed

Lines changed: 766 additions & 0 deletions

File tree

spicy.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package torchwood
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"errors"
7+
"fmt"
8+
"strconv"
9+
"strings"
10+
11+
"golang.org/x/mod/sumdb/note"
12+
"golang.org/x/mod/sumdb/tlog"
13+
)
14+
15+
// FormatProof formats a tlog record inclusion proof (a "spicy signature") for
16+
// the record at index idx with proof p and signed checkpoint signedCheckpoint.
17+
//
18+
// The returned byte slice is encoded according to c2sp.org/tlog-proof.
19+
func FormatProof(idx int64, p tlog.RecordProof, signedCheckpoint []byte) []byte {
20+
return formatProof(idx, p, signedCheckpoint, nil, false)
21+
}
22+
23+
// FormatProofWithHint formats a tlog record inclusion proof (a "spicy signature")
24+
// for the record at index idx with proof p and signed checkpoint signedCheckpoint,
25+
// including a hint.
26+
//
27+
// The returned byte slice is encoded according to c2sp.org/tlog-proof.
28+
func FormatProofWithHint(idx int64, hint []byte, p tlog.RecordProof, signedCheckpoint []byte) []byte {
29+
return formatProof(idx, p, signedCheckpoint, hint, true)
30+
}
31+
32+
func formatProof(idx int64, p tlog.RecordProof, signedCheckpoint []byte, hint []byte, withHint bool) []byte {
33+
var buf bytes.Buffer
34+
fmt.Fprintf(&buf, "c2sp.org/tlog-proof\n")
35+
fmt.Fprintf(&buf, "index %d\n", idx)
36+
if withHint {
37+
fmt.Fprintf(&buf, "hint %s\n", base64.StdEncoding.EncodeToString([]byte(hint)))
38+
}
39+
for _, h := range p {
40+
fmt.Fprintf(&buf, "%s\n", h)
41+
}
42+
fmt.Fprintf(&buf, "\n")
43+
buf.Write(signedCheckpoint)
44+
return buf.Bytes()
45+
}
46+
47+
// VerifyRecordError is returned by [VerifyProof] when the inclusion proof does
48+
// not verify. It can be used to diagnose the issue or print a better error
49+
// message. All of its fields are unauthenticated and must not be trusted.
50+
type VerifyRecordError struct {
51+
Index int64
52+
Hint []byte
53+
}
54+
55+
func (e *VerifyRecordError) Error() string {
56+
return fmt.Sprintf("tlog record inclusion proof verification failed for index %d", e.Index)
57+
}
58+
59+
// VerifyProof verifies a proof (a "spicy signature" encoded according to
60+
// c2sp.org/tlog-proof) for a record hash rh (generally produced with
61+
// [tlog.RecordHash]).
62+
//
63+
// The origin must match the log's origin, and the open function will be used to
64+
// verify the signed checkpoint included in the proof. If open returns an error,
65+
// it is returned directly. If the proof is valid but does not verify the record
66+
// hash rh at the given index, a *[VerifyRecordError] is returned.
67+
func VerifyProof(origin string, open func([]byte) (*note.Note, error), rh tlog.Hash, proof []byte) error {
68+
hdr, rest, ok := strings.Cut(string(proof), "\n")
69+
if !ok || hdr != "c2sp.org/tlog-proof" {
70+
return errors.New("malformed tlog proof: missing header, this may not be a tlog proof")
71+
}
72+
rest, ok = strings.CutPrefix(rest, "index ")
73+
if !ok {
74+
return errors.New("malformed tlog proof")
75+
}
76+
s, rest, ok := strings.Cut(rest, "\n")
77+
if !ok {
78+
return errors.New("malformed tlog proof")
79+
}
80+
idx, err := strconv.ParseInt(s, 10, 64)
81+
if err != nil {
82+
return fmt.Errorf("malformed tlog proof: invalid index: %w", err)
83+
}
84+
if idx < 0 {
85+
return fmt.Errorf("malformed tlog proof: negative index")
86+
}
87+
var hint []byte
88+
if strings.HasPrefix(rest, "hint ") {
89+
rest, ok = strings.CutPrefix(rest, "hint ")
90+
if !ok {
91+
return errors.New("malformed tlog proof")
92+
}
93+
s, rest, ok = strings.Cut(rest, "\n")
94+
if !ok {
95+
return errors.New("malformed tlog proof")
96+
}
97+
hint, err = base64.StdEncoding.DecodeString(s)
98+
if err != nil {
99+
return fmt.Errorf("malformed tlog proof: invalid hint: %w", err)
100+
}
101+
}
102+
var p tlog.RecordProof
103+
for {
104+
var h64 string
105+
h64, rest, ok = strings.Cut(rest, "\n")
106+
if !ok {
107+
return errors.New("malformed tlog proof")
108+
}
109+
if h64 == "" {
110+
break
111+
}
112+
h, err := base64.StdEncoding.DecodeString(h64)
113+
if err != nil {
114+
return fmt.Errorf("malformed tlog proof: invalid hash: %w", err)
115+
}
116+
if len(h) != tlog.HashSize {
117+
return fmt.Errorf("malformed tlog proof: invalid hash length: got %d, want 32", len(h))
118+
}
119+
p = append(p, tlog.Hash(h))
120+
}
121+
// Peek at the origin, if it's wrong, opening will likely fail.
122+
if s, _, _ := strings.Cut(rest, "\n"); s != origin {
123+
return fmt.Errorf("proof origin mismatch: got %q, want %q", s, origin)
124+
}
125+
n, err := open([]byte(rest))
126+
if err != nil {
127+
return err
128+
}
129+
c, err := ParseCheckpoint(n.Text)
130+
if err != nil {
131+
return fmt.Errorf("invalid checkpoint in proof: %w", err)
132+
}
133+
if c.Origin != origin {
134+
return fmt.Errorf("checkpoint origin mismatch: got %q, want %q", c.Origin, origin)
135+
}
136+
if err := tlog.CheckRecord(p, c.N, c.Hash, idx, rh); err != nil {
137+
return &VerifyRecordError{
138+
Index: idx,
139+
Hint: hint,
140+
}
141+
}
142+
return nil
143+
}
144+
145+
// HintFromProof extracts the hint from a tlog proof encoded according to
146+
// c2sp.org/tlog-proof. If no hint is present, it returns an error.
147+
//
148+
// The hint is unauthenticated and must not be trusted.
149+
func HintFromProof(proof []byte) ([]byte, error) {
150+
hdr, rest, ok := strings.Cut(string(proof), "\n")
151+
if !ok || hdr != "c2sp.org/tlog-proof" {
152+
return nil, errors.New("malformed tlog proof: missing header, this may not be a tlog proof")
153+
}
154+
line, rest, ok := strings.Cut(rest, "\n")
155+
if !ok || !strings.HasPrefix(line, "index ") {
156+
return nil, errors.New("malformed tlog proof")
157+
}
158+
line, _, ok = strings.Cut(rest, "\n")
159+
if !ok {
160+
return nil, errors.New("malformed tlog proof")
161+
}
162+
s, ok := strings.CutPrefix(line, "hint ")
163+
if !ok {
164+
return nil, errors.New("tlog proof does not contain a hint")
165+
}
166+
hint, err := base64.StdEncoding.DecodeString(s)
167+
if err != nil {
168+
return nil, fmt.Errorf("malformed tlog proof: invalid hint: %w", err)
169+
}
170+
return hint, nil
171+
}

0 commit comments

Comments
 (0)