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

fix: Support did:vda in validator [DEV-3498] #459

Merged
merged 7 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
6 changes: 6 additions & 0 deletions src/controllers/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
Expand Down
73 changes: 71 additions & 2 deletions src/controllers/validator/did.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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:<namespace>:" 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;
Expand Down
14 changes: 14 additions & 0 deletions src/controllers/validator/identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
}
37 changes: 34 additions & 3 deletions tests/e2e/credential/issue-verify-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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),
Expand All @@ -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: {
Expand Down Expand Up @@ -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,
});
});
20 changes: 20 additions & 0 deletions tests/e2e/payloads/credential/credential-issue-vda.json
Original file line number Diff line number Diff line change
@@ -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"
}
Loading