Skip to content
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

feat: support receiving JFF JWT credential #67

Merged
merged 3 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
diff --git a/dist/authorization-response/PresentationExchange.js b/dist/authorization-response/PresentationExchange.js
index 46e80f434179752a8770dc604c54074451c18f8f..82881914d3c45bffe008da89f67d149d1644c23f 100644
--- a/dist/authorization-response/PresentationExchange.js
+++ b/dist/authorization-response/PresentationExchange.js
@@ -104,10 +104,12 @@ class PresentationExchange {
});
}
static assertValidPresentationSubmission(presentationSubmission) {
- const validationResult = pex_1.PEX.validateSubmission(presentationSubmission);
- if (validationResult[0].message != 'ok') {
- throw new Error(`${types_1.SIOPErrors.RESPONSE_OPTS_PRESENTATIONS_SUBMISSION_IS_NOT_VALID}, details ${JSON.stringify(validationResult[0])}`);
- }
+ // FIXME: enable validation. Currently it fails only in react native
+ // See issue: https://github.com/Sphereon-Opensource/PEX/issues/118
+ // const validationResult = pex_1.PEX.validateSubmission(presentationSubmission);
+ // if (validationResult[0].message != 'ok') {
+ // throw new Error(`${types_1.SIOPErrors.RESPONSE_OPTS_PRESENTATIONS_SUBMISSION_IS_NOT_VALID}, details ${JSON.stringify(validationResult[0])}`);
+ // }
}
/**
* Finds a valid PresentationDefinition inside the given AuthenticationRequestPayload
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"android": "cd apps/expo && yarn android",
"watch": "yarn workspaces foreach -pi run watch",
"fix": "manypkg fix",
"postinstall": "patch-package && yarn check-deps && yarn build",
"postinstall": "yarn check-deps && yarn build",
"build": "yarn workspaces foreach run build",
"upgrade:tamagui": "yarn up '*tamagui*'@latest '@tamagui/*'@latest react-native-web-lite@latest",
"upgrade:tamagui:canary": "yarn up '*tamagui*'@canary '@tamagui/*'@canary react-native-web-lite@canary",
Expand Down Expand Up @@ -41,7 +41,8 @@
"@cosmjs/tendermint-rpc": "npm:@cosmjs-rn/tendermint-rpc@^0.27.1",
"@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"
"@cosmjs/crypto": "npm:@cosmjs-rn/crypto@^0.27.1",
"@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": {
"@babel/runtime": "^7.18.9",
Expand All @@ -57,7 +58,6 @@
"eslint-plugin-react": "^7.32.2",
"expo-linking": "^4.0.1",
"node-gyp": "^9.3.1",
"patch-package": "^7.0.0",
"prettier": "^2.7.1",
"turbo": "^1.8.3",
"typescript": "^4.7.4"
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