diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c3952d6c..809ebffb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,7 @@ jobs: LOGTO_WEBHOOK_SECRET: ${{ secrets.LOGTO_WEBHOOK_SECRET }} MAINNET_RPC_URL: ${{ vars.MAINNET_RPC_URL }} POLYGON_PRIVATE_KEY: ${{ secrets.POLYGON_PRIVATE_KEY }} + POLYGON_RPC_URL: ${{ secrets.POLYGON_RPC_URL }} RESOLVER_URL: ${{ vars.RESOLVER_URL }} TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }} TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} diff --git a/src/controllers/credentials.ts b/src/controllers/credentials.ts index 57759039..83a0221e 100644 --- a/src/controllers/credentials.ts +++ b/src/controllers/credentials.ts @@ -11,10 +11,16 @@ import { Cheqd } from '@cheqd/did-provider-cheqd'; import { OPERATION_CATEGORY_NAME_CREDENTIAL } from '../types/constants.js'; import { CheqdW3CVerifiableCredential } from '../services/w3c-credential.js'; import { isCredentialIssuerDidDeactivated } from '../services/helpers.js'; +import { VeridaDIDValidator } from './validator/did.js'; export class CredentialController { public static issueValidator = [ check(['subjectDid', 'issuerDid']).exists().withMessage('DID is required').bail().isDID().bail(), + check('subjectDid') + .custom((value, { req }) => + new VeridaDIDValidator().validate(value).valid ? !!req.body.credentialSchema : true + ) + .withMessage('credentialSchema is required for a verida DID subject'), check('attributes') .exists() .withMessage('attributes are required') diff --git a/src/controllers/key.ts b/src/controllers/key.ts index ea83c2ba..03af7394 100644 --- a/src/controllers/key.ts +++ b/src/controllers/key.ts @@ -11,7 +11,7 @@ export class KeyController { check('encrypted') .isBoolean() .withMessage('encrypted is required') - .custom((value, { req }) => (value === true ? req.ivHex && req.salt : true)) + .custom((value, { req }) => (value === true ? req.body.ivHex && req.body.salt : true)) .withMessage('Property ivHex, salt is required when encrypted is set to true') .bail(), ]; diff --git a/src/controllers/validator/did.ts b/src/controllers/validator/did.ts index b8f02b7d..a0a2251b 100644 --- a/src/controllers/validator/did.ts +++ b/src/controllers/validator/did.ts @@ -1,6 +1,6 @@ import { CheqdNetwork } from '@cheqd/sdk'; import type { IValidationResult, IValidator, Validatable } from './validator.js'; -import { CheqdIdentifierValidator, KeyIdentifierValidator } from './identifier.js'; +import { CheqdIdentifierValidator, KeyIdentifierValidator, VeridaIdentifierValidator } from './identifier.js'; export class BaseDidValidator implements IValidator { validate(did: Validatable): IValidationResult { @@ -140,12 +140,81 @@ export class KeyDIDValidator extends BaseDidValidator implements IValidator { } } +export class VeridaDIDValidator extends BaseDidValidator implements IValidator { + protected identifierValidator: IValidator; + subject = 'vda'; + + constructor(identifierValidator?: IValidator) { + super(); + // Setup default CheqdIdentifierValidator + if (!identifierValidator) { + identifierValidator = new VeridaIdentifierValidator(); + } + this.identifierValidator = identifierValidator; + } + + public printable(): string { + return this.subject; + } + + validate(did: Validatable): IValidationResult { + // Call base validation + let _v = super.validate(did); + if (!_v.valid) { + return _v; + } + did = did as string; + // Check if DID is vda + const method = did.split(':')[1]; + if (method != this.subject) { + return { + valid: false, + error: 'DID Verida should have "did:vda:" prefix', + }; + } + + // Check namepsace + const namespace = did.split(':')[2]; + if (!namespace) { + return { + valid: false, + error: 'Verida DID namespace is required ("did:vda:mainnet:..." or "did:vda:testnet:...")', + }; + } + // Check if namespace is valid + if (namespace !== CheqdNetwork.Testnet && namespace !== CheqdNetwork.Mainnet) { + return { + valid: false, + error: `Verida DID namespace must be ${CheqdNetwork.Testnet} or ${CheqdNetwork.Mainnet}`, + }; + } + + // Check identifier + const id = did.split(':')[3]; + if (!id) { + return { + valid: false, + error: 'Identifier is required after "did:vda::" prefix', + }; + } + // Check that identifier is valid + _v = this.identifierValidator.validate(id); + if (!_v.valid) { + return { + valid: false, + error: _v.error, + }; + } + return { valid: true }; + } +} + export class DIDValidator implements IValidator { protected didValidators: IValidator[]; constructor(didValidators?: IValidator[]) { if (!didValidators) { - didValidators = [new CheqdDIDValidator(), new KeyDIDValidator()]; + didValidators = [new CheqdDIDValidator(), new KeyDIDValidator(), new VeridaDIDValidator()]; } this.didValidators = didValidators; diff --git a/src/controllers/validator/identifier.ts b/src/controllers/validator/identifier.ts index 5b9e33f8..4819a327 100644 --- a/src/controllers/validator/identifier.ts +++ b/src/controllers/validator/identifier.ts @@ -92,3 +92,17 @@ export class KeyIdentifierValidator implements IValidator { return { valid: true }; } } + +export class VeridaIdentifierValidator implements IValidator { + validate(id: Validatable): IValidationResult { + if (typeof id !== 'string') { + return { + valid: false, + error: 'Verida DID identifier should be a string', + }; + } + id = id as string; + // ToDo add more checks for did:vda identifier + return { valid: true }; + } +} diff --git a/tests/e2e/credential/issue-verify-flow.spec.ts b/tests/e2e/credential/issue-verify-flow.spec.ts index 959c8c62..df6f2af5 100644 --- a/tests/e2e/credential/issue-verify-flow.spec.ts +++ b/tests/e2e/credential/issue-verify-flow.spec.ts @@ -10,7 +10,9 @@ test.use({ storageState: 'playwright/.auth/user.json' }); let jwtCredential: VerifiableCredential, jsonldCredential: VerifiableCredential; test(' Issue a jwt credential', async ({ request }) => { - const credentialData = JSON.parse(fs.readFileSync(`${PAYLOADS_PATH.CREDENTIAL}/credential-issue-jwt.json`, 'utf-8')); + const credentialData = JSON.parse( + fs.readFileSync(`${PAYLOADS_PATH.CREDENTIAL}/credential-issue-jwt.json`, 'utf-8') + ); const response = await request.post(`/credential/issue`, { data: JSON.stringify(credentialData), headers: { @@ -48,7 +50,9 @@ test(' Verify a jwt credential', async ({ request }) => { }); test(' Issue a jwt credential with a deactivated DID', async ({ request }) => { - const credentialData = JSON.parse(fs.readFileSync(`${PAYLOADS_PATH.CREDENTIAL}/credential-issue-jwt.json`, 'utf-8')); + const credentialData = JSON.parse( + fs.readFileSync(`${PAYLOADS_PATH.CREDENTIAL}/credential-issue-jwt.json`, 'utf-8') + ); credentialData.issuerDid = 'did:cheqd:testnet:edce6dfb-b59c-493b-a4b8-1d16a6184349'; const response = await request.post(`/credential/issue`, { data: JSON.stringify(credentialData), @@ -60,7 +64,9 @@ test(' Issue a jwt credential with a deactivated DID', async ({ request }) => { }); test(' Issue a jsonLD credential', async ({ request }) => { - const credentialData = JSON.parse(fs.readFileSync(`${PAYLOADS_PATH.CREDENTIAL}/credential-issue-jsonld.json`, 'utf-8')); + const credentialData = JSON.parse( + fs.readFileSync(`${PAYLOADS_PATH.CREDENTIAL}/credential-issue-jsonld.json`, 'utf-8') + ); const response = await request.post(`/credential/issue`, { data: JSON.stringify(credentialData), headers: { @@ -97,3 +103,28 @@ test(' Verify a jsonld credential', async ({ request }) => { expect(response.status()).toBe(StatusCodes.OK); expect(result.verified).toBe(true); }); + +test(' Issue a jwt credential to a verida DID holder', async ({ request }) => { + const credentialData = JSON.parse( + fs.readFileSync(`${PAYLOADS_PATH.CREDENTIAL}/credential-issue-vda.json`, 'utf-8') + ); + const response = await request.post(`/credential/issue`, { + data: JSON.stringify(credentialData), + headers: { + 'Content-Type': CONTENT_TYPE.APPLICATION_JSON, + }, + }); + const credential = await response.json(); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + expect(credential.proof.type).toBe('JwtProof2020'); + expect(credential.proof).toHaveProperty('jwt'); + expect(typeof credential.issuer === 'string' ? credential.issuer : credential.issuer.id).toBe( + credentialData.issuerDid + ); + expect(credential.type).toContain('VerifiableCredential'); + expect(credential.credentialSubject).toMatchObject({ + ...credentialData.attributes, + id: credentialData.subjectDid, + }); +}); diff --git a/tests/e2e/payloads/credential/credential-issue-vda.json b/tests/e2e/payloads/credential/credential-issue-vda.json new file mode 100644 index 00000000..3e5b6e66 --- /dev/null +++ b/tests/e2e/payloads/credential/credential-issue-vda.json @@ -0,0 +1,20 @@ +{ + "@context": ["https://common.schemas.verida.io/identity/kyc/FinClusive/individual-basic/v0.1.0/schema.json"], + "attributes": { + "firstName": "Alice", + "lastName": "Nikitin", + "dateOfBirth": "1984/07/03", + "streetAddress1": "321A", + "suburb": "Orange", + "state": "California", + "postcode": "90210", + "complianceProfileLevel": "member", + "complianceStatus": "active", + "creationDate": "2023/05/03", + "validUntil": "2032/05/03" + }, + "credentialSchema": "https://common.schemas.verida.io/identity/kyc/FinClusive/individual-basic/v0.1.0/schema.json", + "format": "jwt", + "issuerDid": "did:cheqd:testnet:4JdgsZ4A8LegKXdsKE3v6X", + "subjectDid": "did:vda:testnet:0xdd5bB6467Cae1513ce253738332faBB3206b9583" +}