Skip to content

Commit 3a43473

Browse files
Hayden-IOmhutchinsonroger2hkAlCutter
authored
Copy witness policy file format code from Tessera (#229)
* Defined witness policy configuration (#488) This allows the required witnesses to be defined and the theshold policies that apply within each group. Arbitrarily nested structures can be built, each with different numbers of signatures. Each WitnessGroup provides the URLs at which the witness can be reached to perform witnessing, and a function that determines if the group is satisfied. This format is consistent with the only other known witness policy configuration format out there: https://git.glasklar.is/sigsum/core/sigsum-go/-/blob/main/doc/policy.md Towards #309. * Implementing witnessing code and API (#494) Towards #309. * [Witnessing] Check responses for valid signatures (#500) This now verifies the body of 200 responses. It checks that the note can be verified using the signature, and then returns only the signature that the log has a verifier for. This means that witnesses that return a valid signature and then a load of other signatures will not be able to pollute the checkpoint with these other signatures. On the other hand, it means we will need to consider how to support witness key rotation in Tessera in the future. There are a few ways to solve this, but I don't believe this approach blocks any of them. * Replace m[k]=v loop with `maps.Copy` (#533) * Remove unused things from API (#536) Duplicate docs instead of linking to private docs * Slight API pruning and modernization (#609) A few bits to clean up as we approach a beta release: - Pruned utility method from API - Renamed IntegrationAwaiter to PublicationAwaiter - Modernized some older go idioms * Replace all `Url` with `URL` (#636) * Follow rename to Tessera #645 * Witness policy (#755) This PR adds support for constructing a graph of WitnessGroup/Witness structs which represent the policy defined in a config file complying with the spec here: https://git.glasklar.is/sigsum/core/sigsum-go/-/blob/main/doc/policy.md * Witness submission prefix is a config item. (#756) * Fix some witness related nits (#757) * Read witness policy from []byte (#758) * Check policy handles whitespace and comments ok (#788) * Bump formats to 404c0d5 (#791) * Bump formats to 404c0d5 & tidy * Add 0x04 key to policy tests * Clarify witness notes (#827) Makes it easier to find the option to configure witnesses using a policy file, and clarifies that it expects vkey cosig keys for witnesses. * Move to dedicated package Signed-off-by: Hayden <8418760+Hayden-IO@users.noreply.github.com> * Update function and struct names to align with style guide Signed-off-by: Hayden <8418760+Hayden-IO@users.noreply.github.com> --------- Signed-off-by: Hayden <8418760+Hayden-IO@users.noreply.github.com> Co-authored-by: Martin Hutchinson <mhutchinson@gmail.com> Co-authored-by: Roger Ng <rogerng@google.com> Co-authored-by: Al Cutter <al@google.com>
1 parent 220fcc4 commit 3a43473

3 files changed

Lines changed: 693 additions & 0 deletions

File tree

witness/witness.go

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
// Copyright 2025 The Tessera authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package witness
16+
17+
import (
18+
"bufio"
19+
"bytes"
20+
"fmt"
21+
"net/url"
22+
"strconv"
23+
"strings"
24+
25+
"maps"
26+
27+
f_note "github.com/transparency-dev/formats/note"
28+
"golang.org/x/mod/sumdb/note"
29+
)
30+
31+
// policyComponent describes a component that makes up a policy. This is either a
32+
// single Witness, or a WitnessGroup.
33+
type policyComponent interface {
34+
// Satisfied returns true if the checkpoint is signed by the quorum of
35+
// witnesses involved in this policy component.
36+
Satisfied(cp []byte) bool
37+
38+
// Endpoints returns the details required for updating a witness and checking the
39+
// response. The returned result is a map from the URL that should be used to update
40+
// the witness with a new checkpoint, to the value which is the verifier to check
41+
// the response is well formed.
42+
Endpoints() map[string]note.Verifier
43+
}
44+
45+
// ParsePolicy creates a graph of witness objects that represents the
46+
// policy provided.
47+
//
48+
// The policy structure is as described by [Sigsum's policy format](https://git.glasklar.is/sigsum/core/sigsum-go/-/blob/main/doc/policy.md)
49+
// but with the difference that the configured witness keys MUST be signature type `0x04` `vkey`s as specified
50+
// by C2SP [signed-note](https://github.com/C2SP/C2SP/blob/main/signed-note.md#verifier-keys).
51+
func ParsePolicy(p []byte) (Group, error) {
52+
scanner := bufio.NewScanner(bytes.NewBuffer(p))
53+
components := make(map[string]policyComponent)
54+
55+
var quorumName string
56+
for scanner.Scan() {
57+
line := strings.TrimSpace(scanner.Text())
58+
if i := strings.Index(line, "#"); i >= 0 {
59+
line = line[:i]
60+
}
61+
if line == "" {
62+
continue
63+
}
64+
65+
switch fields := strings.Fields(line); fields[0] {
66+
case "log":
67+
// This keyword is important to clients who might use the policy file, but we don't need to know about it since
68+
// we _are_ the log, so just ignore it.
69+
case "witness":
70+
// Strictly, the URL is optional so policy files can be used client-side, where they don't care about the URL.
71+
// Given this function is parsing to create the graph structure which will be used by a Tessera log to witness
72+
// new checkpoints we'll ignore that special case here.
73+
if len(fields) != 4 {
74+
return Group{}, fmt.Errorf("invalid witness definition: %q", line)
75+
}
76+
name, vkey, witnessURLStr := fields[1], fields[2], fields[3]
77+
if isBadName(name) {
78+
return Group{}, fmt.Errorf("invalid witness name %q", name)
79+
}
80+
if _, ok := components[name]; ok {
81+
return Group{}, fmt.Errorf("duplicate component name: %q", name)
82+
}
83+
witnessURL, err := url.Parse(witnessURLStr)
84+
if err != nil {
85+
return Group{}, fmt.Errorf("invalid witness URL %q: %w", witnessURLStr, err)
86+
}
87+
w, err := New(vkey, witnessURL)
88+
if err != nil {
89+
return Group{}, fmt.Errorf("invalid witness config %q: %w", line, err)
90+
}
91+
components[name] = w
92+
case "group":
93+
if len(fields) < 3 {
94+
return Group{}, fmt.Errorf("invalid group definition: %q", line)
95+
}
96+
97+
name, N, childrenNames := fields[1], fields[2], fields[3:]
98+
if isBadName(name) {
99+
return Group{}, fmt.Errorf("invalid group name %q", name)
100+
}
101+
if _, ok := components[name]; ok {
102+
return Group{}, fmt.Errorf("duplicate component name: %q", name)
103+
}
104+
var n int
105+
switch N {
106+
case "any":
107+
n = 1
108+
case "all":
109+
n = len(childrenNames)
110+
default:
111+
i, err := strconv.ParseUint(N, 10, 8)
112+
if err != nil {
113+
return Group{}, fmt.Errorf("invalid threshold %q for group %q: %w", N, name, err)
114+
}
115+
n = int(i)
116+
}
117+
if c := len(childrenNames); n > c {
118+
return Group{}, fmt.Errorf("group with %d children cannot have threshold %d", c, n)
119+
}
120+
121+
children := make([]policyComponent, len(childrenNames))
122+
for i, cName := range childrenNames {
123+
if isBadName(cName) {
124+
return Group{}, fmt.Errorf("invalid component name %q", cName)
125+
}
126+
child, ok := components[cName]
127+
if !ok {
128+
return Group{}, fmt.Errorf("unknown component %q in group definition", cName)
129+
}
130+
children[i] = child
131+
}
132+
wg := NewGroup(n, children...)
133+
components[name] = wg
134+
case "quorum":
135+
if len(fields) != 2 {
136+
return Group{}, fmt.Errorf("invalid quorum definition: %q", line)
137+
}
138+
quorumName = fields[1]
139+
default:
140+
return Group{}, fmt.Errorf("unknown keyword: %q", fields[0])
141+
}
142+
}
143+
if err := scanner.Err(); err != nil {
144+
return Group{}, err
145+
}
146+
147+
switch quorumName {
148+
case "":
149+
return Group{}, fmt.Errorf("policy file must define a quorum")
150+
case "none":
151+
return NewGroup(0), nil
152+
default:
153+
if isBadName(quorumName) {
154+
return Group{}, fmt.Errorf("invalid quorum name %q", quorumName)
155+
}
156+
policy, ok := components[quorumName]
157+
if !ok {
158+
return Group{}, fmt.Errorf("quorum component %q not found", quorumName)
159+
}
160+
wg, ok := policy.(Group)
161+
if !ok {
162+
// A single witness can be a policy. Wrap it in a group.
163+
return NewGroup(1, policy), nil
164+
}
165+
return wg, nil
166+
}
167+
}
168+
169+
var keywords = map[string]struct{}{
170+
"witness": {},
171+
"group": {},
172+
"any": {},
173+
"all": {},
174+
"none": {},
175+
"quorum": {},
176+
"log": {},
177+
}
178+
179+
func isBadName(n string) bool {
180+
_, isKeyword := keywords[n]
181+
return isKeyword
182+
}
183+
184+
// New returns a Witness given a verifier key and the root URL for where this
185+
// witness can be reached.
186+
func New(vkey string, witnessRoot *url.URL) (Witness, error) {
187+
v, err := f_note.NewVerifierForCosignatureV1(vkey)
188+
if err != nil {
189+
return Witness{}, err
190+
}
191+
192+
u := witnessRoot.JoinPath("/add-checkpoint")
193+
194+
return Witness{
195+
Key: v,
196+
URL: u.String(),
197+
}, err
198+
}
199+
200+
// Witness represents a single witness that can be reached in order to perform a witnessing operation.
201+
// The URLs() method returns the URL where it can be reached for witnessing, and the Satisfied method
202+
// provides a predicate to check whether this witness has signed a checkpoint.
203+
type Witness struct {
204+
Key note.Verifier
205+
URL string
206+
}
207+
208+
// Satisfied returns true if the checkpoint provided is signed by this witness.
209+
// This will return false if there is no signature, and also if the
210+
// checkpoint cannot be read as a valid note. It is up to the caller to ensure
211+
// that the input value represents a valid note.
212+
func (w Witness) Satisfied(cp []byte) bool {
213+
n, err := note.Open(cp, note.VerifierList(w.Key))
214+
if err != nil {
215+
return false
216+
}
217+
return len(n.Sigs) == 1
218+
}
219+
220+
// Endpoints returns the details required for updating a witness and checking the
221+
// response. The returned result is a map from the URL that should be used to update
222+
// the witness with a new checkpoint, to the value which is the verifier to check
223+
// the response is well formed.
224+
func (w Witness) Endpoints() map[string]note.Verifier {
225+
return map[string]note.Verifier{w.URL: w.Key}
226+
}
227+
228+
// NewGroup creates a grouping of Witness or WitnessGroup with a configurable threshold
229+
// of these sub-components that need to be satisfied in order for this group to be satisfied.
230+
//
231+
// The threshold should only be set to less than the number of sub-components if these are
232+
// considered fungible.
233+
func NewGroup(n int, children ...policyComponent) Group {
234+
if n < 0 || n > len(children) {
235+
panic(fmt.Errorf("threshold of %d outside bounds for children %s", n, children))
236+
}
237+
return Group{
238+
Components: children,
239+
N: n,
240+
}
241+
}
242+
243+
// Group defines a group of witnesses, and a threshold of
244+
// signatures that must be met for this group to be satisfied.
245+
// Witnesses within a group should be fungible, e.g. all of the Armored
246+
// Witness devices form a logical group, and N should be picked to
247+
// represent a threshold of the quorum. For some users this will be a
248+
// simple majority, but other strategies are available.
249+
// N must be <= len(WitnessKeys).
250+
type Group struct {
251+
Components []policyComponent
252+
N int
253+
}
254+
255+
// Satisfied returns true if the checkpoint provided has sufficient signatures
256+
// from the witnesses in this group to satisfy the threshold.
257+
// This will return false if there are insufficient signatures, and also if the
258+
// checkpoint cannot be read as a valid note. It is up to the caller to ensure
259+
// that the input value represents a valid note.
260+
//
261+
// The implementation of this requires every witness in the group to verify the
262+
// checkpoint, which is O(N). If this is called every time a witness returns a
263+
// checkpoint then this algorithm is O(N^2). To support large N, this may require
264+
// some rewriting in order to maintain performance.
265+
func (wg Group) Satisfied(cp []byte) bool {
266+
if wg.N <= 0 {
267+
return true
268+
}
269+
satisfaction := 0
270+
for _, c := range wg.Components {
271+
if c.Satisfied(cp) {
272+
satisfaction++
273+
}
274+
if satisfaction >= wg.N {
275+
return true
276+
}
277+
}
278+
return false
279+
}
280+
281+
// Endpoints returns the details required for updating a witness and checking the
282+
// response. The returned result is a map from the URL that should be used to update
283+
// the witness with a new checkpoint, to the value which is the verifier to check
284+
// the response is well formed.
285+
func (wg Group) Endpoints() map[string]note.Verifier {
286+
endpoints := make(map[string]note.Verifier)
287+
for _, c := range wg.Components {
288+
maps.Copy(endpoints, c.Endpoints())
289+
}
290+
return endpoints
291+
}

0 commit comments

Comments
 (0)