-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidator.go
More file actions
283 lines (249 loc) · 8.7 KB
/
Copy pathvalidator.go
File metadata and controls
283 lines (249 loc) · 8.7 KB
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// Package validator provides [validator.Validator], which can validate a
// [ucan.Invocation].
package validator
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"github.com/fil-forge/ucantone/did"
"github.com/fil-forge/ucantone/ipld/datamodel"
"github.com/fil-forge/ucantone/ucan"
"github.com/fil-forge/ucantone/ucan/token"
verrs "github.com/fil-forge/ucantone/validator/errors"
"github.com/fil-forge/ucantone/varsig/algorithm/nonstandard"
"github.com/ipfs/go-cid"
)
// ValidateInvocation determines whether an [ucan.Invocation] is a valid request
// to execute a task. If an invocation is valid, its audience is expected to
// execute its task. If an invocation is invalid, its audience is expected to
// reject the request.
func ValidateInvocation(
ctx context.Context,
inv ucan.Invocation,
options ...Option,
) error {
cfg := makeCfg(options...)
// To be valid, an invocation must be a valid token...
err := ValidateToken(ctx, inv, withConfig(cfg))
if err != nil {
return err
}
// ...and have a valid proof chain...
cap, err := capabilityFromProofChain(ctx, inv, cfg)
if err != nil {
return err
}
// ...and have the capability to perform its task under the proof chain.
var mapArgs datamodel.Map
err = mapArgs.UnmarshalCBOR(bytes.NewReader(inv.ArgumentsBytes()))
if err != nil {
return fmt.Errorf("decoding invocation arguments for capability check: %w", err)
}
err = cap.Allows(
inv.Subject(),
inv.Command(),
mapArgs,
)
if err != nil {
return err
}
return nil
}
// ValidateToken determines whether a [ucan.Token] is a valid UCAN token. To be
// valid, a token must have a valid signature from its issuer and be within its
// time bounds. An [ucan.Invocation] is a token, but has additional
// requirements. An invocation may be a valid token but still an invalid
// invocation, if its proof chain is insufficient.
func ValidateToken(
ctx context.Context,
tok ucan.Token,
options ...Option,
) error {
cfg := makeCfg(options...)
// To be valid, a token must have a valid signature from its issuer...
err := verifyTokenSignature(ctx, tok, cfg)
if err != nil {
return err
}
// ...and not be expired...
err = ValidateNotExpired(tok, cfg.validationTime)
if err != nil {
return err
}
// ...and not be too early.
if dlg, ok := tok.(ucan.Delegation); ok {
// Currently, only delegations have a "not before" time bound in this
// library. But the spec is unclear as to whether all tokens should have
// them, so this check is left in this function for now.
//
// https://github.com/ucan-wg/invocation/issues/45
err = ValidateNotTooEarly(dlg, cfg.validationTime)
if err != nil {
return err
}
}
return nil
}
// verifyTokenSignature verifies the token was signed by the passed verifier.
func verifyTokenSignature(ctx context.Context, tok ucan.Token, cfg validationConfig) error {
if tok.Signature().Header().SignatureAlgorithm() == nonstandard.NonStandard {
return cfg.verifyNonStandardSignature(ctx, tok, cfg.metadata)
}
doc, err := cfg.didResolver.Resolve(ctx, tok.Issuer())
if err != nil {
return err
}
// Look at the correct verification relationship in the DID Document.
var verRel *did.VerificationRelationship
switch tok.(type) {
case ucan.Invocation:
verRel = doc.CapabilityInvocation
case ucan.Delegation:
verRel = doc.CapabilityDelegation
default:
return fmt.Errorf("unsupported token type: %T", tok)
}
// The capability relationships are optional (DID core §5.3): a document
// that expresses one restricts verification to the methods it lists, but
// a document that expresses nothing authorizes all of its verification
// methods — e.g. did:plc documents carry only verificationMethod.
// (Post-parse, an explicitly empty relationship is indistinguishable
// from an absent one and gets the same fallback.)
var vms []did.VerificationMethod
if verRel != nil {
vms = verRel.All()
}
if len(vms) == 0 && doc.VerificationMethods != nil {
for _, vm := range *doc.VerificationMethods {
vms = append(vms, vm)
}
}
// Try each verification method, collecting rejection reasons for the error.
validationTime := time.Unix(int64(cfg.validationTime), 0)
var rejections []verrs.VMRejection
for _, vm := range vms {
if vm.ExpiredAt(validationTime) {
rejections = append(rejections, verrs.VMRejection{VM: vm, Reason: "expired"})
continue
}
if vm.RevokedAt(validationTime) {
rejections = append(rejections, verrs.VMRejection{VM: vm, Reason: "revoked"})
continue
}
f, ok := cfg.verifierFactories[vm.Type]
if !ok {
err = fmt.Errorf("%w for VM type %q", ErrNoVerifierFactory, vm.Type)
}
var v ucan.Verifier
if err == nil {
v, err = f(ctx, vm.Material)
}
if errors.Is(err, ErrNoVerifierFactory) {
rejections = append(rejections, verrs.VMRejection{VM: vm, Reason: "unsupported verification method type"})
continue
}
if err != nil {
return err
}
if token.VerifySignature(tok, v) {
return nil
}
rejections = append(rejections, verrs.VMRejection{VM: vm, Reason: "signature mismatch"})
}
return verrs.NewInvalidSignatureError(tok, rejections)
}
func capabilityFromProofChain(ctx context.Context, inv ucan.Invocation, cfg validationConfig) (Capability, error) {
prfs := make([]ucan.Delegation, 0, len(inv.Proofs()))
for _, p := range inv.Proofs() {
prf, err := cfg.resolveProof(ctx, p)
if err != nil {
return Capability{}, err
}
prfs = append(prfs, prf)
}
currentAuthority := inv.Subject()
currentCapability := NewCapability(inv.Subject())
for i, prf := range prfs {
if err := ValidateToken(ctx, prf, withConfig(cfg)); err != nil {
return Capability{}, err
}
// The first proof must have a non-null subject (that is, may not be a
// powerline delegation).
//
// https://github.com/ucan-wg/delegation#powerline
if i == 0 && prf.Subject() == did.Undef {
return Capability{}, verrs.NewInvalidClaimError("root delegation subject is null")
}
// Every proof's subject must match the invocation's subject, or be null
// (a powerline delegation).
if prf.Subject() != did.Undef && prf.Subject() != inv.Subject() {
return Capability{}, verrs.NewSubjectAlignmentError(inv.Subject(), prf)
}
// Every proof's issuer must match the previous proof's audience (or the
// invocation's subject, for the first proof).
if prf.Issuer() != currentAuthority {
return Capability{}, verrs.NewPrincipalAlignmentError(currentAuthority, prf)
}
currentAuthority = prf.Audience()
var err error
currentCapability, err = currentCapability.Attenuate(prf.Command(), prf.Policy())
if err != nil {
return Capability{}, err
}
}
if currentAuthority != inv.Issuer() {
if len(prfs) == 0 {
// The spec fixtures call this out as a different error case from a
// principal alignment error (`InvalidAudience`).
return Capability{}, verrs.NewInvalidClaimError(fmt.Sprintf("invocation %s is not issued by subject and has no proofs", inv.Link()))
}
return Capability{}, verrs.NewPrincipalAlignmentError(currentAuthority, inv)
}
return currentCapability, nil
}
// ProofResolverFunc finds a delegation corresponding to an external proof link.
type ProofResolverFunc func(ctx context.Context, link cid.Cid) (ucan.Delegation, error)
// NonStandardSignatureVerifierFunc is used to verify signatures from
// non-standard signature algorithms. It can be passed into a UCAN validator in
// order to support delegations signed with non-standard signature algorithms.
type NonStandardSignatureVerifierFunc func(ctx context.Context, token ucan.Token, meta ucan.Container) error
// ProofUnavailable is a [ProofResolverFunc] that always fails.
func ProofUnavailable(ctx context.Context, p cid.Cid) (ucan.Delegation, error) {
return nil, verrs.NewUnavailableProofError(p, errors.New("no proof resolver configured"))
}
// FailNonStandardSignatureVerification is a [NonStandardSignatureVerifierFunc]
// that always fails.
func FailNonStandardSignatureVerification(ctx context.Context, token ucan.Token, meta ucan.Container) error {
return verrs.NewUnverifiableSignatureError(token, errors.New("no non-standard signature verifier configured"))
}
func ProofsFromContainer(c ucan.Container) ProofResolverFunc {
return func(ctx context.Context, l cid.Cid) (ucan.Delegation, error) {
prf, ok := c.Delegation(l)
if !ok {
return nil, verrs.NewUnavailableProofError(l, errors.New("proof not found in container"))
}
return prf, nil
}
}
func ValidateNotExpired(token ucan.Token, now ucan.UnixTimestamp) error {
exp := token.Expiration()
if exp == nil {
return nil
}
if *exp <= now {
return verrs.NewExpiredError(token)
}
return nil
}
func ValidateNotTooEarly(dlg ucan.Delegation, now ucan.UnixTimestamp) error {
nbf := dlg.NotBefore()
if nbf == nil {
return nil
}
if *nbf != 0 && now <= *nbf {
return verrs.NewTooEarlyError(dlg)
}
return nil
}