Skip to content

Commit a172e7e

Browse files
authored
feat: make time bounds validation configurable (#78)
Expiration and Not Valid Before are useful when validating an invocation as part of deciding whether it should be executed. However, if we just want to check the validity of a given delegation chain that happened in the past, we don't care about time bounds. These changes allow passing a custom time bounds validation function to decide how expiration and not valid before are validated.
1 parent 9404c01 commit a172e7e

File tree

6 files changed

+120
-6
lines changed

6 files changed

+120
-6
lines changed

server/handler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func Provide[C any, O ipld.Builder, X failure.IPLDBuilderFailure](
3737
ictx.ResolveProof,
3838
ictx.ParsePrincipal,
3939
ictx.ResolveDIDKey,
40+
ictx.ValidateTimeBounds,
4041
ictx.AuthorityProofs()...,
4142
)
4243

server/options.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type srvConfig struct {
2525
resolveProof validator.ProofResolverFunc
2626
parsePrincipal validator.PrincipalParserFunc
2727
resolveDIDKey validator.PrincipalResolverFunc
28+
validateTimeBounds validator.TimeBoundsValidatorFunc
2829
authorityProofs []delegation.Delegation
2930
altAudiences []ucan.Principal
3031
catch ErrorHandlerFunc
@@ -123,6 +124,14 @@ func WithPrincipalResolver(fn validator.PrincipalResolverFunc) Option {
123124
}
124125
}
125126

127+
// WithTimeBoundsValidator configures a function that validates the time bounds of a delegation.
128+
func WithTimeBoundsValidator(fn validator.TimeBoundsValidatorFunc) Option {
129+
return func(cfg *srvConfig) error {
130+
cfg.validateTimeBounds = fn
131+
return nil
132+
}
133+
}
134+
126135
// WithAuthorityProofs allows to provide a list of proofs that designate other
127136
// principals (beyond the service authority) whose attestations will be recognized as valid.
128137
func WithAuthorityProofs(proofs ...delegation.Delegation) Option {

server/server.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type InvocationContext interface {
3737
validator.ProofResolver
3838
validator.PrincipalParser
3939
validator.PrincipalResolver
40+
validator.TimeBoundsValidator
4041
validator.AuthorityProver
4142
// ID is the DID of the service the invocation was sent to.
4243
ID() principal.Signer
@@ -137,7 +138,12 @@ func NewServer(id principal.Signer, options ...Option) (ServerView[Service], err
137138
resolveDIDKey = validator.FailDIDKeyResolution
138139
}
139140

140-
ctx := serverContext{id, canIssue, validateAuthorization, resolveProof, parsePrincipal, resolveDIDKey, cfg.authorityProofs, cfg.altAudiences}
141+
validateTimeBounds := cfg.validateTimeBounds
142+
if validateTimeBounds == nil {
143+
validateTimeBounds = validator.NotExpiredNotTooEarly
144+
}
145+
146+
ctx := serverContext{id, canIssue, validateAuthorization, resolveProof, parsePrincipal, resolveDIDKey, validateTimeBounds, cfg.authorityProofs, cfg.altAudiences}
141147
svr := &server{id, cfg.service, ctx, codec, catch, cfg.logReceipt}
142148
return svr, nil
143149
}
@@ -154,6 +160,7 @@ type serverContext struct {
154160
resolveProof validator.ProofResolverFunc
155161
parsePrincipal validator.PrincipalParserFunc
156162
resolveDIDKey validator.PrincipalResolverFunc
163+
validateTimeBounds validator.TimeBoundsValidatorFunc
157164
authorityProofs []delegation.Delegation
158165
altAudiences []ucan.Principal
159166
}
@@ -182,6 +189,10 @@ func (sctx serverContext) ResolveDIDKey(ctx context.Context, did did.DID) (did.D
182189
return sctx.resolveDIDKey(ctx, did)
183190
}
184191

192+
func (sctx serverContext) ValidateTimeBounds(dlg delegation.Delegation) validator.InvalidProof {
193+
return sctx.validateTimeBounds(dlg)
194+
}
195+
185196
func (sctx serverContext) AuthorityProofs() []delegation.Delegation {
186197
return sctx.authorityProofs
187198
}

validator/lib.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ func FailDIDKeyResolution(ctx context.Context, d did.DID) (did.DID, UnresolvedDI
3333
return did.Undef, NewDIDKeyResolutionError(d, fmt.Errorf("no DID resolver configured"))
3434
}
3535

36+
func NotExpiredNotTooEarly(dlg delegation.Delegation) InvalidProof {
37+
if ucan.IsExpired(dlg) {
38+
return NewExpiredError(dlg)
39+
}
40+
if ucan.IsTooEarly(dlg) {
41+
return NewNotValidBeforeError(dlg)
42+
}
43+
44+
return nil
45+
}
46+
3647
// PrincipalParser provides verifier instances that can validate UCANs issued
3748
// by a given principal.
3849
type PrincipalParser interface {
@@ -108,6 +119,12 @@ type Validator interface {
108119
Authority() principal.Verifier
109120
}
110121

122+
type TimeBoundsValidator interface {
123+
ValidateTimeBounds(dlg delegation.Delegation) InvalidProof
124+
}
125+
126+
type TimeBoundsValidatorFunc func(dlg delegation.Delegation) InvalidProof
127+
111128
type ClaimContext interface {
112129
Validator
113130
RevocationChecker[any]
@@ -116,6 +133,7 @@ type ClaimContext interface {
116133
PrincipalParser
117134
PrincipalResolver
118135
AuthorityProver
136+
TimeBoundsValidator
119137
}
120138

121139
type claimContext struct {
@@ -125,6 +143,7 @@ type claimContext struct {
125143
resolveProof ProofResolverFunc
126144
parsePrincipal PrincipalParserFunc
127145
resolveDIDKey PrincipalResolverFunc
146+
validateTimeBounds TimeBoundsValidatorFunc
128147
authorityProofs []delegation.Delegation
129148
}
130149

@@ -135,6 +154,7 @@ func NewClaimContext(
135154
resolveProof ProofResolverFunc,
136155
parsePrincipal PrincipalParserFunc,
137156
resolveDIDKey PrincipalResolverFunc,
157+
validateTimeBounds TimeBoundsValidatorFunc,
138158
authorityProofs ...delegation.Delegation,
139159
) ClaimContext {
140160
return claimContext{
@@ -144,6 +164,7 @@ func NewClaimContext(
144164
resolveProof,
145165
parsePrincipal,
146166
resolveDIDKey,
167+
validateTimeBounds,
147168
authorityProofs,
148169
}
149170
}
@@ -172,6 +193,10 @@ func (cc claimContext) ResolveDIDKey(ctx context.Context, did did.DID) (did.DID,
172193
return cc.resolveDIDKey(ctx, did)
173194
}
174195

196+
func (cc claimContext) ValidateTimeBounds(dlg delegation.Delegation) InvalidProof {
197+
return cc.validateTimeBounds(dlg)
198+
}
199+
175200
func (cc claimContext) AuthorityProofs() []delegation.Delegation {
176201
return cc.authorityProofs
177202
}
@@ -194,6 +219,7 @@ func NewValidationContext[Caveats any](
194219
resolveProof ProofResolverFunc,
195220
parsePrincipal PrincipalParserFunc,
196221
resolveDIDKey PrincipalResolverFunc,
222+
validateTimeBounds TimeBoundsValidatorFunc,
197223
authorityProofs ...delegation.Delegation,
198224
) ValidationContext[Caveats] {
199225
return validationContext[Caveats]{
@@ -204,6 +230,7 @@ func NewValidationContext[Caveats any](
204230
resolveProof,
205231
parsePrincipal,
206232
resolveDIDKey,
233+
validateTimeBounds,
207234
authorityProofs,
208235
},
209236
capability,
@@ -310,12 +337,10 @@ func ResolveProofs(ctx context.Context, proofs []delegation.Proof, resolver Proo
310337
// Validate a delegation to check it is within the time bound and that it is
311338
// authorized by the issuer.
312339
func Validate(ctx context.Context, dlg delegation.Delegation, prfs []delegation.Delegation, cctx ClaimContext) (delegation.Delegation, InvalidProof) {
313-
if ucan.IsExpired(dlg) {
314-
return nil, NewExpiredError(dlg)
315-
}
316-
if ucan.IsTooEarly(dlg) {
317-
return nil, NewNotValidBeforeError(dlg)
340+
if invalid := cctx.ValidateTimeBounds(dlg); invalid != nil {
341+
return nil, invalid
318342
}
343+
319344
return VerifyAuthorization(ctx, dlg, prfs, cctx)
320345
}
321346

validator/lib_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ func TestAccess(t *testing.T) {
105105
ProofUnavailable,
106106
parseEdPrincipal,
107107
FailDIDKeyResolution,
108+
NotExpiredNotTooEarly,
108109
)
109110

110111
a, x := Access(t.Context(), inv, vctx)
@@ -141,6 +142,7 @@ func TestAccess(t *testing.T) {
141142
ProofUnavailable,
142143
parseEdPrincipal,
143144
FailDIDKeyResolution,
145+
NotExpiredNotTooEarly,
144146
)
145147

146148
a, x := Access(t.Context(), inv, vctx)
@@ -186,6 +188,7 @@ func TestAccess(t *testing.T) {
186188
ProofUnavailable,
187189
parseEdPrincipal,
188190
FailDIDKeyResolution,
191+
NotExpiredNotTooEarly,
189192
)
190193

191194
a, x := Access(t.Context(), inv, vctx)
@@ -237,6 +240,7 @@ func TestAccess(t *testing.T) {
237240
},
238241
parseEdPrincipal,
239242
FailDIDKeyResolution,
243+
NotExpiredNotTooEarly,
240244
)
241245

242246
a, x := Access(t.Context(), inv, vctx)
@@ -251,6 +255,40 @@ func TestAccess(t *testing.T) {
251255
require.Equal(t, fixtures.Alice.DID(), a.Proofs()[0].Issuer().DID())
252256
require.Equal(t, fixtures.Bob.DID(), a.Proofs()[0].Audience().DID())
253257
})
258+
259+
t.Run("expiration and not valid before can be ignored", func(t *testing.T) {
260+
exp := ucan.Now() - 5
261+
nbf := ucan.Now() + 500
262+
inv, err := storeAdd.Invoke(
263+
fixtures.Alice,
264+
fixtures.Service,
265+
fixtures.Alice.DID().String(),
266+
storeAddCaveats{Link: testLink},
267+
delegation.WithExpiration(exp),
268+
delegation.WithNotBefore(nbf),
269+
)
270+
require.NoError(t, err)
271+
272+
vctx := NewValidationContext(
273+
fixtures.Service.Verifier(),
274+
storeAdd,
275+
IsSelfIssued,
276+
validateAuthOk,
277+
ProofUnavailable,
278+
parseEdPrincipal,
279+
FailDIDKeyResolution,
280+
func(dlg delegation.Delegation) InvalidProof {
281+
return nil
282+
},
283+
)
284+
285+
a, x := Access(t.Context(), inv, vctx)
286+
require.NoError(t, x)
287+
require.Equal(t, storeAdd.Can(), a.Capability().Can())
288+
require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With())
289+
require.Equal(t, fixtures.Alice.DID(), a.Issuer().DID())
290+
require.Equal(t, fixtures.Service.DID(), a.Audience().DID())
291+
})
254292
})
255293

256294
t.Run("unauthorized", func(t *testing.T) {
@@ -273,6 +311,7 @@ func TestAccess(t *testing.T) {
273311
ProofUnavailable,
274312
parseEdPrincipal,
275313
FailDIDKeyResolution,
314+
NotExpiredNotTooEarly,
276315
)
277316

278317
a, x := Access(t.Context(), inv, vctx)
@@ -305,6 +344,7 @@ func TestAccess(t *testing.T) {
305344
ProofUnavailable,
306345
parseEdPrincipal,
307346
FailDIDKeyResolution,
347+
NotExpiredNotTooEarly,
308348
)
309349

310350
a, x := Access(t.Context(), inv, vctx)
@@ -337,6 +377,7 @@ func TestAccess(t *testing.T) {
337377
ProofUnavailable,
338378
parseEdPrincipal,
339379
FailDIDKeyResolution,
380+
NotExpiredNotTooEarly,
340381
)
341382

342383
a, x := Access(t.Context(), inv, vctx)
@@ -370,6 +411,7 @@ func TestAccess(t *testing.T) {
370411
ProofUnavailable,
371412
parseEdPrincipal,
372413
FailDIDKeyResolution,
414+
NotExpiredNotTooEarly,
373415
)
374416

375417
a, x := Access(t.Context(), inv, vctx)
@@ -404,6 +446,7 @@ func TestAccess(t *testing.T) {
404446
ProofUnavailable,
405447
parseEdPrincipal,
406448
FailDIDKeyResolution,
449+
NotExpiredNotTooEarly,
407450
)
408451

409452
a, x := Access(t.Context(), inv, vctx)
@@ -447,6 +490,7 @@ func TestAccess(t *testing.T) {
447490
ProofUnavailable,
448491
parseEdPrincipal,
449492
FailDIDKeyResolution,
493+
NotExpiredNotTooEarly,
450494
)
451495

452496
a, x := Access(t.Context(), inv, vctx)
@@ -491,6 +535,7 @@ func TestAccess(t *testing.T) {
491535
ProofUnavailable,
492536
parseEdPrincipal,
493537
FailDIDKeyResolution,
538+
NotExpiredNotTooEarly,
494539
)
495540

496541
a, x := Access(t.Context(), inv, vctx)
@@ -554,6 +599,7 @@ func TestAccess(t *testing.T) {
554599
ProofUnavailable,
555600
parseEdPrincipal,
556601
FailDIDKeyResolution,
602+
NotExpiredNotTooEarly,
557603
)
558604

559605
a, x := Access(t.Context(), inv, vctx)
@@ -597,6 +643,7 @@ func TestAccess(t *testing.T) {
597643
ProofUnavailable,
598644
parseEdPrincipal,
599645
FailDIDKeyResolution,
646+
NotExpiredNotTooEarly,
600647
)
601648

602649
a, x := Access(t.Context(), inv, vctx)
@@ -641,6 +688,7 @@ func TestAccess(t *testing.T) {
641688
ProofUnavailable,
642689
parseEdPrincipal,
643690
FailDIDKeyResolution,
691+
NotExpiredNotTooEarly,
644692
)
645693

646694
a, x := Access(t.Context(), inv, vctx)
@@ -684,6 +732,7 @@ func TestAccess(t *testing.T) {
684732
ProofUnavailable,
685733
parseEdPrincipal,
686734
FailDIDKeyResolution,
735+
NotExpiredNotTooEarly,
687736
)
688737

689738
a, x := Access(t.Context(), inv, vctx)
@@ -727,6 +776,7 @@ func TestAccess(t *testing.T) {
727776
ProofUnavailable,
728777
parseEdPrincipal,
729778
FailDIDKeyResolution,
779+
NotExpiredNotTooEarly,
730780
)
731781

732782
a, x := Access(t.Context(), inv, vctx)
@@ -770,6 +820,7 @@ func TestAccess(t *testing.T) {
770820
ProofUnavailable,
771821
parseEdPrincipal,
772822
FailDIDKeyResolution,
823+
NotExpiredNotTooEarly,
773824
)
774825

775826
a, x := Access(t.Context(), inv, vctx)
@@ -822,6 +873,7 @@ func TestAccess(t *testing.T) {
822873
ProofUnavailable,
823874
parseEdPrincipal,
824875
FailDIDKeyResolution,
876+
NotExpiredNotTooEarly,
825877
)
826878

827879
cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), fixtures.Service.DID(), testLink)
@@ -869,6 +921,7 @@ func TestAccess(t *testing.T) {
869921
ProofUnavailable,
870922
parseEdPrincipal,
871923
FailDIDKeyResolution,
924+
NotExpiredNotTooEarly,
872925
)
873926

874927
cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), fixtures.Alice.DID(), testLink)
@@ -915,6 +968,7 @@ func TestAccess(t *testing.T) {
915968
ProofUnavailable,
916969
parseEdPrincipal,
917970
FailDIDKeyResolution,
971+
NotExpiredNotTooEarly,
918972
)
919973

920974
cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), space.DID(), testLink)
@@ -952,6 +1006,7 @@ func TestClaim(t *testing.T) {
9521006
ProofUnavailable,
9531007
parseEdPrincipal,
9541008
FailDIDKeyResolution,
1009+
NotExpiredNotTooEarly,
9551010
)
9561011

9571012
a, x := Claim(t.Context(), storeAdd, []delegation.Proof{delegation.FromLink(dlg.Link())}, vctx)
@@ -992,6 +1047,7 @@ func TestClaim(t *testing.T) {
9921047
ProofUnavailable,
9931048
parseEdPrincipal,
9941049
FailDIDKeyResolution,
1050+
NotExpiredNotTooEarly,
9951051
)
9961052

9971053
a, x := Claim(t.Context(), storeAdd, []delegation.Proof{delegation.FromDelegation(dlg)}, vctx)

0 commit comments

Comments
 (0)