Skip to content

Commit

Permalink
feat: Allow to acquire credentials without using a proof for V13. Thi…
Browse files Browse the repository at this point in the history
…s is rare and has to be supported by the issuer. For instance when using DPop and authorization code
  • Loading branch information
nklomp committed Aug 31, 2024
1 parent f696867 commit 2f1fcee
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 52 deletions.
110 changes: 81 additions & 29 deletions packages/client/lib/OpenID4VCIClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ import {
ProofOfPossessionCallbacks,
toAuthorizationResponsePayload,
} from '@sphereon/oid4vci-common';
import { CredentialFormat } from '@sphereon/ssi-types';
import { CredentialFormat, DIDDocument } from '@sphereon/ssi-types';
import Debug from 'debug';

import { AccessTokenClient } from './AccessTokenClient';
import { createAuthorizationRequestUrl } from './AuthorizationCodeClient';
import { CredentialOfferClient } from './CredentialOfferClient';
import { CredentialRequestOpts } from './CredentialRequestClient';
import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
import { CredentialRequestClientBuilderV1_0_13 } from './CredentialRequestClientBuilderV1_0_13';
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
import { generateMissingPKCEOpts, sendNotification } from './functions';
Expand Down Expand Up @@ -351,7 +351,41 @@ export class OpenID4VCIClientV1_0_13 {
return { ...this.accessTokenResponse, ...(this.dpopResponseParams && { params: this.dpopResponseParams }) };
}

public async acquireCredentials({
public async acquireCredentialsWithoutProof(args: {
credentialIdentifier?: string;
credentialTypes?: string | string[];
context?: string[];
format?: CredentialFormat | OID4VCICredentialFormat;
kid?: string;
jwk?: JWK;
alg?: Alg | string;
jti?: string;
deferredCredentialAwait?: boolean;
deferredCredentialIntervalInMS?: number;
experimentalHolderIssuanceSupported?: boolean;
createDPoPOpts?: CreateDPoPClientOpts;
}): Promise<CredentialResponse & { access_token: string }> {
return await this.acquireCredentialsImpl(args);
}
public async acquireCredentials(args: {
credentialIdentifier?: string;
credentialTypes?: string | string[];
context?: string[];
proofCallbacks: ProofOfPossessionCallbacks<any>;
format?: CredentialFormat | OID4VCICredentialFormat;
kid?: string;
jwk?: JWK;
alg?: Alg | string;
jti?: string;
deferredCredentialAwait?: boolean;
deferredCredentialIntervalInMS?: number;
experimentalHolderIssuanceSupported?: boolean;
createDPoPOpts?: CreateDPoPClientOpts;
}): Promise<CredentialResponse & { access_token: string }> {
return await this.acquireCredentialsImpl(args);
}

private async acquireCredentialsImpl({
credentialIdentifier,
credentialTypes,
context,
Expand All @@ -368,7 +402,7 @@ export class OpenID4VCIClientV1_0_13 {
credentialIdentifier?: string;
credentialTypes?: string | string[];
context?: string[];
proofCallbacks: ProofOfPossessionCallbacks<any>;
proofCallbacks?: ProofOfPossessionCallbacks<any>;
format?: CredentialFormat | OID4VCICredentialFormat;
kid?: string;
jwk?: JWK;
Expand All @@ -388,11 +422,11 @@ export class OpenID4VCIClientV1_0_13 {
if (kid) this._state.kid = kid;

const requestBuilder = this.credentialOffer
? CredentialRequestClientBuilder.fromCredentialOffer({
? CredentialRequestClientBuilderV1_0_13.fromCredentialOffer({
credentialOffer: this.credentialOffer,
metadata: this.endpointMetadata,
})
: CredentialRequestClientBuilder.fromCredentialIssuer({
: CredentialRequestClientBuilderV1_0_13.fromCredentialIssuer({
credentialIssuer: this.getIssuer(),
credentialIdentifier: credentialIdentifier,
metadata: this.endpointMetadata,
Expand Down Expand Up @@ -451,32 +485,50 @@ export class OpenID4VCIClientV1_0_13 {
}

const credentialRequestClient = requestBuilder.build();
const proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({
accessTokenResponse: this.accessTokenResponse,
callbacks: proofCallbacks,
version: this.version(),
})
.withIssuer(this.getIssuer())
.withAlg(this.alg);

if (this._state.jwk) {
proofBuilder.withJWK(this._state.jwk);
}
if (this._state.kid) {
proofBuilder.withKid(this._state.kid);
}
let proofBuilder: ProofOfPossessionBuilder<any> | undefined;
if (proofCallbacks) {
proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({
accessTokenResponse: this.accessTokenResponse,
callbacks: proofCallbacks,
version: this.version(),
})
.withIssuer(this.getIssuer())
.withAlg(this.alg);

if (this.clientId) {
proofBuilder.withClientId(this.clientId);
}
if (jti) {
proofBuilder.withJti(jti);
if (this._state.jwk) {
proofBuilder.withJWK(this._state.jwk);
}
if (this._state.kid) {
proofBuilder.withKid(this._state.kid);
}

if (this.clientId) {
proofBuilder.withClientId(this.clientId);
}
if (jti) {
proofBuilder.withJti(jti);
}
}
const response = await credentialRequestClient.acquireCredentialsUsingProof({
proofInput: proofBuilder,
...(credentialIdentifier ? { credentialIdentifier, subjectIssuance } : { format, context, credentialTypes, subjectIssuance }),
createDPoPOpts,
});
const request = proofBuilder
? await credentialRequestClient.createCredentialRequest<DIDDocument>({
proofInput: proofBuilder,
credentialTypes,
context,
format,
version: this.version(),
credentialIdentifier,
subjectIssuance,
})
: await credentialRequestClient.createCredentialRequestWithoutProof<DIDDocument>({
credentialTypes,
context,
format,
version: this.version(),
credentialIdentifier,
subjectIssuance,
});
const response = await credentialRequestClient.acquireCredentialsUsingRequest(request, createDPoPOpts);
this._state.dpopResponseParams = response.params;
if (response.errorBody) {
debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`);
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/__tests__/IT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ describe('OID4VCI-Client should', () => {

const credentialResponse = await client.acquireCredentials({
credentialIdentifier: 'OpenBadgeCredential',
format: 'jwt_vc_json-ld',
// format: 'jwt_vc_json-ld',
proofCallbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,20 +196,21 @@ describe('RP using test vectors', () => {
const presentationDefinitions = await authRequest.getPresentationDefinitions()

// Will throw an error because the path_nested is actually wrong. Should be $.vp.verifiableCredential[0], but is $.verifiableCredential[0]
await expect (authorizationResponse.verify({
correlationId: '1234',
verifyJwtCallback: verifyJwtCallback,
audience:
'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0',
verification: {
presentationVerificationCallback,
revocationOpts: {
revocationVerification: RevocationVerification.NEVER,
await expect(
authorizationResponse.verify({
correlationId: '1234',
verifyJwtCallback: verifyJwtCallback,
audience:
'did:ion:EiBWe9RtHT7VZ-Juff8OnnJAyFJtCokcYHx1CQkFtpl7pw:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJrZXktMSIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkNfT1VKeEg2aUljQzZYZE5oN0ptQy1USFhBVmFYbnZ1OU9FRVo4dHE5TkkiLCJraWQiOiJrZXktMSJ9LCJwdXJwb3NlcyI6WyJhdXRoZW50aWNhdGlvbiJdLCJ0eXBlIjoiSnNvbldlYktleTIwMjAifV19fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNYTkJqSWZMVGZOV0NHMFQ2M2VaYmJEZFZoSmJUTjgtSmZlaUx4dW1oZW53In0sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlCZVZ5RXBDb0NPeXJ6VDhDSHlvQW1acU1CT1o0VTZqcm1sdUt1SjlxS0pkZyIsInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQnhkcHlyamlVSFZ1akNRWTBKMkhBUFFYZnNwWFBKYWluV21mV3RNcFhneFEifX0',
verification: {
presentationVerificationCallback,
revocationOpts: {
revocationVerification: RevocationVerification.NEVER,
},
},
},
presentationDefinitions,
})).rejects.toThrowError()

presentationDefinitions,
}),
).rejects.toThrowError()
})
})

Expand Down Expand Up @@ -359,7 +360,7 @@ class TestVectors {
public static issuerKey: KeyLike
public static issuerPrivateKey: string
public static issuerPublicKey: string
public static issuerHexPrivateKey : string
public static issuerHexPrivateKey: string

public static holderJwk = {
kty: 'OKP',
Expand Down
11 changes: 7 additions & 4 deletions packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
IVerifiablePresentation,
PresentationSubmission,
W3CVerifiablePresentation,
WrappedVerifiablePresentation
WrappedVerifiablePresentation,
} from '@sphereon/ssi-types'

import { AuthorizationRequest } from '../authorization-request'
Expand All @@ -19,7 +19,7 @@ import {
RevocationVerification,
SIOPErrors,
SupportedVersion,
VerifiedOpenID4VPSubmission
VerifiedOpenID4VPSubmission,
} from '../types'

import { AuthorizationResponse } from './AuthorizationResponse'
Expand All @@ -29,7 +29,7 @@ import {
PresentationDefinitionWithLocation,
PresentationVerificationCallback,
VerifyAuthorizationResponseOpts,
VPTokenLocation
VPTokenLocation,
} from './types'

function extractNonceFromWrappedVerifiablePresentation(wrappedVp: WrappedVerifiablePresentation): string | undefined {
Expand Down Expand Up @@ -158,7 +158,10 @@ export const createPresentationSubmission = async (
console.log(`No submission_data in VPs and not provided. Will try to deduce, but it is better to create the submission data beforehand`)
for (const definitionOpt of opts.presentationDefinitions) {
const definition = 'definition' in definitionOpt ? definitionOpt.definition : definitionOpt
const result = new PEX().evaluatePresentation(definition, wrappedPresentation.original, { generatePresentationSubmission: true, presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL })
const result = new PEX().evaluatePresentation(definition, wrappedPresentation.original, {
generatePresentationSubmission: true,
presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
})
if (result.areRequiredCredentialsPresent) {
submission = result.value
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {
SelectResults,
Status,
VerifiablePresentationFromOpts,
VerifiablePresentationResult
VerifiablePresentationResult,
} from '@sphereon/pex'
import { Format, PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models'
import { PresentationEvaluationResults } from '@sphereon/pex/dist/main/lib/evaluation/core'
import { Format, PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models'
import {
CredentialMapper,
Hasher,
Expand All @@ -18,7 +18,7 @@ import {
OriginalVerifiableCredential,
OriginalVerifiablePresentation,
W3CVerifiablePresentation,
WrappedVerifiablePresentation
WrappedVerifiablePresentation,
} from '@sphereon/ssi-types'

import { extractDataFromPath, getWithUrl } from '../helpers'
Expand All @@ -28,7 +28,7 @@ import {
PresentationDefinitionLocation,
PresentationDefinitionWithLocation,
PresentationSignCallback,
PresentationVerificationCallback
PresentationVerificationCallback,
} from './types'

export class PresentationExchange {
Expand Down

0 comments on commit 2f1fcee

Please sign in to comment.