1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
|
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package torchwood
import (
"encoding/base64"
"errors"
"fmt"
"strconv"
"strings"
"golang.org/x/mod/sumdb/note"
"golang.org/x/mod/sumdb/tlog"
)
const maxCheckpointSize = 1e6
// A Checkpoint is a tree head to be formatted according to c2sp.org/checkpoint.
//
// A checkpoint looks like this:
//
// example.com/origin
// 923748
// nND/nri/U0xuHUrYSy0HtMeal2vzD9V4k/BO79C+QeI=
//
// It can be followed by extra extension lines.
type Checkpoint struct {
Origin string
tlog.Tree
// Extension is empty or a sequence of non-empty lines,
// each terminated by a newline character.
Extension string
}
// ParseCheckpoint parses a c2sp.org/tlog-checkpoint payload without signatures.
func ParseCheckpoint(text string) (Checkpoint, error) {
// This is an extended version of tlog.ParseTree.
if strings.Count(text, "\n") < 3 || len(text) > maxCheckpointSize {
return Checkpoint{}, errors.New("malformed checkpoint")
}
if !strings.HasSuffix(text, "\n") {
return Checkpoint{}, errors.New("malformed checkpoint")
}
lines := strings.SplitN(text, "\n", 4)
n, err := strconv.ParseInt(lines[1], 10, 64)
if err != nil || n < 0 || lines[1] != strconv.FormatInt(n, 10) {
return Checkpoint{}, errors.New("malformed checkpoint")
}
h, err := base64.StdEncoding.DecodeString(lines[2])
if err != nil || len(h) != tlog.HashSize {
return Checkpoint{}, errors.New("malformed checkpoint")
}
rest := lines[3]
for rest != "" {
before, after, found := strings.Cut(rest, "\n")
if before == "" || !found {
return Checkpoint{}, errors.New("malformed checkpoint")
}
rest = after
}
var hash tlog.Hash
copy(hash[:], h)
return Checkpoint{lines[0], tlog.Tree{N: n, Hash: hash}, lines[3]}, nil
}
func (c Checkpoint) String() string {
return fmt.Sprintf("%s\n%d\n%s\n%s",
c.Origin,
c.N,
base64.StdEncoding.EncodeToString(c.Hash[:]),
c.Extension,
)
}
type unverifiedNoteError struct {
err error
n *note.Note
}
func (e *unverifiedNoteError) Error() string {
return fmt.Sprintf("note verification failed: %v", e.err)
}
func (e *unverifiedNoteError) Unwrap() []error {
return []error{e.err, ¬e.UnverifiedNoteError{Note: e.n}}
}
// VerifyCheckpoint parses and verifies a signed c2sp.org/tlog-checkpoint.
//
// If the note signatures do not satisfy the provided policy, an error wrapping
// *[note.UnverifiedNoteError] is returned.
func VerifyCheckpoint(signedCheckpoint []byte, policy Policy) (Checkpoint, *note.Note, error) {
n, err := note.Open(signedCheckpoint, policy)
if err != nil {
return Checkpoint{}, nil, err
}
c, err := ParseCheckpoint(n.Text)
if err != nil {
return Checkpoint{}, nil, fmt.Errorf("parsing checkpoint: %v", err)
}
if err := policy.Check(c.Origin, n.Sigs); err != nil {
return Checkpoint{}, nil, &unverifiedNoteError{err: err, n: n}
}
// Check that at least one component of the policy checked the origin.
if err := policy.Check("check.invalid", n.Sigs); err == nil {
return Checkpoint{}, nil, errors.New("policy is not checking the checkpoint origin")
}
return c, n, nil
}
|