-
Notifications
You must be signed in to change notification settings - Fork 239
feat: custom verification callback for the authorization request on the holder side for verification #2303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…the holder side for verification Signed-off-by: Berend Sliedrecht <berend@animo.id>
🦋 Changeset detectedLatest commit: 03b0011 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Chore is generally for non feature related changes (e.g. preparing for a release). This is more a feature (as it allows you to do something that wasn't possible before) |
if (!result) { | ||
throw new CredoError('verificationAuthorizationCallback returned false. User-provided validation failed.') | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will swallow any errors. I think it might be better to make the inner method throw. Or we at least return the error so we can add it as cause. i think requiring the callback to throw has my preference
can you add a changeset (just know i will keep commenting this on every PR, so might be good to just add it to every PR by default 😅) |
export interface ResolveOpenId4VpAuthorizationRequestOptions { | ||
trustedCertificates?: EncodedX509Certificate[] | ||
origin?: string | ||
verifyAuthorizationRequestCallback?: VerifyAuthorizationRequestCallback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Soo looking at it like this, what does the callback provide over just doing
- resolve
- call custom callback method
The callback just adds another layer of indirection.
For the verifier side the callback makes more sense since we automatically send a http response (and you might want to block it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also thinking whether the name of the callback may mislead into thinking it will override the verification (which it doesn't).
To continue on my previous comment. The input to the verify is just the authorization request, which is not always the full context (which you get returned from the method). So I'd actually lean to not merge this, and just call our custom callback after calling the credo resolve method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It enforces it slightly more, the callback is optional so it is not enforced strictly. The API will be extended before merging to get more values next to the authorizationRequest
in the callback. I just need to find out everything that is required or can be nice to have in the verification context.
Haha yes, I will! |
…s the holder to do additional validation on the authorization request when resolving it Signed-off-by: Berend Sliedrecht <berend@animo.id>
…sed on the registration_certificate Signed-off-by: Berend Sliedrecht <berend@animo.id>
45d4b54
to
ae03ac5
Compare
const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) | ||
|
||
await expect( | ||
holderTenant.modules.openId4VcHolder.resolveOpenId4VpAuthorizationRequest( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Main interesting code, i.e. validation the registration certificate, happens here.
|
||
export type VerifyAuthorizationRequestOptions = { | ||
authorizationRequest: Openid4vpAuthorizationRequest | Openid4vpAuthorizationRequestDcApi | ||
jwsService: JwsService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense to pass the agentContext
as the first param to the callback. The user can then inject anything they need. Attestations could be other things than JWTs as well, so it's too focused on a specific case.
If the context is passed the user can just do agentContext.resolve(JwsService)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, makes sense
for (const va of authorizationRequest.verifier_attestations) { | ||
// Here we verify it as a registration certificate according to | ||
// https://bmi.usercontent.opencode.de/eudi-wallet/eidas-2.0-architekturkonzept/flows/Wallet-Relying-Party-Authentication/#registration-certificate | ||
if (va.format === 'jwt') { | ||
if (typeof va.data !== 'string') { | ||
throw new Error('Only inline JWTs are supported') | ||
} | ||
|
||
const { isValid } = await jwsService.verifyJws(holder.agent.context, { jws: va.data }) | ||
const jwt = Jwt.fromSerializedJwt(va.data) | ||
|
||
if (!isValid) { | ||
throw new Error('Invalid signature on JWT provided in the verifier_attestations') | ||
} | ||
|
||
if (jwt.header.typ !== 'rc-rp+jwt') { | ||
throw new Error(`only 'rc-rp+jwt' is supported as header typ. Request included: ${jwt.header.typ}`) | ||
} | ||
|
||
const registrationCertificateHeaderSchema = z | ||
.object({ | ||
typ: z.literal('rc-rp+jwt'), | ||
alg: z.string(), | ||
// sprin-d did not define this | ||
x5u: z.string().url().optional(), | ||
// sprin-d did not define this | ||
'x5t#s256': z.string().optional(), | ||
}) | ||
.passthrough() | ||
|
||
// TODO: does not support intermediaries | ||
const registrationCertificatePayloadSchema = z | ||
.object({ | ||
credentials: z.array( | ||
z.object({ | ||
id: z.string().optional(), | ||
format: z.string(), | ||
multiple: z.boolean().default(false), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is too complex for what's being tested. We're testing the callback, and this adds the very complex structure of a registration certificate. I would like to keep this test simple, and add the EUDI specific validations to a new repository.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that we are not testing what is relevant here. I can move it to another repo, that is fine.
// TODO: export from oid4vp | ||
export type ParsedTransactionDataEntry = NonNullable<ResolvedOpenid4vpAuthorizationRequest['transactionData']>[number] | ||
|
||
export type VerifyAuthorizationRequestOptions = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel we're still missing relevant information from the OpenId4VpResolvedAuthorizationRequest
interface here.
Also we should try to stay as consistent as possible with OpenId4VpResolvedAuthorizationRequest
.
So
authorizationRequestPayload: OpenId4VpAuthorizationRequestPayload
and ideally:
/**
* Metadata about the signed authorization request.
*
* Only present if the authorization request was signed
*/
signedAuthorizationRequest?: {
signer: VerifiedJarRequest['signer']
payload: VerifiedJarRequest['jwt']['payload']
header: VerifiedJarRequest['jwt']['header']
}
also add the origin (in case of DC-API):
/**
* Origin of the request, to be used with Digital Credentials API
*/
origin?: string
Maybe we can just pass the same interface? Or do we call this before the DCQL and Presentation Exchange Queries are run? (in that case we can omit these from the interface). But i would rather not add client
here if it's not also available in the resolved value.
}) | ||
} catch (e) { | ||
throw new CredoError( | ||
`verificationAuthorizationCallback returned false. User-provided validation failed. cause: ${e}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method does not return false anymore
`verificationAuthorizationCallback returned false. User-provided validation failed. cause: ${e}`, | |
`error during call to user-provided verifyAuthorizationRequestCallback. cause: ${e}`, |
… request Signed-off-by: Berend Sliedrecht <berend@animo.id>
775bf07
to
cc916d4
Compare
Signed-off-by: Berend Sliedrecht <berend@animo.id>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One remaining comment
|
||
export type VerifyAuthorizationRequestOptions = { | ||
agentContext: AgentContext | ||
} & OpenId4VpResolvedAuthorizationRequest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transactionData, pex and dcql will never be defined, so just using the interface is misleading. Can we do a Pick on the interface?
Signed-off-by: Berend Sliedrecht <berend@animo.id>
f36d781
to
03b0011
Compare
66d3384
into
openwallet-foundation:main
…he holder side for verification (openwallet-foundation#2303) Signed-off-by: Berend Sliedrecht <berend@animo.id>
…he holder side for verification (openwallet-foundation#2303) Signed-off-by: Berend Sliedrecht <berend@animo.id> Signed-off-by: Ariel Gentile <gentilester@gmail.com>
Signed-off-by: Berend Sliedrecht berend@animo.id