Skip to content

Commit

Permalink
Merge pull request #94 from Sphereon-Opensource/develop
Browse files Browse the repository at this point in the history
New release
  • Loading branch information
nklomp authored Feb 29, 2024
2 parents eb17a5c + 8e592ac commit 753a10f
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 44 deletions.
12 changes: 12 additions & 0 deletions packages/common/lib/types/CredentialIssuance.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,22 @@ export interface ProofOfPossessionCallbacks<DIDDoc> {
verifyCallback?: JWTVerifyCallback<DIDDoc>;
}

/**
* Signature algorithms.
*
* TODO: Move towards string literal unions and string type, given we do not provide signature/key implementations in this library to begin with
* @See: https://github.com/Sphereon-Opensource/OID4VCI/issues/88
*/
export enum Alg {
EdDSA = 'EdDSA',
ES256 = 'ES256',
ES256K = 'ES256K',
PS256 = 'PS256',
PS384 = 'PS384',
PS512 = 'PS512',
RS256 = 'RS256',
RS384 = 'RS384',
RS512 = 'RS512',
}

export type Typ =
Expand Down
4 changes: 2 additions & 2 deletions packages/common/lib/types/OpenID4VCIErrors.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Alg } from './CredentialIssuance.types';
import { Alg } from './CredentialIssuance.types'

export const BAD_PARAMS = 'Wrong parameters provided';
export const URL_NOT_VALID = 'Request url is not valid';
export const JWS_NOT_VALID = 'JWS is not valid';
export const PROOF_CANT_BE_CONSTRUCTED = "Proof can't be constructed.";
export const NO_JWT_PROVIDED = 'No JWT provided';
export const TYP_ERROR = 'Typ must be "openid4vci-proof+jwt"';
export const ALG_ERROR = `Algorithm is a required field and must be one of: ${Object.keys(Alg).join(', ')}`;
export const ALG_ERROR = `Algorithm is a required field, you are free to use the signing algorithm of your choice or one of the following: ${Object.keys(Alg).join(', ')}`;
export const KID_JWK_X5C_ERROR = 'Only one must be present: kid, jwk or x5c';
export const KID_DID_NO_DID_ERROR = 'A DID value needs to be returned when kid is present';
export const DID_NO_DIDDOC_ERROR = 'A DID Document needs to be resolved when a DID is encountered';
Expand Down
2 changes: 1 addition & 1 deletion packages/common/lib/types/StateManager.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface CredentialOfferSession extends StateType {
export enum IssueStatus {
OFFER_CREATED = 'OFFER_CREATED',
OFFER_URI_RETRIEVED = 'OFFER_URI_RETRIEVED', // This state is optional. as an offer uri is optional
ACCESS_TOKEN_REQUESTED = 'ACCESS_TOKEN_CREATED', // Optional state, given the token endpoint could also be on a separate AS
ACCESS_TOKEN_REQUESTED = 'ACCESS_TOKEN_REQUESTED', // Optional state, given the token endpoint could also be on a separate AS
ACCESS_TOKEN_CREATED = 'ACCESS_TOKEN_CREATED', // Optional state, given the token endpoint could also be on a separate AS
CREDENTIAL_REQUEST_RECEIVED = 'CREDENTIAL_REQUEST_RECEIVED', // Credential request received. Next state would either be error or issued
CREDENTIAL_ISSUED = 'CREDENTIAL_ISSUED',
Expand Down
12 changes: 8 additions & 4 deletions packages/issuer/lib/VcIssuer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Alg,
ALG_ERROR,
AUD_ERROR,
CNonceState,
Expand Down Expand Up @@ -33,14 +32,19 @@ import {
toUniformCredentialOfferRequest,
TYP_ERROR,
UniformCredentialRequest,
URIState,
URIState
} from '@sphereon/oid4vci-common'
import { CompactSdJwtVc, CredentialMapper, W3CVerifiableCredential } from '@sphereon/ssi-types'
import { v4 } from 'uuid'

import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions'
import { LookupStateManager } from './state-manager'
import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types'
import {
CredentialDataSupplier,
CredentialDataSupplierArgs,
CredentialIssuanceInput,
CredentialSignerCallback
} from './types'

const SECOND = 1000

Expand Down Expand Up @@ -463,7 +467,7 @@ export class VcIssuer<DIDDoc extends object> {

if (typ !== 'openid4vci-proof+jwt') {
throw Error(TYP_ERROR)
} else if (!alg || !(alg in Alg)) {
} else if (!alg) {
throw Error(ALG_ERROR)
} else if (!([kid, jwk, x5c].filter((x) => !!x).length === 1)) {
// only 1 is allowed, but need to look into whether jwk and x5c are allowed together
Expand Down
184 changes: 147 additions & 37 deletions packages/issuer/lib/__tests__/VcIssuer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { OpenID4VCIClient } from '@sphereon/oid4vci-client'
import {
Alg,
ALG_ERROR,
CredentialOfferSession,
CredentialSupported,
IssuerCredentialSubjectDisplay,
IssueStatus,
STATE_MISSING_ERROR,
STATE_MISSING_ERROR
} from '@sphereon/oid4vci-common'
import { IProofPurpose, IProofType } from '@sphereon/ssi-types'
import { DIDDocument } from 'did-resolver'
Expand All @@ -16,13 +17,36 @@ import { MemoryStates } from '../state-manager'

const IDENTIPROOF_ISSUER_URL = 'https://issuer.research.identiproof.io'

const verifiableCredential = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://w3id.org/security/suites/jws-2020/v1'
],
id: 'http://university.example/credentials/1872',
type: [
'VerifiableCredential',
'ExampleAlumniCredential'
],
issuer: 'https://university.example/issuers/565049',
issuanceDate: new Date().toISOString(),
credentialSubject: {
id: 'did:example:ebfeb1f712ebc6f1c276e12ec21',
alumniOf: {
id: 'did:example:c276e12ec21ebfeb1f712ebc6f1',
name: 'Example University'
}
}
}

describe('VcIssuer', () => {
let vcIssuer: VcIssuer<DIDDocument>
const issuerState = 'previously-created-state'
const clientId = 'sphereon:wallet'
const preAuthorizedCode = 'test_code'

beforeAll(async () => {
const jwtVerifyCallback: jest.Mock = jest.fn()

beforeEach(async () => {
jest.clearAllMocks()
const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11()
.withCryptographicSuitesSupported('ES256K')
Expand Down Expand Up @@ -105,29 +129,7 @@ describe('VcIssuer', () => {
},
}),
)
.withJWTVerifyCallback(() =>
Promise.resolve({
did: 'did:example:1234',
kid: 'did:example:1234#auth',
alg: 'ES256k',
didDocument: {
'@context': 'https://www.w3.org/ns/did/v1',
id: 'did:example:1234',
},
jwt: {
header: {
typ: 'openid4vci-proof+jwt',
alg: Alg.ES256K,
kid: 'test-kid',
},
payload: {
aud: 'https://credential-issuer',
iat: +new Date(),
nonce: 'test-nonce',
},
},
}),
)
.withJWTVerifyCallback(jwtVerifyCallback)
.build()
})

Expand Down Expand Up @@ -272,6 +274,29 @@ describe('VcIssuer', () => {

// Of course this doesn't work. The state is part of the proof to begin with
it('should fail issuing credential if an invalid state is used', async () => {
jwtVerifyCallback.mockResolvedValue({
did: 'did:example:1234',
kid: 'did:example:1234#auth',
alg: Alg.ES256K,
didDocument: {
'@context': 'https://www.w3.org/ns/did/v1',
id: 'did:example:1234',
},
jwt: {
header: {
typ: 'openid4vci-proof+jwt',
alg: Alg.ES256K,
kid: 'test-kid',
},
payload: {
aud: IDENTIPROOF_ISSUER_URL,
iat: +new Date(),
nonce: 'test-nonce',
},
}
}
)

await expect(
vcIssuer.issueCredential({
credentialRequest: {
Expand All @@ -287,23 +312,65 @@ describe('VcIssuer', () => {
).rejects.toThrow(Error(STATE_MISSING_ERROR + ' (test-nonce)'))
})

// Of course this doesn't work. The state is part of the proof to begin with
xit('should issue credential if a valid state is passed in', async () => {
await expect(
it.each([...Object.values<string>(Alg), 'CUSTOM'])('should issue %s signed credential if a valid state is passed in', async (alg: string) => {
jwtVerifyCallback.mockResolvedValue({
did: 'did:example:1234',
kid: 'did:example:1234#auth',
alg: alg,
didDocument: {
'@context': 'https://www.w3.org/ns/did/v1',
id: 'did:example:1234',
},
jwt: {
header: {
typ: 'openid4vci-proof+jwt',
alg: alg,
kid: 'test-kid',
},
payload: {
aud: IDENTIPROOF_ISSUER_URL,
iat: +new Date(),
nonce: 'test-nonce',
},
}
}
)

let createdAt = +new Date()
await vcIssuer.cNonces.set('test-nonce', {
cNonce: 'test-nonce',
preAuthorizedCode: 'test-pre-authorized-code',
createdAt: createdAt
})
await vcIssuer.credentialOfferSessions.set('test-pre-authorized-code', {
createdAt: createdAt,
preAuthorizedCode: 'test-pre-authorized-code',
credentialOffer: {
credential_offer: {
credential_issuer: 'did:key:test',
credentials: []
}
},
lastUpdatedAt: createdAt,
status: IssueStatus.ACCESS_TOKEN_CREATED
})

expect(
vcIssuer.issueCredential({
credential: verifiableCredential,
credentialRequest: {
types: ['VerifiableCredential'],
format: 'jwt_vc_json',
proof: {
proof_type: 'jwt',
jwt: 'ye.ye.ye',
},
jwt: 'ye.ye.ye'
}
},
// issuerState,
}),
newCNonce: 'new-test-nonce'
})
).resolves.toEqual({
c_nonce: expect.any(String),
c_nonce_expires_in: 90000,
c_nonce: 'new-test-nonce',
c_nonce_expires_in: 300000,
credential: {
'@context': ['https://www.w3.org/2018/credentials/v1'],
credentialSubject: {},
Expand All @@ -314,11 +381,54 @@ describe('VcIssuer', () => {
jwt: 'ye.ye.ye',
proofPurpose: 'assertionMethod',
type: 'JwtProof2020',
verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf',
verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf'
},
type: ['VerifiableCredential'],
type: ['VerifiableCredential']
},
format: 'jwt_vc_json',
format: 'jwt_vc_json'
})
})

it('should fail issuing credential if the signing algorithm is missing', async () => {
let createdAt = +new Date()
await vcIssuer.cNonces.set('test-nonce', {
cNonce: 'test-nonce',
preAuthorizedCode: 'test-pre-authorized-code',
createdAt: createdAt
})

jwtVerifyCallback.mockResolvedValue({
did: 'did:example:1234',
kid: 'did:example:1234#auth',
alg: undefined,
didDocument: {
'@context': 'https://www.w3.org/ns/did/v1',
id: 'did:example:1234',
},
jwt: {
header: {
typ: 'openid4vci-proof+jwt',
alg: undefined,
kid: 'test-kid',
},
payload: {
aud: IDENTIPROOF_ISSUER_URL,
iat: +new Date(),
nonce: 'test-nonce',
},
}
}
)

expect(vcIssuer.issueCredential({
credentialRequest: {
types: ['VerifiableCredential'],
format: 'jwt_vc_json',
proof: {
proof_type: 'jwt',
jwt: 'ye.ye.ye',
},
},
})).rejects.toThrow(Error(ALG_ERROR))
})
})

0 comments on commit 753a10f

Please sign in to comment.