-
Notifications
You must be signed in to change notification settings - Fork 25
/
mattr.launchpad.spec.ts
252 lines (215 loc) · 11.4 KB
/
mattr.launchpad.spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import { PresentationSignCallBackParams, PresentationSubmissionLocation } from '@sphereon/pex';
import { W3CVerifiablePresentation } from '@sphereon/ssi-types';
import * as ed25519 from '@transmute/did-key-ed25519';
import { resolve as didKeyResolve } from '@transmute/did-key-ed25519';
import { fetch } from 'cross-fetch';
import { DIDResolutionResult } from 'did-resolver';
import { DIDResolutionOptions } from 'did-resolver/src/resolver';
import { importJWK, JWK, SignJWT } from 'jose';
import * as u8a from 'uint8arrays';
import {
AuthorizationRequest,
AuthorizationResponse,
CheckLinkedDomain,
OP,
PresentationDefinitionWithLocation,
PresentationExchange,
SigningAlgo,
SupportedVersion,
VerificationMode,
} from '../../src';
export interface InitiateOfferRequest {
types: string[];
}
export interface InitiateOfferResponse {
authorizeRequestUri: string;
state: string;
nonce: string;
}
export const UNIT_TEST_TIMEOUT = 30000;
export const VP_CREATE_URL = 'https://launchpad.mattrlabs.com/api/vp/create';
export const OPENBADGE_JWT_VC =
'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8jNkJoRk1DR1RKZyJ9.eyJpc3MiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwic3ViIjoiZGlkOmtleTp6Nk1raXRHVmduTGRORlpqbUE5WEpwQThrM29lakVudU1GN205NkJEN3BaTGprWTIiLCJuYmYiOjE2OTYzNjA1MTEsImV4cCI6MTcyNzk4MjkxMSwidmMiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSBEZWdyZWUiLCJkZXNjcmlwdGlvbiI6IkpGRiBQbHVnZmVzdCAzIE9wZW5CYWRnZSBDcmVkZW50aWFsIiwiY3JlZGVudGlhbEJyYW5kaW5nIjp7ImJhY2tncm91bmRDb2xvciI6IiM0NjRjNDkifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL21hdHRyLmdsb2JhbC9jb250ZXh0cy92Yy1leHRlbnNpb25zL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9leHRlbnNpb25zLmpzb24iLCJodHRwczovL3czaWQub3JnL3ZjLXJldm9jYXRpb24tbGlzdC0yMDIwL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1raXRHVmduTGRORlpqbUE5WEpwQThrM29lakVudU1GN205NkJEN3BaTGprWTIiLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJuYW1lIjoiVGVhbXdvcmsiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sImltYWdlIjp7ImlkIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0zLTIwMjMvaW1hZ2VzL0pGRi1WQy1FRFUtUExVR0ZFU1QzLWJhZGdlLWltYWdlLnBuZyIsInR5cGUiOiJJbWFnZSJ9LCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4ifX0sImlzc3VlciI6eyJpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8iLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IiwiaWNvblVybCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmciLCJpbWFnZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmcifX19.JDQ5kp_nvqJbL9Q8o2xIdt_r_WG0cB1o-Boy1RiDZhXRlVTgwAxvCa41OiL97VnbovN98tL7VtXbM6slAt6TBg';
export const jwk: JWK = {
crv: 'Ed25519',
d: 'kTRm0aONHYwNPA-w_DtjMHUIWjE3K70qgCIhWojZ0eU',
x: 'NeA0d8sp86xRh3DczU4m5wPNIbl0HCSwOBcMN3sNmdk',
kty: 'OKP',
};
// pub hex: 35e03477cb29f3ac518770dccd4e26e703cd21b9741c24b038170c377b0d99d9
const hexPrivateKey = '913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5';
const didStr = `did:key:z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
const kid = `${didStr}#z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
console.log(kid);
export const generateDid = async (opts?: { seed?: Uint8Array }) => {
const { didDocument, keys } = await ed25519.generate(
{
secureRandom: () => {
return opts?.seed ?? '913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5';
},
},
{ accept: 'application/did+json' },
);
return { keys, didDocument };
};
const resolve = async (didUrl: string, options?: DIDResolutionOptions): Promise<DIDResolutionResult> => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return await didKeyResolve(didUrl, options);
};
describe('OID4VCI-Client using Mattr issuer should', () => {
async function testWithOp(format: string | string[]) {
const did = await generateDid({ seed: u8a.fromString(hexPrivateKey, 'base16') });
expect(did).toBeDefined();
expect(did.didDocument).toBeDefined();
const offer = await getOffer(format);
const { authorizeRequestUri, state, nonce } = offer;
expect(authorizeRequestUri).toBeDefined();
expect(state).toBeDefined();
expect(nonce).toBeDefined();
const correlationId = 'test';
const op: OP = OP.builder()
.addResolver('key', { resolve })
.withCheckLinkedDomain(CheckLinkedDomain.NEVER)
.withPresentationSignCallback(presentationSignCalback)
.withSignature({ alg: SigningAlgo.EDDSA, kid, did: didStr, hexPrivateKey })
.build();
const verifiedAuthRequest = await op.verifyAuthorizationRequest(authorizeRequestUri, { correlationId });
expect(verifiedAuthRequest).toBeDefined();
expect(verifiedAuthRequest.presentationDefinitions).toHaveLength(1);
const pex = new PresentationExchange({ allDIDs: [didStr], allVerifiableCredentials: [OPENBADGE_JWT_VC] });
const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
verifiedAuthRequest.authorizationRequestPayload,
);
await pex.selectVerifiableCredentialsForSubmission(pd[0].definition);
const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, [OPENBADGE_JWT_VC], presentationSignCalback, {
presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
proofOptions: { nonce },
holderDID: didStr,
});
const authResponse = await op.createAuthorizationResponse(verifiedAuthRequest, {
issuer: didStr,
presentationExchange: {
verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
presentationSubmission: verifiablePresentationResult.presentationSubmission,
},
correlationId,
});
expect(authResponse).toBeDefined();
expect(authResponse.response.payload).toBeDefined();
expect(authResponse.response.payload.presentation_submission).toBeDefined();
expect(authResponse.response.payload.vp_token).toBeDefined();
console.log(JSON.stringify(authResponse));
const result = await op.submitAuthorizationResponse(authResponse);
expect(result.status).toEqual(200);
}
async function testWithPayloads(format: string | string[]) {
const did = await generateDid({ seed: u8a.fromString(hexPrivateKey, 'base16') });
expect(did).toBeDefined();
expect(did.didDocument).toBeDefined();
const offer = await getOffer(format);
const { authorizeRequestUri, state, nonce } = offer;
expect(authorizeRequestUri).toBeDefined();
expect(state).toBeDefined();
expect(nonce).toBeDefined();
const correlationId = 'test';
const verifiedAuthRequest = await AuthorizationRequest.verify(authorizeRequestUri, {
correlationId,
verification: {
mode: VerificationMode.INTERNAL,
resolveOpts: {},
},
});
expect(verifiedAuthRequest).toBeDefined();
expect(verifiedAuthRequest.presentationDefinitions).toHaveLength(1);
const pex = new PresentationExchange({ allDIDs: [didStr], allVerifiableCredentials: [OPENBADGE_JWT_VC] });
const pd: PresentationDefinitionWithLocation[] = await PresentationExchange.findValidPresentationDefinitions(
verifiedAuthRequest.authorizationRequestPayload,
);
await pex.selectVerifiableCredentialsForSubmission(pd[0].definition);
const verifiablePresentationResult = await pex.createVerifiablePresentation(pd[0].definition, [OPENBADGE_JWT_VC], presentationSignCalback, {
presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
proofOptions: { nonce },
holderDID: didStr,
});
const authResponse = await AuthorizationResponse.fromVerifiedAuthorizationRequest(
verifiedAuthRequest,
{
presentationExchange: {
verifiablePresentations: [verifiablePresentationResult.verifiablePresentation],
presentationSubmission: verifiablePresentationResult.presentationSubmission,
},
signature: { hexPrivateKey: '913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5', did: didStr, kid, alg: SigningAlgo.EDDSA },
},
{ correlationId, verification: { mode: VerificationMode.INTERNAL, resolveOpts: {} }, nonce, state },
);
expect(authResponse).toBeDefined();
expect(authResponse.payload).toBeDefined();
expect(authResponse.payload.presentation_submission).toBeDefined();
expect(authResponse.payload.vp_token).toBeDefined();
console.log(JSON.stringify(authResponse));
}
it(
'succeed using OpenID4VCI version 11 and ldp_vc request/responses',
async () => {
await testWithPayloads('OpenBadgeCredential');
},
UNIT_TEST_TIMEOUT,
);
it(
'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
async () => {
await testWithOp('OpenBadgeCredential');
},
UNIT_TEST_TIMEOUT,
);
});
async function getOffer(types: string | string[]): Promise<InitiateOfferResponse> {
const credentialOffer = await fetch(VP_CREATE_URL, {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
//make sure to serialize your JSON body
body: JSON.stringify({
types: Array.isArray(types) ? types : [types],
}),
});
return (await credentialOffer.json()) as InitiateOfferResponse;
}
describe('Mattr OID4VP v18 credential offer', () => {
test('should verify using request directly', async () => {
const offer = await getOffer('OpenBadgeCredential');
const authorizationRequest = await AuthorizationRequest.fromUriOrJwt(offer.authorizeRequestUri);
console.log(JSON.stringify(authorizationRequest.payload, null, 2));
console.log(JSON.stringify(await authorizationRequest.getSupportedVersionsFromPayload()));
const verification = await authorizationRequest.verify({
correlationId: 'test',
verification: {
mode: VerificationMode.INTERNAL,
resolveOpts: {},
},
});
console.log(JSON.stringify(verification));
expect(verification).toBeDefined();
expect(verification.versions).toEqual([SupportedVersion.SIOPv2_D12_OID4VP_D18]);
/**
* pd value: {"id":"dae5d9b6-8145-4297-99b2-b8fcc5abb5ad","input_descriptors":[{"id":"OpenBadgeCredential","format":{"jwt_vc_json":{"alg":["EdDSA"]},"jwt_vc":{"alg":["EdDSA"]}},"constraints":{"fields":[{"path":["$.vc.type"],"filter":{"type":"array","items":{"type":"string"},"contains":{"const":"OpenBadgeCredential"}}}]}}]}
*/
});
});
async function presentationSignCalback(args: PresentationSignCallBackParams): Promise<W3CVerifiablePresentation> {
const importedJwk = await importJWK(jwk, 'EdDSA');
const jwt = await new SignJWT({ vp: { ...args.presentation }, nonce: args.options.proofOptions.nonce, iss: args.options.holderDID })
.setProtectedHeader({
typ: 'JWT',
alg: 'EdDSA',
kid,
})
.setIssuedAt()
.setExpirationTime('2h')
.sign(importedJwk);
console.log(`VP: ${jwt}`);
return jwt;
}