Skip to content

Commit

Permalink
Merge pull request #165 from TimoGlastra/feat/mdoc-handling
Browse files Browse the repository at this point in the history
feat: mso mdoc handling
  • Loading branch information
sanderPostma authored Oct 31, 2024
2 parents 5e4237d + 403debf commit 3b516be
Show file tree
Hide file tree
Showing 18 changed files with 1,190 additions and 547 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const vJarmDirectPostJwtParams = v.looseObject({
...v.omit(vJarmAuthResponseParams, ['iss', 'aud', 'exp']).entries,
...v.partial(v.pick(vJarmAuthResponseParams, ['iss', 'aud', 'exp'])).entries,

vp_token: v.string(),
vp_token: v.union([v.string(), v.array(v.pipe(v.string(), v.nonEmpty()))]),
presentation_submission: v.unknown(),
nonce: v.optional(v.string()),
});
Expand Down
1 change: 0 additions & 1 deletion packages/siop-oid4vp/lib/__tests__/IT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1831,4 +1831,3 @@ describe('RP and OP interaction should', () => {
expect(resState?.status).toBe('error')
})
})

251 changes: 251 additions & 0 deletions packages/siop-oid4vp/lib/__tests__/MsoMdoc.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { SigningAlgo } from '@sphereon/oid4vc-common'
import { PEX } from '@sphereon/pex'
import { PresentationDefinitionV2 } from '@sphereon/pex-models'
import { OriginalVerifiableCredential } from '@sphereon/ssi-types'

import {
OP,
PassBy,
PresentationDefinitionWithLocation,
PresentationExchange,
PresentationVerificationCallback,
PropertyTarget,
ResponseIss,
ResponseType,
RevocationVerification,
RP,
Scope,
SubjectType,
SupportedVersion,
VPTokenLocation,
} from '..'

import { getVerifyJwtCallback, internalSignature } from './DidJwtTestUtils'
import { getResolver } from './ResolverTestUtils'
import { mockedGetEnterpriseAuthToken, pexHasher, sdJwtVcPresentationSignCallback, WELL_KNOWN_OPENID_FEDERATION } from './TestUtils'

jest.setTimeout(30000)

const EXAMPLE_REDIRECT_URL = 'https://acme.com/hello'

const HOLDER_DID = 'did:example:ebfeb1f712ebc6f1c276e12ec21'

const mdocBase64UrlUniversity =
'uQACam5hbWVTcGFjZXOhd2V1LmV1cm9wYS5lYy5ldWRpLnBpZC4xhNgYWGikaGRpZ2VzdElEAHFlbGVtZW50SWRlbnRpZmllcmp1bml2ZXJzaXR5bGVsZW1lbnRWYWx1ZWlpbm5zYnJ1Y2tmcmFuZG9tWCDPDfrRde4BPN5uQhSGnm8zmhFiMm2pjTzx5z3JmEKLKdgYWGOkaGRpZ2VzdElEAXFlbGVtZW50SWRlbnRpZmllcmZkZWdyZWVsZWxlbWVudFZhbHVlaGJhY2hlbG9yZnJhbmRvbVggOUutjAeZTM2jcre7I4Gfeqy81azrsSXtbpWH65QmJTbYGFhhpGhkaWdlc3RJRAJxZWxlbWVudElkZW50aWZpZXJkbmFtZWxlbGVtZW50VmFsdWVoSm9obiBEb2VmcmFuZG9tWCD3XuNqynfdWeNM9qanYauAk5iin3lXV4eCd4RqNaCVBdgYWGGkaGRpZ2VzdElEA3FlbGVtZW50SWRlbnRpZmllcmNub3RsZWxlbWVudFZhbHVlaWRpc2Nsb3NlZGZyYW5kb21YICmBo2MFCt3SoUx36ZNOSPXRcA5hb1ABmy5Q5F9V6_ulamlzc3VlckF1dGiEQ6EBJqIEWDF6RG5hZXJDa3ppOERHNTZRVWN0aTJaSk1jd2ZFcFpLb2VYNW4xRlp3THZjQWZ2VHZpGCGBWPwwgfkwgaCgAwIBAgIQElXcBkTBG_kaIWLYwVbnAzAKBggqhkjOPQQDAjANMQswCQYDVQQGEwJERTAeFw0yNDEwMzAxMTAwMThaFw0yNTEwMzAxMTAwMThaMA0xCzAJBgNVBAYTAkRFMDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgADfu2vJOiV-lZLsM5p3CGYjMXX_hjj9LsQybiK0c9ixVujAjAAMAoGCCqGSM49BAMCA0gAMEUCIQDVhXXnyqyJ7Y8VECpvP4sZ1jTbnQ684CmFAUR2kHuArAIgAhDDybZ9k_sAFpArd9YAlfSBgA6r2SgmhXyxfYdQ26pZAd3YGFkB2LkABmd2ZXJzaW9uYzEuMG9kaWdlc3RBbGdvcml0aG1nU0hBLTI1Nmx2YWx1ZURpZ2VzdHOhd2V1LmV1cm9wYS5lYy5ldWRpLnBpZC4xpABYIHxEA-V6vOFCQAuHYIYARAxRgZ_5DgIUy-i9SL_1AMRiAVggcm01ODxrEhO8x6ZsfdhiiZd-e8Qvww0z-C_jlm-rCoICWCAuLB7-RZv_qA5elyMAWDQZUTQXpR20Y-HyHOel7EsCxgNYIJE9tUTIRvZt8NJSmI4-j0NzqKUtt2DBYQZ9CpoC8o64bWRldmljZUtleUluZm-5AAFpZGV2aWNlS2V5pAECIAEhWCB1WBBG2WGAzEWzM4UUUpcGFiJxtCI6sRp_o0SaMJhnNSJYIDDCu4r2F0N8khrP-Hww23HaQTW4X_-bXYwMED_orB7UZ2RvY1R5cGVxb3JnLmV1LnVuaXZlcnNpdHlsdmFsaWRpdHlJbmZvuQAEZnNpZ25lZMB0MjAyNC0xMC0zMFQxMTowMDoyMFppdmFsaWRGcm9twHQyMDI0LTEwLTMwVDExOjAwOjIwWmp2YWxpZFVudGlswHQyMDI1LTEwLTMwVDExOjAwOjIwWm5leHBlY3RlZFVwZGF0ZfdYQNiBC_noBzIuL0HdBNCe5GWNKQ07GbRc1Kn0yQ2NE4qY6PbPzd3O4UAaTpeqHclMbHOoAJssSAbxIEooKan-vXI'
const mdocBase64UrlUniversityPresentation =
'uQADZ3ZlcnNpb25jMS4waWRvY3VtZW50c4GjZ2RvY1R5cGVxb3JnLmV1LnVuaXZlcnNpdHlsaXNzdWVyU2lnbmVkuQACam5hbWVTcGFjZXOhd2V1LmV1cm9wYS5lYy5ldWRpLnBpZC4xgtgYWGGkaGRpZ2VzdElEAnFlbGVtZW50SWRlbnRpZmllcmRuYW1lbGVsZW1lbnRWYWx1ZWhKb2huIERvZWZyYW5kb21YICTUPEzNlBwbcWWOXijZrs4Ed37zoxDCKJYvv0qKtpuv2BhYY6RoZGlnZXN0SUQBcWVsZW1lbnRJZGVudGlmaWVyZmRlZ3JlZWxlbGVtZW50VmFsdWVoYmFjaGVsb3JmcmFuZG9tWCC6uRVoNoBBcj5b-IEDTCUFoNEGVGsMSZP-3YuMUVCKrGppc3N1ZXJBdXRohEOhASaiBFgxekRuYWV0bk5naHRrNHk1VzFDNGpBM3E4VmRYbzhlUzNpWWViRm5MR3I3ZlhTYVVUNhghgVj8MIH5MIGgoAMCAQICEF36OiPSysIvMaLWuTCava8wCgYIKoZIzj0EAwIwDTELMAkGA1UEBhMCREUwHhcNMjQxMDMwMTI1ODQ0WhcNMjUxMDMwMTI1ODQ0WjANMQswCQYDVQQGEwJERTA5MBMGByqGSM49AgEGCCqGSM49AwEHAyIAA6VBlDzOG438-hsPWMSY56vJWrz8m5OaIimg0rG0vY6towIwADAKBggqhkjOPQQDAgNIADBFAiBc_30LjkQFX9YxWUyYH5jFK4Smw2h4KKYU85BBH2xDTAIhAKqb7RwT5_qoVJNYcom0x3N1eVd49TuPZfkbNaZsmhi5WQHd2BhZAdi5AAZndmVyc2lvbmMxLjBvZGlnZXN0QWxnb3JpdGhtZ1NIQS0yNTZsdmFsdWVEaWdlc3RzoXdldS5ldXJvcGEuZWMuZXVkaS5waWQuMaQAWCDrF96Sw8aHk1fZ8B92ZQE7I37MHjVSDoEq4MGhHuMIcwFYIAEsfqF7G_6k-lw2NKPRwHlWSalgrYsbXdcqz1ghPa-nAlggGq9DTWd1xmO8O84B0PCKhtf0daiT34V4xkU-wSGHYUwDWCDX5TNczi_TZSwmJ1VVeEzXpKXR9eweibocvAfpmKHEU21kZXZpY2VLZXlJbmZvuQABaWRldmljZUtleaQBAiABIVggN4_nyaOESmuHV8xhsUl2VqxaF83kIraAc2GV7M2-BKEiWCC0GqqvYnJ6U12ccZVDAOH8CeNGs9oOAF46jXJfauTSO2dkb2NUeXBlcW9yZy5ldS51bml2ZXJzaXR5bHZhbGlkaXR5SW5mb7kABGZzaWduZWTAdDIwMjQtMTAtMzBUMTI6NTg6NDRaaXZhbGlkRnJvbcB0MjAyNC0xMC0zMFQxMjo1ODo0NFpqdmFsaWRVbnRpbMB0MjAyNS0xMC0zMFQxMjo1ODo0NFpuZXhwZWN0ZWRVcGRhdGX3WEC3VoysIcxum_HtX5OCFEA3BwzhHcYmESJDzY58vz0Ez7Zo3fmP3D0M8evzMk7_Cz7_hwVL8sdLgiKpho5UXrunbGRldmljZVNpZ25lZLkAAmpuYW1lU3BhY2Vz2BhDuQAAamRldmljZUF1dGi5AAJvZGV2aWNlU2lnbmF0dXJlhEOhASag91hA9peGbzwyivN7UXvk4smItYMdt-RvcU87ZvXdDfRqIQsWSxGLcke2lHcit77fIEAw_8w0MOzM7ObQWK3T4vTMl2lkZXZpY2VNYWP3ZnN0YXR1cwA'

const sdJwt =
'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rcnpRUEJyNHB5cUM3NzZLS3RyejEzU2NoTTVlUFBic3N1UHVRWmI1dDR1S1EifQ.eyJ2Y3QiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZGVncmVlIjoiYmFjaGVsb3IiLCJjbmYiOnsia2lkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIn0sImlzcyI6ImRpZDprZXk6ejZNa3J6UVBCcjRweXFDNzc2S0t0cnoxM1NjaE01ZVBQYnNzdVB1UVpiNXQ0dUtRIiwiaWF0IjoxNzMwMjkzMTIzLCJfc2QiOlsiVEtuSUJwVGp3ZmpVdFZra3ZBUWNrSDZxSEZFbmFsb1ZtZUF6UmlzZlNNNCIsInRLTFAxWFM3Vm55YkJET2ZWV3hTMVliNU5TTjhlMVBDMHFqRnBnbjd5XzgiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.GhgxbTA_cLZ6-enpOrTRqhIoZEzJoJMSQeutQdhcIayhiem9yd8i0x-h6NhQbN1NrNPwi-JQhy5lpNopVia_AA~WyI3NDU5ODc1MjgyODgyMTY5MjY3NTk1MTgiLCJ1bml2ZXJzaXR5IiwiaW5uc2JydWNrIl0~'

function getPresentationDefinitionV2(withSdJwtInputDescriptor = false): PresentationDefinitionV2 {
const pd: PresentationDefinitionV2 = {
id: 'mDL-sample-req',
input_descriptors: [
{
id: 'org.eu.university',
format: {
mso_mdoc: {
alg: ['ES256', 'ES384', 'ES512', 'EdDSA', 'ESB256', 'ESB320', 'ESB384', 'ESB512'],
},
},
constraints: {
fields: [
{
path: ["$['eu.europa.ec.eudi.pid.1']['name']"],
intent_to_retain: false,
},
{
path: ["$['eu.europa.ec.eudi.pid.1']['degree']"],
intent_to_retain: false,
},
],
limit_disclosure: 'required',
},
},
],
}

if (withSdJwtInputDescriptor) {
pd.input_descriptors.push({
id: 'OpenBadgeCredentialDescriptor',
format: {
'vc+sd-jwt': {
'sd-jwt_alg_values': ['EdDSA'],
},
},
constraints: {
limit_disclosure: 'required',
fields: [
{
path: ['$.vct'],
filter: {
type: 'string',
const: 'OpenBadgeCredential',
},
},
{
path: ['$.university'],
},
],
},
})
}

return pd
}

function getVCs(): OriginalVerifiableCredential[] {
return [sdJwt, mdocBase64UrlUniversity]
}

describe('mdoc RP and OP interaction should', () => {
it('succeed when calling with presentation definitions and right verifiable presentation without id token', async () => {
const opMockEntity = await mockedGetEnterpriseAuthToken('OP')
const rpMockEntity = await mockedGetEnterpriseAuthToken('RP')

const presentationVerificationCallback: PresentationVerificationCallback = async (presentation) => {
// higher level library needs to implement actual verification
return { verified: presentation === mdocBase64UrlUniversityPresentation }
}

const resolver = getResolver('ethr')
const rp = RP.builder({
requestVersion: SupportedVersion.SIOPv2_D12_OID4VP_D18,
})
.withClientId(rpMockEntity.did)
.withHasher(pexHasher)
.withResponseType([ResponseType.VP_TOKEN])
.withRedirectUri(EXAMPLE_REDIRECT_URL)
.withPresentationDefinition({ definition: getPresentationDefinitionV2() }, [
PropertyTarget.REQUEST_OBJECT,
PropertyTarget.AUTHORIZATION_REQUEST,
])
.withPresentationVerification(presentationVerificationCallback)
.withRevocationVerification(RevocationVerification.NEVER)
.withRequestBy(PassBy.VALUE)
.withCreateJwtCallback(internalSignature(rpMockEntity.hexPrivateKey, rpMockEntity.did, `${rpMockEntity.did}#controller`, SigningAlgo.ES256K))
.withVerifyJwtCallback(getVerifyJwtCallback(resolver))
.withAuthorizationEndpoint('www.myauthorizationendpoint.com')
.withClientMetadata({
client_id: WELL_KNOWN_OPENID_FEDERATION,
idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
responseTypesSupported: [ResponseType.VP_TOKEN],
vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
subjectTypesSupported: [SubjectType.PAIRWISE],
subject_syntax_types_supported: ['did', 'did:key'],
passBy: PassBy.VALUE,
})
.withSupportedVersions(SupportedVersion.SIOPv2_ID1)
.build()
const op = OP.builder()
.withPresentationSignCallback(sdJwtVcPresentationSignCallback)
.withExpiresIn(1000)
.withHasher(pexHasher)
.withCreateJwtCallback(internalSignature(opMockEntity.hexPrivateKey, opMockEntity.did, `${opMockEntity.did}#controller`, SigningAlgo.ES256K))
.withVerifyJwtCallback(getVerifyJwtCallback(resolver))
.withRegistration({
authorizationEndpoint: 'www.myauthorizationendpoint.com',
idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA],
issuer: ResponseIss.SELF_ISSUED_V2,
requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256],
responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN],
vpFormats: { jwt_vc: { alg: [SigningAlgo.EDDSA] } },
scopesSupported: [Scope.OPENID_DIDAUTHN, Scope.OPENID],
subjectTypesSupported: [SubjectType.PAIRWISE],
subject_syntax_types_supported: [],
passBy: PassBy.VALUE,
})
.withSupportedVersions(SupportedVersion.SIOPv2_ID1)
.build()

const requestURI = await rp.createAuthorizationRequestURI({
correlationId: '1234',
nonce: 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg',
state: 'b32f0087fc9816eb813fd11f',
jwtIssuer: { method: 'did', alg: SigningAlgo.ES256K, didUrl: `${rpMockEntity.did}#controller` },
})

// Let's test the parsing
const parsedAuthReqURI = await op.parseAuthorizationRequestURI(requestURI.encodedUri)
expect(parsedAuthReqURI.authorizationRequestPayload).toBeDefined()
expect(parsedAuthReqURI.requestObjectJwt).toBeDefined()

if (!parsedAuthReqURI.requestObjectJwt) throw new Error('requestObjectJwt is undefined')
const verifiedAuthReqWithJWT = await op.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt)
expect(verifiedAuthReqWithJWT.issuer).toMatch(rpMockEntity.did)
const pex = new PresentationExchange({
allDIDs: [HOLDER_DID],
allVerifiableCredentials: getVCs(),
hasher: pexHasher,
})
const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
parsedAuthReqURI.authorizationRequestPayload,
)
const results = await pex.selectVerifiableCredentialsForSubmission(pd[0].definition)
expect(results).toEqual({
errors: [],
matches: [
{
name: 'org.eu.university',
rule: 'all',
vc_path: ['$.verifiableCredential[0]'],
type: 'InputDescriptor',
id: 'org.eu.university',
},
],
areRequiredCredentialsPresent: 'info',
verifiableCredential: [mdocBase64UrlUniversity],
warnings: [],
vcIndexes: [1],
})

// NOTE: for now we don't support creating mdoc presentations yes, so we mock that part.
// Will be added in a follow up PR (need to extend PEX first)
const presentationResult = new PEX().evaluatePresentation(pd[0].definition, mdocBase64UrlUniversityPresentation, {
generatePresentationSubmission: true,
})
expect(presentationResult).toEqual({
areRequiredCredentialsPresent: 'info',
errors: [],
presentations: [mdocBase64UrlUniversityPresentation],
value: {
definition_id: 'mDL-sample-req',
descriptor_map: [
{
format: 'mso_mdoc',
id: 'org.eu.university',
path: '$',
},
],
id: expect.any(String),
},
warnings: [],
})

const authenticationResponseWithJWT = await op.createAuthorizationResponse(verifiedAuthReqWithJWT, {
jwtIssuer: {
method: 'did',
alg: SigningAlgo.ES256K,
didUrl: `${rpMockEntity.did}#controller`,
},
presentationExchange: {
verifiablePresentations: [mdocBase64UrlUniversityPresentation],
vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE,
presentationSubmission: presentationResult.value,
},
})
expect(authenticationResponseWithJWT.response.payload).toBeDefined()
expect(authenticationResponseWithJWT.response.idToken).toBeUndefined()

const verifiedAuthResponseWithJWT = await rp.verifyAuthorizationResponse(authenticationResponseWithJWT.response.payload, {
presentationDefinitions: [{ definition: pd[0].definition, location: pd[0].location }],
})

// Cannot extract nonce, should be handled by the verification callback that verifies
// session transcript, until device response parsing is fixed
expect(verifiedAuthResponseWithJWT.oid4vpSubmission?.nonce).toEqual(undefined)
expect(verifiedAuthResponseWithJWT.idToken).toBeUndefined()
})
})
10 changes: 7 additions & 3 deletions packages/siop-oid4vp/lib/__tests__/PresentationExchange.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,9 @@ describe('presentation exchange manager tests', () => {
const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, vcs, presentationSignCallback, {})
await PresentationExchange.validatePresentationsAgainstDefinitions(
pd,
verifiablePresentationResult.verifiablePresentations.map(verifiablePresentation => CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentation)),
verifiablePresentationResult.verifiablePresentations.map((verifiablePresentation) =>
CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentation),
)[0],
undefined,
{
presentationSubmission: verifiablePresentationResult.presentationSubmission,
Expand Down Expand Up @@ -524,7 +526,7 @@ describe('presentation exchange manager tests', () => {
},
),
).rejects.toThrow(
'Could not find VerifiableCredentials matching presentationDefinition object in the provided VC list, details: [{"status":"error","tag":"SubmissionDoesNotSatisfyDefinition","message":"Expected all input descriptors (2) to be satisfifed in submission, but found 1. Missing wa_driver_license"}]',
`message: Could not find VerifiableCredentials matching presentationDefinition object in the provided VC list, details: [{"status":"error","tag":"SubmissionDoesNotSatisfyDefinition","message":"Expected all input descriptors ('wa_driver_license', 'ca_driver_license') to be satisfied in submission, but found 'ca_driver_license'. Missing 'wa_driver_license'"}]`,
)
})

Expand All @@ -551,7 +553,9 @@ describe('presentation exchange manager tests', () => {
await expect(
PresentationExchange.validatePresentationsAgainstDefinitions(
pd,
verifiablePresentationResult.verifiablePresentations.map(verifiablePresentation => CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentation)),
verifiablePresentationResult.verifiablePresentations.map((verifiablePresentation) =>
CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentation),
)[0],
() => {
throw new Error('Verification failed')
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,5 @@ async function presentationSignCalback(args: PresentationSignCallBackParams): Pr
.setExpirationTime('2h')
.sign(importedJwk)

console.log(`VP: ${jwt}`)
return jwt
}
Loading

0 comments on commit 3b516be

Please sign in to comment.