Skip to content

Commit

Permalink
feat: support receiving JFF JWT credential
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Sep 25, 2023
1 parent 0015e6f commit 6c8bdd1
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 72 deletions.
50 changes: 50 additions & 0 deletions .yarn/patches/@aries-framework-core-npm-0.4.1-bf3ca88ff9.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
diff --git a/build/modules/vc/models/credential/W3cCredential.js b/build/modules/vc/models/credential/W3cCredential.js
index 0c03676b24f394c7b48e9b41d8101a576c94484b..d5b1ff8847721223e5bb486eb4716d790a849013 100644
--- a/build/modules/vc/models/credential/W3cCredential.js
+++ b/build/modules/vc/models/credential/W3cCredential.js
@@ -68,16 +68,37 @@ class W3cCredential {
return utils_1.JsonTransformer.fromJSON(json, W3cCredential);
}
}
-__decorate([
- (0, class_transformer_1.Expose)({ name: '@context' }),
+__decorate(
+ [
+ (0, class_transformer_1.Expose)({ name: "@context" }),
(0, validators_2.IsCredentialJsonLdContext)(),
- __metadata("design:type", Array)
-], W3cCredential.prototype, "context", void 0);
-__decorate([
+ // FIXME: credentials issued by MATTR in JWT format use a string value for the @context
+ // The spec requires it to be an array.
+ // See: https://jfflabs.slack.com/archives/C05FTNY6GH2/p1695650296840639?thread_ts=1695648357.864189&cid=C05FTNY6GH2
+ (0, class_transformer_1.Transform)(({ type, value }) => {
+ if (type !== class_transformer_1.TransformationType.PLAIN_TO_CLASS)
+ return value;
+ if (Array.isArray(value)) return value;
+ if (typeof value === "string") return [value];
+ }),
+ __metadata("design:type", Array),
+ ],
+ W3cCredential.prototype,
+ "context",
+ void 0
+);
+__decorate(
+ [
(0, class_validator_1.IsOptional)(),
- (0, validators_1.IsUri)(),
- __metadata("design:type", String)
-], W3cCredential.prototype, "id", void 0);
+ // FIXME: credential issued by MATTR launchpad uses an UUID as the `jti`, which is not a valid URI
+ // See: https://jfflabs.slack.com/archives/C05FTNY6GH2/p1695648357864189
+ // (0, validators_1.IsUri)(),
+ __metadata("design:type", String),
+ ],
+ W3cCredential.prototype,
+ "id",
+ void 0
+);
__decorate([
IsCredentialType(),
__metadata("design:type", Array)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@cosmjs/utils": "npm:@cosmjs-rn/utils@^0.27.1",
"@cosmjs/proto-signing": "npm:@cosmjs-rn/proto-signing@^0.27.1",
"@cosmjs/crypto": "npm:@cosmjs-rn/crypto@^0.27.1",
"@aries-framework/[email protected]": "patch:@aries-framework/core@npm%3A0.4.1#./.yarn/patches/@aries-framework-core-npm-0.4.1-bf3ca88ff9.patch",
"@sphereon/[email protected]": "patch:@sphereon/did-auth-siop@npm%3A0.3.2-unstable.0#./.yarn/patches/@sphereon-did-auth-siop-npm-0.3.2-unstable.0-6a34120d09.patch"
},
"dependencies": {
Expand Down
18 changes: 13 additions & 5 deletions packages/agent/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,22 @@ export const receiveCredentialFromOpenId4VciOffer = async ({
}) => {
// Prefer did:jwk, otherwise use did:key, otherwise use undefined
const didMethod =
supportsAllDidMethods || supportedDidMethods.includes('did:jwk')
supportsAllDidMethods || supportedDidMethods?.includes('did:jwk')
? 'jwk'
: supportedDidMethods.includes('did:key')
: // If supportedDidMethods is undefined, it means we couldn't determine the supported did methods
// This is either because an inline credential offer was used, or the issuer didn't declare which
// did methods are supported.
// NOTE: MATTR launchpad for JFF MUST use did:key. So it is important that the default
// method is did:key if supportedDidMethods is undefined.
supportedDidMethods?.includes('did:key') || supportedDidMethods === undefined
? 'key'
: undefined

if (!didMethod) {
throw new Error(
`No supported did method could be found. Supported methods are did:key and did:jwk. Issuer supports ${supportedDidMethods.join(
', '
)}`
`No supported did method could be found. Supported methods are did:key and did:jwk. Issuer supports ${
supportedDidMethods?.join(', ') ?? 'Unknown'
}`
)
}

Expand Down Expand Up @@ -111,6 +116,9 @@ export const receiveCredentialFromOpenId4VciOffer = async ({
verifyCredentialStatus: false,
allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson],
allowedProofOfPossessionSignatureAlgorithms: [
// NOTE: MATTR launchpad for JFF MUST use EdDSA. So it is important that the default (first allowed one)
// is EdDSA. The list is ordered by preference, so if no suites are defined by the issuer, the first one
// will be used
JwaSignatureAlgorithm.EdDSA,
JwaSignatureAlgorithm.ES256,
],
Expand Down
190 changes: 128 additions & 62 deletions packages/openid4vc-client/src/OpenId4VcClientService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import type {
W3cVerifyCredentialResult,
} from '@aries-framework/core'
import type {
CredentialOfferFormat,
CredentialResponse,
CredentialSupported,
Jwt,
OfferedCredentialsWithMetadata,
OpenIDResponse,
} from '@sphereon/oid4vci-common'

Expand Down Expand Up @@ -187,21 +188,28 @@ export class OpenId4VcClientService {

// Loop through all the credentialTypes in the credential offer
for (const offeredCredential of client.getOfferedCredentialsWithMetadata()) {
const format = (
isInlineCredentialOffer(offeredCredential)
? offeredCredential.inlineCredentialOffer.format
: offeredCredential.credentialSupported.format
) as SupportedCredentialFormats

// TODO: support inline credential offers. Not clear to me how to determine the did method / alg, etc..
if (offeredCredential.type === OfferedCredentialType.InlineCredentialOffer) {
throw new AriesFrameworkError("Inline credential offers aren't supported")
}

const supportedCredentialMetadata = offeredCredential.credentialSupported

// FIXME
// If the credential id ends with the format, it is a v8 credential supported that has been
// split into multiple entries (each entry can now only have one format). For now we continue
// as assume there will be another entry with the correct format.
if (supportedCredentialMetadata.id?.endsWith(`-${supportedCredentialMetadata.format}`)) {
const format = getUniformFormat(supportedCredentialMetadata.format)
if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) {
continue
// Check if the format is supported/allowed
if (!allowedCredentialFormats.includes(format)) continue
} else {
const supportedCredentialMetadata = offeredCredential.credentialSupported

// FIXME
// If the credential id ends with the format, it is a v8 credential supported that has been
// split into multiple entries (each entry can now only have one format). For now we continue
// as assume there will be another entry with the correct format.
if (supportedCredentialMetadata.id?.endsWith(`-${supportedCredentialMetadata.format}`)) {
const uniformFormat = getUniformFormat(
supportedCredentialMetadata.format
) as SupportedCredentialFormats
if (!allowedCredentialFormats.includes(uniformFormat)) continue
}
}

Expand All @@ -211,7 +219,7 @@ export class OpenId4VcClientService {
{
allowedCredentialFormats,
allowedProofOfPossessionSignatureAlgorithms,
credentialMetadata: supportedCredentialMetadata,
offeredCredentialWithMetadata: offeredCredential,
proofOfPossessionVerificationMethodResolver:
options.proofOfPossessionVerificationMethodResolver,
}
Expand Down Expand Up @@ -243,10 +251,19 @@ export class OpenId4VcClientService {
.withTokenFromResponse(accessToken)
.build()

const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({
proofInput,
credentialSupported: supportedCredentialMetadata,
})
let credentialResponse: OpenIDResponse<CredentialResponse>

if (isInlineCredentialOffer(offeredCredential)) {
credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({
proofInput,
inlineCredentialOffer: offeredCredential.inlineCredentialOffer,
})
} else {
credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({
proofInput,
credentialSupported: offeredCredential.credentialSupported,
})
}

const credential = await this.handleCredentialResponse(agentContext, credentialResponse, {
verifyCredentialStatus: options.verifyCredentialStatus,
Expand All @@ -261,8 +278,15 @@ export class OpenId4VcClientService {
})
this.logger.debug('Full credential', credentialRecord)

// Set the OpenId4Vc credential metadata and update record
setOpenId4VcCredentialMetadata(credentialRecord, supportedCredentialMetadata, serverMetadata)
if (!isInlineCredentialOffer(offeredCredential)) {
const supportedCredentialMetadata = offeredCredential.credentialSupported
// Set the OpenId4Vc credential metadata and update record
setOpenId4VcCredentialMetadata(
credentialRecord,
supportedCredentialMetadata,
serverMetadata
)
}

receivedCredentials.push(credentialRecord)
}
Expand All @@ -282,12 +306,12 @@ export class OpenId4VcClientService {
proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver
allowedCredentialFormats: SupportedCredentialFormats[]
allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[]
credentialMetadata: CredentialSupported
offeredCredentialWithMetadata: OfferedCredentialsWithMetadata
}
) {
const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods } =
this.getProofOfPossessionRequirements(agentContext, {
credentialMetadata: options.credentialMetadata,
offeredCredentialWithMetadata: options.offeredCredentialWithMetadata,
allowedCredentialFormats: options.allowedCredentialFormats,
allowedProofOfPossessionSignatureAlgorithms:
options.allowedProofOfPossessionSignatureAlgorithms,
Expand All @@ -305,22 +329,29 @@ export class OpenId4VcClientService {
JwkClass.keyType
)

const format = getUniformFormat(options.credentialMetadata.format)
const format = isInlineCredentialOffer(options.offeredCredentialWithMetadata)
? options.offeredCredentialWithMetadata.inlineCredentialOffer.format
: options.offeredCredentialWithMetadata.credentialSupported.format

// Now we need to determine the did method and alg based on the cryptographic suite
const verificationMethod = await options.proofOfPossessionVerificationMethodResolver({
credentialFormat: format as SupportedCredentialFormats,
proofOfPossessionSignatureAlgorithm: signatureAlgorithm,
supportedVerificationMethods,
keyType: JwkClass.keyType,
supportedCredentialId: options.credentialMetadata.id as string,
supportedCredentialId: !isInlineCredentialOffer(options.offeredCredentialWithMetadata)
? options.offeredCredentialWithMetadata.credentialSupported.id
: undefined,
supportsAllDidMethods,
supportedDidMethods,
})

// Make sure the verification method uses a supported did method
if (
!supportsAllDidMethods &&
// If supportedDidMethods is undefined, it means the issuer didn't include the binding methods in the metadata
// The user can still select a verification method, but we can't validate it
supportedDidMethods !== undefined &&
!supportedDidMethods.find((supportedDidMethod) =>
verificationMethod.id.startsWith(supportedDidMethod)
)
Expand Down Expand Up @@ -353,32 +384,44 @@ export class OpenId4VcClientService {
agentContext: AgentContext,
options: {
allowedCredentialFormats: SupportedCredentialFormats[]
credentialMetadata: CredentialSupported
offeredCredentialWithMetadata: OfferedCredentialsWithMetadata
allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[]
}
): ProofOfPossessionRequirements {
const { credentialMetadata, allowedCredentialFormats } = options
const { offeredCredentialWithMetadata, allowedCredentialFormats } = options

// Extract format from offer
let format =
offeredCredentialWithMetadata.type === OfferedCredentialType.InlineCredentialOffer
? offeredCredentialWithMetadata.inlineCredentialOffer.format
: offeredCredentialWithMetadata.credentialSupported.format

// Get uniform format, so we don't have to deal with the different spec versions
const format = getUniformFormat(credentialMetadata.format)
format = getUniformFormat(format)

if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) {
throw new AriesFrameworkError(
`Issuer only supports format '${format}' for credential type '${
credentialMetadata.id as string
}', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'`
)
}
const credentialMetadata =
offeredCredentialWithMetadata.type === OfferedCredentialType.CredentialSupported
? offeredCredentialWithMetadata.credentialSupported
: undefined

const issuerSupportedCryptographicSuites =
credentialMetadata.cryptographic_suites_supported ?? []
const issuerSupportedBindingMethods: string[] =
credentialMetadata.cryptographic_binding_methods_supported ??
const issuerSupportedCryptographicSuites = credentialMetadata?.cryptographic_suites_supported
const issuerSupportedBindingMethods =
credentialMetadata?.cryptographic_binding_methods_supported ??
// FIXME: somehow the MATTR Launchpad returns binding_methods_supported instead of cryptographic_binding_methods_supported
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(credentialMetadata.binding_methods_supported as string[] | undefined) ??
[]
(credentialMetadata?.binding_methods_supported as string[] | undefined)

if (!isInlineCredentialOffer(offeredCredentialWithMetadata)) {
const credentialMetadata = offeredCredentialWithMetadata.credentialSupported
if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) {
throw new AriesFrameworkError(
`Issuer only supports format '${format}' for credential type '${
credentialMetadata.id as string
}', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'`
)
}
}

// For each of the supported algs, find the key types, then find the proof types
const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry)
Expand All @@ -388,41 +431,55 @@ export class OpenId4VcClientService {
switch (format) {
case 'jwt_vc_json':
case 'jwt_vc_json-ld':
potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find(
(signatureAlgorithm) => issuerSupportedCryptographicSuites.includes(signatureAlgorithm)
)
// If undefined, it means the issuer didn't include the cryptographic suites in the metadata
// We just guess that the first one is supported
if (issuerSupportedCryptographicSuites === undefined) {
potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0]
} else {
potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find(
(signatureAlgorithm) => issuerSupportedCryptographicSuites.includes(signatureAlgorithm)
)
}
break
case 'ldp_vc':
// We need to find it based on the JSON-LD proof type
potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find(
(signatureAlgorithm) => {
const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm)
if (!JwkClass) return false

// TODO: getByKeyType should return a list
const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType)
if (!matchingSuite) return false

return issuerSupportedCryptographicSuites.includes(matchingSuite.proofType)
}
)
// If undefined, it means the issuer didn't include the cryptographic suites in the metadata
// We just guess that the first one is supported
if (issuerSupportedCryptographicSuites === undefined) {
potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0]
} else {
// We need to find it based on the JSON-LD proof type
potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find(
(signatureAlgorithm) => {
const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm)
if (!JwkClass) return false

// TODO: getByKeyType should return a list
const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType)
if (!matchingSuite) return false

return issuerSupportedCryptographicSuites.includes(matchingSuite.proofType)
}
)
}
break
default:
throw new AriesFrameworkError(
`Unsupported requested credential format '${credentialMetadata.format}' with id ${
credentialMetadata.id as string
`Unsupported requested credential format '${format}' with id ${
credentialMetadata?.id ?? 'Inline credential offer'
}`
)
}

const supportsAllDidMethods = issuerSupportedBindingMethods.includes('did')
const supportedDidMethods = issuerSupportedBindingMethods.filter((method) =>
const supportsAllDidMethods = issuerSupportedBindingMethods?.includes('did') ?? false
const supportedDidMethods = issuerSupportedBindingMethods?.filter((method) =>
method.startsWith('did:')
)

if (!potentialSignatureAlgorithm) {
throw new AriesFrameworkError(
`Could not establish signature algorithm for id ${credentialMetadata.id as string}`
`Could not establish signature algorithm for format ${format} and id ${
credentialMetadata?.id ?? 'Inline credential offer'
}`
)
}

Expand Down Expand Up @@ -559,3 +616,12 @@ export class OpenId4VcClientService {
}
}
}

function isInlineCredentialOffer(
offeredCredential: OfferedCredentialsWithMetadata
): offeredCredential is {
inlineCredentialOffer: CredentialOfferFormat
type: OfferedCredentialType.InlineCredentialOffer
} {
return offeredCredential.type === OfferedCredentialType.InlineCredentialOffer
}
Loading

0 comments on commit 6c8bdd1

Please sign in to comment.