feat: Improved/simplified validator code#7
Conversation
alanshaw
left a comment
There was a problem hiding this comment.
👏 I really like this! There's some blocking comments, but this makes a lot more sense now - thank you.
| case ed25519.Code: | ||
| return ed25519.Decode(bytes) | ||
| case secp256k1.Code: | ||
| return secp256k1.Decode(bytes) |
There was a problem hiding this comment.
This forces consumers to bundle code for both ed25519 and secp256k1 even if they are not using both.
I would either move this to a separate package or create a registry pattern (similar to varsig) where ed25519 is supported by default but you can add secp256k1 support if you need it.
There was a problem hiding this comment.
Oh, yeah, great call.
| Header: h, | ||
| TokenPayload1_0_0_rc1: tokenPayload, | ||
| }, nil | ||
| } |
There was a problem hiding this comment.
You might want to rebase against #8 as this exposes this method and it's now not nearly as complicated as it needs to be here.
|
|
||
| // NopValidateAuthorization is a [ValidateAuthorizationFunc] that does no | ||
| // validation and returns nil. | ||
| func NopValidateAuthorization(ctx context.Context, auth Authorization) error { |
There was a problem hiding this comment.
Why does this get removed? Was previously intended to be for checking revocations.
There was a problem hiding this comment.
Only because it was never called. Totally reasonable to have, we just have to actually use it. Although I might suggest we make it explicitly about revocation, since "validation" is what the whole thing is about.
There was a problem hiding this comment.
Totally fine to rename - I don't know why it was named so generically but we will need this at some point. I think it's worth leaving it in.
There was a problem hiding this comment.
Since it's not part of the signature, just an option, can we leave it out until we actually use it? We'll know how to write it properly then, and until then it's just a lie.
There was a problem hiding this comment.
until then it's just a lie.
To clarify, UCAN revocation was implemented and used in the JS upload service and client. I think it's a bit misleading to call it a lie. It has not been implemented in Sprue or Guppy yet, but support here is a prerequisite.
Please at least open an issue to re-add support.
There was a problem hiding this comment.
When I say it's a lie, what I mean is that having it in the code implies that it does something, but it doesn't. I don't want Ucantone to promise its consumer it'll do something, especially when it comes to security, and then not do it. It's absolutely a requirement, but I don't want to act like we've already implemented it before we do.
There was a problem hiding this comment.
Oh: I think I may have been unclear. I'm not saying that no consumers used it. I'm saying it literally does nothing. That function was never called, even if you provided one.
| // invocation against proof policies. | ||
| func New[A Arguments](cmd ucan.Command, options ...capability.Option) (*Capability[A], error) { | ||
| cap, err := capability.New(cmd, options...) | ||
| func New[A Arguments](cmd ucan.Command) (*Capability[A], error) { |
There was a problem hiding this comment.
Yep, just didn't want to throw it in the same PR.
frrist
left a comment
There was a problem hiding this comment.
One small drive-by comment. As Alan mentioned, after rebasing on main much of the seralization code should be a bit simpler. You'll also find the changes you've made to the did type have already landed in main: Method(), Identifier(), Defined(), and an Undef type.
| if !strings.HasPrefix(str, did.KeyPrefix) { | ||
| return nil, fmt.Errorf("must start with '%s'", did.KeyPrefix) | ||
| } | ||
| code, bytes, err := multibase.Decode(str[len(did.KeyPrefix):]) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if code != multibase.Base58BTC { | ||
| return nil, errors.New("not Base58BTC encoded") | ||
| } | ||
|
|
||
| keyTypeCode, _, err := varint.FromUvarint(bytes) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("reading uvarint: %w", err) | ||
| } |
There was a problem hiding this comment.
Could we instead call did.Parse on the input string, this keeps all did parsing code in one place, then make assertions on it using the did.Method() and did.Identifier() methods?
Ports the DID resolvers from `go-libstoracha` and deals with the fallout of upgrading to latest ucantone. It has a soft dependency on fil-forge/ucantone#7 in that you'll need to wrap the resolvers with something to make them look like what is required by ucantone right now.
Signature verification is the same operation for every token, parameterized by token type's Signature Payload definition.
Also, add a few more missing tests
Previously only the `isOrdered` operations handled a `int`/`int64` mismatch. `OpEqual` needs to handle non-numeric things also, so it took a different code path which missed the normalization. Now the normalization happens outside of `isOrdered` to cover that.
1544b5e to
a2cc064
Compare
Consumers don't have to pull in all encryption packages.
a2cc064 to
56144ff
Compare
alanshaw
left a comment
There was a problem hiding this comment.
This is great, such a big improvement. I'm happy for this to merge provided all the feedback can be addressed in some form.
Please could you also quickly update the server documentation on the README - https://github.com/fil-forge/ucantone#server ? 🙏
| @@ -73,13 +68,11 @@ func (d *Dispatcher) Execute(req execution.Request) (execution.Response, error) | |||
| opts := []validator.Option{validator.WithMetadata(req.Metadata())} | |||
There was a problem hiding this comment.
Just a note here - if our server is using a did:web there's an implicit requirement that we pass in a DID document resolver or map resolver that includes the server DID (preferably the latter since we actually know the did:key) so that it can resolve any delegations that it has issued. It would be nice if that was taken care of automatically or at least explicitly documented - it's a bit of a footgun.
There was a problem hiding this comment.
Yeah, that makes sense. What do you mean by "automatically", though?
There was a problem hiding this comment.
Previously we passed authority that would have allowed us to do that...automatically.
| } | ||
| } | ||
|
|
||
| func WithAuthorizationValidator(validateAuthorization ValidateAuthorizationFunc) Option { |
There was a problem hiding this comment.
If you're going to remove it then please create an issue to add support for revocation checking during validation.
There was a problem hiding this comment.
Rename tp command_definition_test.go?
|
|
||
| // SigPayload returns the decoded signature payload (varsig header + token payload). | ||
| func (d *Delegation) SigPayload() *ddm.SigPayloadModel { | ||
| func (d *Delegation) SigPayload() cbg.CBORMarshaler { |
There was a problem hiding this comment.
Why does this change? It's not part of any interface IIRC.
There was a problem hiding this comment.
Oh, good call. I had added it to an interface and then realized it wasn't actually useful now that we have SignedBytes() instead, and never switched this back.
|
|
||
| // SigPayload returns the decoded signature payload (varsig header + token payload). | ||
| func (inv *Invocation) SigPayload() *idm.SigPayloadModel { | ||
| func (inv *Invocation) SigPayload() cbg.CBORMarshaler { |
|
|
||
| // ResolveDIDKeyVerifier is a [DIDVerifierResolverFunc] that only supports `did:key` | ||
| // DIDs and returns an error for any other DID method. | ||
| func ResolveDIDKeyVerifier(ctx context.Context, d did.DID) (ucan.Verifier, error) { |
There was a problem hiding this comment.
This is table stakes. There should at least be some warning that when passing a custom verifier via WithVerifierResolver that it MUST support this baseline resolution. Or perhaps the validator should automatically do this?
There was a problem hiding this comment.
I want to avoid baking did:key resolution into the validator as anything more than a default. did:keys are extremely common, but they're not actually required for UCAN at all. The entire spec works if that method doesn't exist in the universe. But accidentally dropping support for them would almost certainly cause your validator to be useless in practice, so it's certainly a footgun.
I've made it easier to compose resolvers in 6101d63 and provided a convenience option to tighten that up even more in practice. How's that look?
|
|
||
| // NopValidateAuthorization is a [ValidateAuthorizationFunc] that does no | ||
| // validation and returns nil. | ||
| func NopValidateAuthorization(ctx context.Context, auth Authorization) error { |
There was a problem hiding this comment.
until then it's just a lie.
To clarify, UCAN revocation was implemented and used in the JS upload service and client. I think it's a bit misleading to call it a lie. It has not been implemented in Sprue or Guppy yet, but support here is a prerequisite.
Please at least open an issue to re-add support.
| } | ||
|
|
||
| func TestFixtures(t *testing.T) { | ||
| fixturesFile, err := os.Open("./internal/fixtures/invocations.json") |
There was a problem hiding this comment.
Can we please continue to validate against the fixtures? That way if new fixtures are added to the list we can bring them in here and easily test against the new cases.
There was a problem hiding this comment.
Oof, yes! This was a bad merge when I moved the new implementation in to replace the old one. It should absolutely still have the fixture tests.
| } | ||
|
|
||
| func WithAuthorizationValidator(validateAuthorization ValidateAuthorizationFunc) Option { | ||
| func WithVerifierResolver(resolveDIDKey DIDVerifierResolverFunc) Option { |
There was a problem hiding this comment.
I would include "DID" in this option name, also the parameter technically doesn't have to resolve to DID key, it just has to be able to produce a verifier. Attestations for example 😉
| func WithVerifierResolver(resolveDIDKey DIDVerifierResolverFunc) Option { | |
| func WithDIDVerifierResolver(resolveDID DIDVerifierResolverFunc) Option { |
These were genericised for an interface change that never came to pass.
Also removes `verifier.Parse(string)`, which was only ever called after stringifying an already-parsed DID, in favor of `verifier.FromDIDKey(did.DID)`.
As a consequence, realigns errors with expected errors from fixtures.
|
@alanshaw Addressed and merged! I'd love your eyes on that last round of changes, just in case. |
These were removed in #7 but are used in `libforge`, `indexing-service` and `piri`. The parse functions per key type allow you to support multiple keys but restrict parsing to a specific one. They also return the verifier type for the specific key (not the interface), allowing users to access type specific methods (in the future).
This aligns the validator code closer to UCAN 1.0 and removes some of (I think) the vestiges of the old
go-ucanto. The main work is invalidator.Validate, which replacesvalidator.Access. It now takes dramatically fewer arguments and options:authority: Handled by DID resolution. (Also, stops giving theauthorityunproven access to do and delegate anything to any subject, which is off-spec and not required for Forge.)capability: Not relevant to validation; all invocations are validated the same way.canIssue: Defined by UCAN itself, not configurable.parsePrincipal: Folded into DID resolution.validateAuthorization: Was never actually called (🙈).validationTime: Time is time is time.verifyNonStandardSignature: Attestation is still a thing.resolveDIDKey: becomesresolveDIDVerifier, which resolves a DID to a verifier. This reflects the actual DID operation: resolving a DID to a document and finding verification methods. No longer uses intermediate fictitiousdid:keyDIDs to represent keys that aren't actually DIDs. Returns a singleVerifier, even though a DID may have multiple verification methods; we can construct aVerifierwhich composes multipleVerifiers if needed.This change introduces a second thing called "capability" which is actually what the UCAN spec calls a "capability" rather than what
go-ucantoand the original TS version called a "capability". We can replace "capability" with "command" now, but that's significant enough to warrant its own PR.