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