File: checkpoint.go

package info (click to toggle)
torchwood 0.9.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 632 kB
  • sloc: python: 237; sql: 135; sh: 17; makefile: 14
file content (118 lines) | stat: -rw-r--r-- 3,213 bytes parent folder | download | duplicates (2)
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, &note.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
}