Skip to content

Commit 277820f

Browse files
committed
torchwood: implement c2sp.org/tlog-sig@v1 proof format and verification
1 parent 41992bd commit 277820f

2 files changed

Lines changed: 760 additions & 0 deletions

File tree

spicy.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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-sig@v1.
19+
func FormatProof(idx int64, p tlog.RecordProof, signedCheckpoint []byte) []byte {
20+
return formatProof(idx, p, signedCheckpoint, nil, false)
21+
}
22+
23+
// FormatProofWithExtraData formats a tlog record inclusion proof (a "spicy
24+
// signature") for the record at index idx with proof p and signed checkpoint
25+
// signedCheckpoint, including extra data.
26+
//
27+
// The returned byte slice is encoded according to c2sp.org/tlog-sig@v1.
28+
func FormatProofWithExtraData(idx int64, extra []byte, p tlog.RecordProof, signedCheckpoint []byte) []byte {
29+
return formatProof(idx, p, signedCheckpoint, extra, true)
30+
}
31+
32+
func formatProof(idx int64, p tlog.RecordProof, signedCheckpoint []byte, extra []byte, withExtra bool) []byte {
33+
var buf bytes.Buffer
34+
fmt.Fprintf(&buf, "c2sp.org/tlog-sig@v1\n")
35+
if withExtra {
36+
fmt.Fprintf(&buf, "extra %s\n", base64.StdEncoding.EncodeToString([]byte(extra)))
37+
}
38+
fmt.Fprintf(&buf, "index %d\n", idx)
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+
Extra []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-sig@v1) 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-sig@v1" {
70+
return errors.New("malformed tlog proof: missing header, this may not be a tlog proof")
71+
}
72+
var extra []byte
73+
if rest, ok = strings.CutPrefix(rest, "extra "); ok {
74+
var s string
75+
s, rest, ok = strings.Cut(rest, "\n")
76+
if !ok {
77+
return errors.New("malformed tlog proof: unexpected end of extra line")
78+
}
79+
var err error
80+
extra, err = base64.StdEncoding.DecodeString(s)
81+
if err != nil {
82+
return fmt.Errorf("malformed tlog proof: invalid extra: %w", err)
83+
}
84+
}
85+
rest, ok = strings.CutPrefix(rest, "index ")
86+
if !ok {
87+
return errors.New("malformed tlog proof: expected index line")
88+
}
89+
s, rest, ok := strings.Cut(rest, "\n")
90+
if !ok {
91+
return errors.New("malformed tlog proof: unexpected end of index line")
92+
}
93+
idx, err := strconv.ParseInt(s, 10, 64)
94+
if err != nil {
95+
return fmt.Errorf("malformed tlog proof: invalid index: %w", err)
96+
}
97+
if idx < 0 {
98+
return fmt.Errorf("malformed tlog proof: negative index")
99+
}
100+
var p tlog.RecordProof
101+
for {
102+
var h64 string
103+
h64, rest, ok = strings.Cut(rest, "\n")
104+
if !ok {
105+
return errors.New("malformed tlog proof: unexpected end of proof lines")
106+
}
107+
if h64 == "" {
108+
break
109+
}
110+
h, err := base64.StdEncoding.DecodeString(h64)
111+
if err != nil {
112+
return fmt.Errorf("malformed tlog proof: invalid hash: %w", err)
113+
}
114+
if len(h) != tlog.HashSize {
115+
return fmt.Errorf("malformed tlog proof: invalid hash length: got %d, want 32", len(h))
116+
}
117+
p = append(p, tlog.Hash(h))
118+
}
119+
// Peek at the origin, if it's wrong, opening will likely fail.
120+
if s, _, _ := strings.Cut(rest, "\n"); s != origin {
121+
return fmt.Errorf("proof origin mismatch: got %q, want %q", s, origin)
122+
}
123+
n, err := open([]byte(rest))
124+
if err != nil {
125+
return err
126+
}
127+
c, err := ParseCheckpoint(n.Text)
128+
if err != nil {
129+
return fmt.Errorf("invalid checkpoint in proof: %w", err)
130+
}
131+
if c.Origin != origin {
132+
return fmt.Errorf("checkpoint origin mismatch: got %q, want %q", c.Origin, origin)
133+
}
134+
if err := tlog.CheckRecord(p, c.N, c.Hash, idx, rh); err != nil {
135+
return &VerifyRecordError{
136+
Index: idx,
137+
Extra: extra,
138+
}
139+
}
140+
return nil
141+
}
142+
143+
// ProofExtraData extracts the extra data from a tlog proof encoded according to
144+
// c2sp.org/tlog-sig@v1. If no extra data is present, it returns an error.
145+
//
146+
// The extra data is unauthenticated and must not be trusted.
147+
func ProofExtraData(proof []byte) ([]byte, error) {
148+
hdr, rest, ok := strings.Cut(string(proof), "\n")
149+
if !ok || hdr != "c2sp.org/tlog-sig@v1" {
150+
return nil, errors.New("malformed tlog proof: missing header, this may not be a tlog proof")
151+
}
152+
line, _, ok := strings.Cut(rest, "\n")
153+
if !ok {
154+
return nil, errors.New("malformed tlog proof: unexpected end of proof")
155+
}
156+
s, ok := strings.CutPrefix(line, "extra ")
157+
if !ok {
158+
return nil, errors.New("tlog proof does not contain extra data")
159+
}
160+
extra, err := base64.StdEncoding.DecodeString(s)
161+
if err != nil {
162+
return nil, fmt.Errorf("malformed tlog proof: invalid extra: %w", err)
163+
}
164+
return extra, nil
165+
}

0 commit comments

Comments
 (0)