|
| 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