Skip to content

Commit

Permalink
Merge pull request #12 from identity-com/feature/PMNT-1946_piiFactory
Browse files Browse the repository at this point in the history
Feature/pmnt 1946 pii factory
  • Loading branch information
kevinhcolgan authored Mar 31, 2020
2 parents de8c3b7 + 7c7111a commit 5596b7f
Show file tree
Hide file tree
Showing 22 changed files with 4,885 additions and 423 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ node_modules
coverage
/dist
.idea
.launch*
.vscode*
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Utility Library to securely handle verifiable presentations.

[![CircleCI](https://circleci.com/gh/identity-com/verifiable-presentations.svg?style=svg)](https://circleci.com/gh/identity-com/verifiable-presentations)

Verifiable-presentations provides methods to run operations over a collection of verifiable credentials and evidences, such as query for credential identifiers, search for claims, feth claim value, and non-cryptographically and cryptographically secure verify the data against the proofs.
Verifiable-presentations provides methods to run operations over a collection of verifiable credentials and evidences, such as query for credential identifiers, search for claims, fetch claim values, and non-cryptographically and cryptographically secure verify the data against the proofs.

[Documentation](https://identity-com.github.io/verifiable-presentations/)

Expand Down Expand Up @@ -46,4 +46,43 @@ claims = await presentationManager.findClaims(criteria);
const claimValue = await presentationManager.getClaimValue(claims[0]);
```

## PIIFactory

A class to allow easy extraction of PII from a DSR Response based on a specific dsrRequest implementation, with a given mapping and formatters specific to that DSR. It also allows generating a new Scope Request based on unique URL generation.

```
const mapping = {
first_name: { identifier: 'claim-cvc:Name.givenNames-v1' },
last_name: { identifier: 'claim-cvc:Name.familyNames-v1' },
date_of_birth: { identifier: 'claim-cvc:Document.dateOfBirth-v1' },
street: { identifier: 'claim-cvc:Identity.address-v1' },
};
const formatters = {
street: { format: claimValue => `${claimValue.street} ${claimValue.unit}` },
date_of_birth: { format: claimValue => `${claimValue.year}-${claimValue.month}-${claimValue.day}` },
};
const piiFactory = new PIIFactory(dsrRequest, mapping, formatters);
const eventsURL = 'https://testEvents';
const idvDid = 'did:ethr:0x1a88a35421a4a0d3e13fe4e8ebcf18e9a249dc5a';
const dsrResolver = {
id: '123',
// key pair generated purely for this test
signingKeys: {
xpub: '0414a08b13afa8d33c499ec828065775915ddf0301634d35e26c6cec4ad0f0f2b72c79e90357d47c7ba65a3c03bb22ac7e273c5d01494448a155df8a28da33b48d',
xprv: 'a4947aa34ce507e995a60a455582d97f3fd1163eba3dd990ea1541a8fa049828',
},
};
const urlGeneratorFn = evidenceName => `https://<test cloud provider>/<unique Id>/${evidenceName}.json`;
// generate a DSR
const dsr = await piiFactory.generateDSR(eventsURL, idvDid, dsrResolver, urlGeneratorFn);
// extract PII from a DSR response
const extractedPII = await piiFactory.extractPII(dsrResponse);
```

For more detailed working examples, please, refer to the tests in [`index.test.ts`](src/__tests__/index.test.ts).
61 changes: 53 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"devDependencies": {
"@types/jest": "^24.9.1",
"@types/node": "^12.12.31",
"@types/uuid": "^7.0.2",
"jest": "^24.9.0",
"prettier": "^1.19.1",
"ts-jest": "^24.3.0",
Expand All @@ -53,7 +54,9 @@
},
"dependencies": {
"@identity.com/credential-commons": "^1.0.32",
"@types/lodash": "^4.14.149",
"crypto": "^1.0.1"
"@identity.com/dsr": "^1.0.14",
"@types/ramda": "github:types/npm-ramda#dist",
"crypto": "^1.0.1",
"ramda": "^0.25.0"
}
}
194 changes: 194 additions & 0 deletions src/PIIFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import R from 'ramda';
import * as uuidv4 from 'uuid/v4';
import { VerifiablePresentationManager, ClaimCriteriaMap, CredentialArtifacts, Evidence } from './VerifiablePresentationManager';
import DsrResolver from '@identity.com/dsr';
import { Interface } from 'readline';
import { Credential } from './Credential';

const { ScopeRequest } = DsrResolver;
/**
* Get the names of the evidence documents promised by a DSR response
*
* EvidenceProofs in a DSR response look like this:
* "evidenceProofs": [{
* "name": "credential-cvc:IdDocument-v2",
* "proofs": {
* "idDocumentFront": {
* "data": "195f9bf62b1a807abe26828c13f29e443169cdc5f60b22b470bfa50eef55a5a4"
* }
* }
* }]
* For each entry in the list of evidence proofs, extract the actual document names
* @return {Array<String>}
*/
const evidenceProofsToDSRDocumentNames = (evidenceProofs: any = []) => R.pipe(
R.pluck('proofs'),
R.map(Object.keys),
R.flatten,
)(evidenceProofs);

interface RequestedDocument {
name?: string,
proofs?: object
}
/** returns the evidences document names from the provider DSR */
const dsrRequestedDocuments = (dsrRequest: object) => Object.keys(R.pathOr([], ['payload', 'channels', 'evidences'], dsrRequest));
/**
* Traverse the provided presentation credentials and creates a map of document proofs for each
* credential identifier
* @param {Array<Credential>} presentations: an array of credentials
* @param {Array<String>} requestedDocuments: documents that the DSR asks for
* @returns {Array}: e.g. {
* * name: 'credential-cvc:IdDocument-v2',
* * proofs: {
* * idDocumentFront: { data: '195f9bf62b1a807abe26828c13f29e443169cdc5f60b22b470bfa50eef55a5a4' },
* * }
* * }
*/
const evidenceProofsFromCredentials = (presentations: Credential[], requestedDocuments: any = []) => presentations
.map(
(credential: Credential) => {
const evidenceClaims = R.pathOr([], ['claim', 'document', 'evidences'], credential);
const filteredEvidenceClaims = R.pick(requestedDocuments, evidenceClaims);
return { name: credential.identifier, proofs: R.mergeAll(filteredEvidenceClaims) };
}
)
// only return evidence proofs for credentials that require evidence
.filter(evidenceProofsByCredential => R.not(R.isEmpty(evidenceProofsByCredential.proofs)));

/**
* Return the list of evidence documents that the DSR response promises
* @param {Object} dsrResponse
* @param {Object} dsrRequest
*/
const expectedEvidenceProofs = (dsrResponse: DSRResponse, dsrRequest: object) => {
const presentations: Credential[] = dsrResponse.verifiableData.map(R.prop('credential'));
return evidenceProofsFromCredentials(presentations, dsrRequestedDocuments(dsrRequest));
};



/**
* Add an upload url to an evidence channel, so the client knows where to send the evidence documents.
* The url will be generated by the client to be unique
* @param {Function} urlGeneratorFn: A function to generate a unique URL based on the evidence name provided
* @return {function(*, *): {url: *}}
*/
const addEvidenceUrl = (urlGeneratorFn: (evidenceName: string) => string) => (evidenceChannelConfiguration: object, evidenceName: string) => {
const url = urlGeneratorFn(evidenceName);
return {
...evidenceChannelConfiguration,
url,
};
};

export interface CredentialItemRequest {
requestIndex?: number,
identifier?: string,
constraints?: any,
}
export interface VerifiableDataItem {
credentialItemRequest?: CredentialItemRequest,
credential?: Credential,
requestStatus?: any[] | null,
userId?: string | null
}
export interface DSRResponse {
verifiableData?: VerifiableDataItem[],
requestStatus?: any[] | null,
userId?: string | null
}
export interface Formatters {
[key: string]: any;
}
/**
* A class for extracting PII from a DSR Response based on a specific dsrRequest implementation, with a given mapping and formatters,
* specific to that DSR
*/
export class PIIFactory {
dsrRequest: object;
mapping: ClaimCriteriaMap;
formatters: Formatters;
/**
* @param {Object} dsrRequest
* @param {ClaimCriteriaMap} mapping
* @param {Formatters} formatters
*/
constructor(dsrRequest: object, mapping: ClaimCriteriaMap, formatters: object) {
this.dsrRequest = dsrRequest;
this.mapping = mapping;
this.formatters = formatters;
};

/**
* The client is asked for a list of documents that the provider is interested in (requestedDocuments)
* It returns in the credential, a list of documents that it can provide.
* Return the intersection between these two, to identify which documents are expected.
*
* @param evidenceProofs
* @return {string[]}
*/
expectedDocumentsGivenEvidenceProofs(evidenceProofs?: any[]) {
const promisedDocuments = evidenceProofsToDSRDocumentNames(evidenceProofs);
return R.intersection(dsrRequestedDocuments(this.dsrRequest), promisedDocuments);
};

/**
* Validate the dsr response, extract the PII, and format it for the provider.
* @param dsrResponse
* @return {Promise<{evidenceProofs: *, formattedClaims: *}>}
*/
async extractPII(dsrResponse: DSRResponse) {
const presentations: Credential[] = dsrResponse.verifiableData.map(R.prop('credential'));
const evidenceProofs = expectedEvidenceProofs(dsrResponse, this.dsrRequest);
const artifacts: CredentialArtifacts = {
presentations,
evidences: [],
};
try {
const verifiablePresentation = new VerifiablePresentationManager({
skipAddVerify: true, skipGetVerify: true, allowGetUnverified: true,
});
// this throws an error if the DSR response is invalid
await verifiablePresentation.addCredentialArtifacts(artifacts);

// using the credential to provider mapping, get the credentials values
const mappedClaimValues = await verifiablePresentation.mapClaimValues(this.mapping);

const formatIfFormatterExists = (value: any, key: string) => (value && this.formatters[key] ? this.formatters[key].format(value) : value);
const formattedClaims = R.mapObjIndexed(formatIfFormatterExists, mappedClaimValues);

return { formattedClaims, evidenceProofs };
} catch (error) {
throw new Error('The dsr response on the requirements is invalid');
}
};
/**
* Generate a DSR based on the template and evidence functions provided
* @param {String} eventsURL
* @param {String} idvDid
* @param {Object} dsrResolver
* @param {Function} urlGeneratorFn
*/
generateDSR(eventsURL: string, idvDid: string, dsrResolver: object, urlGeneratorFn: (evidenceName: string) => string) {

if (!this.dsrRequest) { throw new Error('DSR not provided'); }

const uuid = uuidv4.default();
const requestedItems = R.pathOr([], ['payload', 'credentialItems'], this.dsrRequest);
// iterate over the requested items array, set the values on the path constraints.meta.issue.is.$eq for the idv value
const updatedRequestedItems = R.map(R.assocPath((['constraints', 'meta', 'issuer', 'is', '$eq']), idvDid))(requestedItems);

// add a fresh S3 upload url to each evidence channel, tailored for this user
const evidenceChannelTemplate = R.pathOr([], ['payload', 'channels', 'evidences'], this.dsrRequest);
const evidences = R.mapObjIndexed(addEvidenceUrl(urlGeneratorFn), evidenceChannelTemplate);

const channelsConfig = {
eventsURL,
evidences,
};
const appConfig = R.pathOr([], ['payload', 'requesterInfo', 'app'], this.dsrRequest);

return ScopeRequest.buildSignedRequestBody(new ScopeRequest.ScopeRequest(uuid, updatedRequestedItems, channelsConfig, appConfig, dsrResolver));
};
}
Loading

0 comments on commit 5596b7f

Please sign in to comment.