Skip to content

Conversation

@berendsliedrecht
Copy link
Member

  • feat: Relying Party Authorization
  • feat: authz disclosure policy verification

})
}

const result = runDcqlQuery(attributeBasedAccessControlPolicy.attribute_based_access_control as Query.Output, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we should use the approach as used by Credo: https://github.com/openwallet-foundation/credo-ts/blob/1ac3c1d258fbef3fea655072bdf61d73960733c6/packages/core/src/modules/dcql/DcqlService.ts#L153

And actually parse the query first instead of casting it as .Output

Comment on lines 23 to 27
credentials.push({
credential_format: 'vc+sd-jwt',
vct: sdJwtCredential.prettyClaims.vct,
claims: sdJwtCredential.prettyClaims,
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just using dc+sd-jwt is fine here, since the DCQL queries can only contain dc+sd-jwt right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this from Credo.

But I see now in the changelog of openid4vp they renamed vc+... to dc+....

Removed it.

const credentials: DcqlCredential[] = []
const sdJwtService = agentContext.dependencyManager.resolve(SdJwtVcService)
for (const authorizationAttestation of authorizationAttestations) {
const sdJwtCredential = sdJwtService.fromCompact(authorizationAttestation)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you filter the attestations beforehand, so we always know this will be an Sd-JWT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a check, whether it is not a registration certificate. I assumed this method would handle it, but I can add a layer around it.

Comment on lines +7 to +10
export const validateAllowListPolicy = (
allowListPolicy: AllowListPolicy,
relyingPartyAccessCertificate: X509Certificate
) => allowListPolicy.allowlist.includes(relyingPartyAccessCertificate.subject)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer returning objects with keys for these kinds of things. I think there's a good chance we need to add more context/metadata in the future. Especially if we e.g. want to return the entry at some piont.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for this case, returning a boolean makes sense as it is just a validation API, not an extraction. Also, including the entry does not really make sense as the entry (the rpAccessCert is provided by the user).

Can change it for consistency, though.

presentation: false,
})

return result.canBeSatisfied
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, prefer objects which allow for extension

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer as above.

Comment on lines +72 to +86
try {
const { isValid } = await jwsService.verifyJws(agentContext, {
jws: authorizationAttestation,
trustedCertificates,
})
isValidAndTrusted = isValid
} catch {
if (allowUntrustedSigned) {
const { isValid } = await jwsService.verifyJws(agentContext, {
jws: authorizationAttestation,
trustedCertificates: jwt.header.x5c ?? [],
})
isValidButUntrusted = isValid
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to tweak the API in Credo to handle this better. Verifying twice is not a good approach

Comment on lines 88 to 96
if (!signedAuthorizationRequest) {
throw new Error('Authorization request must be signed for the authorization attestation')
}

if (signedAuthorizationRequest.signer.method !== 'x5c') {
throw new Error(
'x5c is only supported for key derivation on the authorization request containing a authorization attestation'
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed returning metadata over throwing errors. Any reason to still throw errors here. If we're doing this we at least need to start throwing catchable errors so you can handle the errors correctly

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what makes this error uncatchable, but I changed it to a context object.


// TODO: confirm that the `signingCertificate.subject` is the DN of the RP
if (payload.sub !== accessCertificate.subject) {
throw new Error('The Subject of the Authorization Attestation should equal the distinguished of the Relying Party')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new Error('The Subject of the Authorization Attestation should equal the distinguished of the Relying Party')
throw new Error('The Subject of the Authorization Attestation should equal the distinguished name of the Relying Party')

})

if (!isValid) {
throw new Error(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be good to have a cause here, to not swallow the error.

*
* Checks all the matched credentials for additional disclosure policies as set by the issuer
*
* Make sure the disclosure policies are set manually on the metadata of the record, under the `__disclosurePolicy` key
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we include something that identifies it as eudi in the keyname


export type AuthzPolicy = AllowListPolicy | RootOfTrustPolicy | AttributeBasedAccessControlPolicy

type VerifyAuthorizationForOpenid4VpAuthorizationRequestOptions = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type VerifyAuthorizationForOpenid4VpAuthorizationRequestOptions = {
export type VerifyAuthorizationForOpenid4VpAuthorizationRequestOptions = {

import { type RootOfTrustPolicy, validateRootOfTrustPolicy } from './validateRootOfTrustPolicy'
import { isAuthorizationAttestation } from './verifyAuthorizationAttestation'

export type AuthzPolicy = AllowListPolicy | RootOfTrustPolicy | AttributeBasedAccessControlPolicy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally don't use abbreviations

Suggested change
export type AuthzPolicy = AllowListPolicy | RootOfTrustPolicy | AttributeBasedAccessControlPolicy
export type AuthorizationPolicy = AllowListPolicy | RootOfTrustPolicy | AttributeBasedAccessControlPolicy

Comment on lines +27 to +52
credentials: z.array(
z.object({
format: z.string(),
multiple: z.boolean().default(false),
meta: z
.object({
vct_values: z.array(z.string()).optional(),
doctype_value: z.string().optional(),
})
.optional(),
trusted_authorities: z
.array(z.object({ type: z.string(), values: z.array(z.string()) }))
.nonempty()
.optional(),
require_cryptographic_holder_binding: z.boolean().default(true),
claims: z
.array(
z.object({
id: z.string().optional(),
path: z.array(z.string()).nonempty().nonempty(),
values: z.array(z.number().or(z.boolean())).optional(),
})
)
.nonempty()
.optional(),
claim_sets: z.array(z.array(z.string())).nonempty().optional(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way that we could reuse the DCQL query type, or do we really need to redefine this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, if they are both in zod otherwise I think it would add unnecessary complexity.

Comment on lines 24 to 26
if (typeof va.data !== 'string') {
throw new Error('Only inline data supported')
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data can also be an object, in which case the error is a bit confusing

Copy link
Member

@TimoGlastra TimoGlastra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a quick look, still need to do a thorough review. I'm missing some of the returning context instead of throwing errors that we discussed.

@berendsliedrecht
Copy link
Member Author

I wanted to handle the errors with context in a different pr, as it also includes the registration certificate verification, but I can also do it here.

@berendsliedrecht
Copy link
Member Author

Added a return context to the disclosurePolicy validation:

export type VerifyDisclosurePoliciesForOpenId4VpAuthorizationRequestReturnContext = {
  isValid: boolean
  isSignedWithX509: boolean
  disclosurePolicies: {
    [credentialId: string]: {
      isAllowListPolicyValid?: boolean
      isRootOfTrustPolicyValid?: boolean
      isAttributeBasedAccessControlValid?: boolean
    }
  }
}

I think this provides enough context for a wallet to display information to the user about the request, lmk if something is missing.

Signed-off-by: Berend Sliedrecht <[email protected]>
Comment on lines +112 to +113
strictEqual(result?.isValid, false)
equal(result?.isRegistrationCertificateQueryEqualOrSubsetOfAuthorizationRequestQuery, false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
strictEqual(result?.isValid, false)
equal(result?.isRegistrationCertificateQueryEqualOrSubsetOfAuthorizationRequestQuery, false)
strictEqual(result.isValid, false)
equal(result.isRegistrationCertificateQueryEqualOrSubsetOfAuthorizationRequestQuery, false)


await rejects(
verifyOpenid4VpAuthorizationRequest(agent.context, {
verifyRegistrationCertificateInOpenid4VpAuthorizationRequest(agent.context, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does it throw vs return return invalid?

Copy link
Member

@TimoGlastra TimoGlastra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments. Overall looks good, but we need a bit more consistency in when we throw errors vs return isValid: false


equal(result?.[0].isValidAndTrusted, false)
equal(result?.[0].isValidButUntrusted, true)
equal(result?.isValid, true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
equal(result?.isValid, true)
equal(result.isValid, true)

if (!resolvedAuthorizationRequest.authorizationRequestPayload.verifier_attestations) return
for (const va of resolvedAuthorizationRequest.authorizationRequestPayload.verifier_attestations) {
if (typeof va.data !== 'string') {
throw new Error('Authorization Attestations of string are currently only supported')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create a specific error for this library? So we can catch error as being an EUDI verification error?

Comment on lines +182 to +183
registrationCertificateHeaderSchema.parse(jwt.header)
const parsedPayload = registrationCertificatePayloadSchema.parse(jwt.payload.toJson())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this should also be part of the returned context, (and we should add an human error message there).

It's very random now:

  • JWS signature verification fails -> return isValid: false
  • registration certificate header schema parsing fails -> throw error

const rpCert = X509Certificate.fromEncodedCertificate(rpCertEncoded)

if (rpCert.subject !== parsedPayload.sub) {
returnContext.isAccessCertificateSubjectEqualToRegistrationCertificate = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also set these to true if they did succeed, so you know which checks were performed.

const isValidDcqlQuery = isDcqlQueryEqualOrSubset(dcql.queryResult, parsedPayload as unknown as DcqlQuery)
if (!isValidDcqlQuery) {
returnContext.isValid = false
returnContext.isRegistrationCertificateQueryEqualOrSubsetOfAuthorizationRequestQuery = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this with a JSDoc comment be sufficient?

Suggested change
returnContext.isRegistrationCertificateQueryEqualOrSubsetOfAuthorizationRequestQuery = false
returnContext.isAuthorizationRequestQueryValid = false

Comment could be like:

/**
* Returns whether the authorization request query is equal or a subset of the registration certificate query
*/


// TODO:
// - Should we support more hashing confirmation claim values?
const authorizationAttestationPayloadSchema = z.object({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we don't have to redefine the JWT payload every time 🤔 . Fine for now, but might be interesting to build on top of a base defined in e.g. openid4vc libs

// TODO: I am not sure how to validate this, based on the description
//
// rootOfTrust: the certificate of a RP must be derived from a list of specific root certificates. In the Implementing Acts it's called Specific root of trust. The value has to be the distinguished name of the root certificate.
export const validateRootOfTrustPolicy = (rootOfTrustPolicy: RootOfTrustPolicy) => true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we just need to do a match on the distinguished name (issuer or subject) of the certificate?

So probaby the issuer field of the root/intermediate certificate in the authorization request must match one of the distinguides names from the list passed

Comment on lines +46 to +49
const queryResult = Query.query(
Query.parse(attributeBasedAccessControlPolicy.attribute_based_access_control),
credentials
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can throw if an invalid query is used. Do we catch that somewhere?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants