diff --git a/demo/src/demo.ts b/demo/src/demo.ts index 0221416d..30cbaad7 100644 --- a/demo/src/demo.ts +++ b/demo/src/demo.ts @@ -22,10 +22,10 @@ import { generateRequestCredentialMessage } from './utils/request_credential_mes import { getChainCredits, addAuthority } from './utils/createAuthorities' import { createAccount } from './utils/createAccount' -import type { - SignCallback, - // DocumenentMetaData, -} from '@cord.network/types' +// import type { +// SignCallback, +// // DocumenentMetaData, +// } from '@cord.network/types' function getChallenge(): string { return Cord.Utils.UUID.generate() @@ -94,8 +94,7 @@ async function main() { await createDid(authorIdentity) const delegateOneKeys = generateKeypairs(delegateOneMnemonic) console.log( - `šŸ› Delegate (${delegateOneDid?.assertionMethod![0].type}): ${ - delegateOneDid.uri + `šŸ› Delegate (${delegateOneDid?.assertionMethod![0].type}): ${delegateOneDid.uri }` ) // Create Delegate Two DID @@ -103,8 +102,7 @@ async function main() { await createDid(authorIdentity) const delegateTwoKeys = generateKeypairs(delegateTwoMnemonic) console.log( - `šŸ› Delegate (${delegateTwoDid?.assertionMethod![0].type}): ${ - delegateTwoDid.uri + `šŸ› Delegate (${delegateTwoDid?.assertionMethod![0].type}): ${delegateTwoDid.uri }` ) // Create Delegate 3 DID @@ -112,8 +110,7 @@ async function main() { await createDid(authorIdentity) const delegate3Keys = generateKeypairs(delegate3Mnemonic) console.log( - `šŸ› Delegate (${delegate3Did?.assertionMethod![0].type}): ${ - delegate3Did.uri + `šŸ› Delegate (${delegate3Did?.assertionMethod![0].type}): ${delegate3Did.uri }` ) console.log('āœ… Identities created!') @@ -208,13 +205,13 @@ async function main() { schema, registryDelegate, registry.identifier, - callBackFn + callBackFn ) console.dir(document, { depth: null, colors: true, }) - let x = await createStream( + await createStream( delegateTwoDid.uri, authorIdentity, async ({ data }) => ({ @@ -225,69 +222,71 @@ async function main() { ) console.log('āœ… Credential created!') - console.log('šŸ–ļø Stream update...') - let newContent: any = { - name: 'Adi', - age: 23, - id: '123456789987654311', - gender: 'Male', - country: 'India', - } - - const updatedDocument = await Cord.Document.updateStream( - document, - newContent, - schema, - callBackFn, - {} - ) - console.log('šŸ”– Document after the updation\n', updatedDocument) - - console.log('āš“ Anchoring the updated document on the blockchain...') - const api = Cord.ConfigService.get('api') - const { streamHash } = Cord.Stream.fromDocument(updatedDocument) - const authorization = Cord.Registry.uriToIdentifier( - updatedDocument.authorization - ) - const streamTx = api.tx.stream.update( - updatedDocument.identifier.replace('stream:cord:', ''), - // updatedDocument.identifier, - streamHash, - authorization - ) + // console.log('šŸ–ļø Stream update...') + // let newContent: any = { + // name: 'Adi', + // age: 23, + // id: '123456789987654311', + // gender: 'Male', + // country: 'India', + // } + // + // const updatedDocument = await Cord.Document.updateStream( + // document, + // newContent, + // schema, + // callBackFn, + // {} + // ) + // console.log('šŸ”– Document after the updation\n', updatedDocument) + // + // console.log('āš“ Anchoring the updated document on the blockchain...') + // const api = Cord.ConfigService.get('api') + // const { streamHash } = Cord.Stream.fromDocument(updatedDocument) + // const authorization = Cord.Registry.uriToIdentifier( + // updatedDocument.authorization + // ) + // const streamTx = api.tx.stream.update( + // updatedDocument.identifier.replace('stream:cord:', ''), + // // updatedDocument.identifier, + // streamHash, + // authorization + // ) + // + // const authorizedStreamTx = await Cord.Did.authorizeTx( + // delegateTwoDid.uri, + // streamTx, + // async ({ data }) => ({ + // signature: delegateTwoKeys.assertionMethod.sign(data), + // keyType: delegateTwoKeys.assertionMethod.type, + // }), + // authorIdentity.address + // ) + // try { + // await Cord.Chain.signAndSubmitTx(authorizedStreamTx, authorIdentity) + // } + // catch (e) { + // console.log('Error: \n', e.message) + // } + // - const authorizedStreamTx = await Cord.Did.authorizeTx( - delegateTwoDid.uri, - streamTx, - async ({ data }) => ({ - signature: delegateTwoKeys.assertionMethod.sign(data), - keyType: delegateTwoKeys.assertionMethod.type, - }), - authorIdentity.address - ) - try{ - await Cord.Chain.signAndSubmitTx(authorizedStreamTx, authorIdentity) - } - catch(e) { - console.log('Error: \n',e.message) - } - - // Step 5: Create a Presentation - console.log(`\nā„ļø Presentation Creation `) + console.log(`\nā„ļø Selective Disclosure Presentation Creation `) const challenge = getChallenge() - const presentation = await createPresentation( - updatedDocument, - async ({ data }) => ({ + const presentation = await createPresentation({ + document: document, + signCallback: async ({ data }) => ({ signature: holderKeys.authentication.sign(data), keyType: holderKeys.authentication.type, keyUri: `${holderDid.uri}${holderDid.authentication[0].id}`, }), - ['name', 'id'], - challenge - ) + // Comment the below line to have a full disclosure + selectedAttributes: ['name', 'id', 'address.pin', 'address.location',], + challenge: challenge + }); + console.dir(presentation, { depth: null, colors: true, @@ -302,30 +301,33 @@ async function main() { }) if (isValid) { - console.log('āœ… 301 :Verification successful! šŸŽ‰') + console.log('āœ… Verification successful! šŸŽ‰') } else { - console.log('āœ… 301 :Verification failed! šŸš«') + console.log('āœ… Verification failed! šŸš«') } - console.log(`\nā„ļø Messaging `) - const schemaId = Cord.Schema.idToChain(schema.$id) - console.log(' Generating the message - Sender -> Receiver') - const message = await generateRequestCredentialMessage( - holderDid.uri, - verifierDid.uri, - schemaId - ) - - console.log(' Encrypting the message - Sender -> Receiver') - const encryptedMessage = await encryptMessage( - message, - holderDid.uri, - verifierDid.uri, - holderKeys.keyAgreement - ) + // Uncomment the following section to enable messaging demo + // + // console.log(`\nā„ļø Messaging `) + // const schemaId = Cord.Schema.idToChain(schema.$id) + // console.log(' Generating the message - Sender -> Receiver') + // const message = await generateRequestCredentialMessage( + // holderDid.uri, + // verifierDid.uri, + // schemaId + // ) + // + // console.log(' Encrypting the message - Sender -> Receiver') + // const encryptedMessage = await encryptMessage( + // message, + // holderDid.uri, + // verifierDid.uri, + // holderKeys.keyAgreement + // ) + // + // console.log(' Decrypting the message - Receiver') + // await decryptMessage(encryptedMessage, verifierKeys.keyAgreement) - console.log(' Decrypting the message - Receiver') - await decryptMessage(encryptedMessage, verifierKeys.keyAgreement) // Step 7: Revoke a Credential console.log(`\nā„ļø Revoke credential - ${document.identifier}`) diff --git a/demo/src/utils/createDocument.ts b/demo/src/utils/createDocument.ts index 6a437b5c..79a8e2f4 100644 --- a/demo/src/utils/createDocument.ts +++ b/demo/src/utils/createDocument.ts @@ -27,7 +27,11 @@ export async function createDocument( country: 'India', address: { street: 'a', - pin: 54032 + pin: 54032, + location: { + state: 'karnataka', + country: 'india' + } } }, holder, diff --git a/demo/src/utils/createPresentation.ts b/demo/src/utils/createPresentation.ts index 3913208f..f96560a3 100644 --- a/demo/src/utils/createPresentation.ts +++ b/demo/src/utils/createPresentation.ts @@ -9,17 +9,33 @@ import * as Cord from '@cord.network/sdk' * @param {string} [challenge] - A challenge string that will be signed by the user's private key. * @returns A promise that resolves to a document presentation. */ -export async function createPresentation( - document: Cord.IDocument, - signCallback: Cord.SignCallback, - selectedAttributes?: string[], - challenge?: string -): Promise { +export async function createPresentation({ + document, + signCallback, + selectedAttributes = [], + challenge, +}: Cord.PresentationOptions): Promise { // Create a presentation with only the specified fields revealed, if specified. return Cord.Document.createPresentation({ document, signCallback, selectedAttributes, challenge, - }) + }); } + + +// export async function createPresentation( +// document, +// signCallback: Cord.SignCallback, +// selectedAttributes?: string[], +// challenge?: string +// ): Promise { +// // Create a presentation with only the specified fields revealed, if specified. +// return Cord.Document.createPresentation({ +// document, +// signCallback, +// selectedAttributes, +// challenge, +// }) +// } diff --git a/packages/modules/src/content/Content.ts b/packages/modules/src/content/Content.ts index 321140d5..3a30ae4c 100644 --- a/packages/modules/src/content/Content.ts +++ b/packages/modules/src/content/Content.ts @@ -11,7 +11,7 @@ import { Identifier, Crypto, DataUtils, - jsonabc, + // jsonabc, } from '@cord.network/utils' import * as Did from '@cord.network/did' import * as Schema from '../schema/index.js' @@ -26,41 +26,36 @@ const VC_VOCAB = 'https://www.w3.org/2018/credentials/v1' * @param expanded Return an expanded instead of a compacted represenation. While property transformation is done explicitely in the expanded format, it is otherwise done implicitly via adding JSON-LD's reserved `@context` properties while leaving [[IContent]][contents] property keys untouched. * @returns An object which can be serialized into valid JSON-LD representing an [[IContent]]'s ['contents']. */ + function jsonLDcontents( content: PartialContent, expanded = true ): Record { - const { schemaId, contents, holder, issuer } = content - if (!schemaId) new SDKErrors.SchemaIdentifierMissingError() - const vocabulary = `${schemaId}#` - const result: Record = {} - if (issuer) result['issuer'] = issuer - if (holder) result['holder'] = holder + const { schemaId, contents, holder, issuer } = content; + + if (!schemaId) throw new SDKErrors.SchemaIdentifierMissingError(); + + const vocabulary = `${schemaId}#`; + const result: Record = {}; + + if (issuer) result['issuer'] = issuer; + if (holder) result['holder'] = holder; + + const flattenedContents = DataUtils.flattenObject(contents || {}); if (!expanded) { return { - ...jsonabc.sortObj(result), + ...result, '@context': { '@vocab': vocabulary }, - ...jsonabc.sortObj(contents ?? {}), - } + ...contents, + }; } - Object.entries(contents || {}).forEach(([key, value]) => { - let val = value - if (typeof value === 'object') { - /* FIXME: GH-issue #40 */ - /* Supporting object inside is tricky, and jsonld expansion is even more harder */ - /* for now, we got things under control with this check but need more work here */ + Object.entries(flattenedContents).forEach(([key, value]) => { + result[vocabulary + key] = value; + }); - let newObj = {} - Object.entries(jsonabc.sortObj(value)).forEach(([k, v]) => { - newObj[vocabulary + k] = v - }) - val = newObj - } - result[vocabulary + key] = val - }) - return result + return result; } /** @@ -108,6 +103,7 @@ function makeStatementsJsonLD(content: PartialContent): string[] { export function hashContents( content: PartialContent, options: Crypto.HashingOptions & { + selectedAttributes?: string[], canonicalisation?: (content: PartialContent) => string[] } = {} ): { @@ -119,12 +115,20 @@ export function hashContents( const canonicalisation = options.canonicalisation || defaults.canonicalisation // use canonicalisation algorithm to make hashable statement strings const statements = canonicalisation(content) + + let filteredStatements = statements + if (options.selectedAttributes && options.selectedAttributes.length) { + filteredStatements = DataUtils.filterStatements(statements, options.selectedAttributes); + } + // iterate over statements to produce salted hashes - const processed = Crypto.hashStatements(statements, options) + const processed = Crypto.hashStatements(filteredStatements, options) + // produce array of salted hashes to add to credential const hashes = processed .map(({ saltedHash }) => saltedHash) .sort((a, b) => hexToBn(a).cmp(hexToBn(b))) + // produce nonce map, where each nonce is keyed with the unsalted hash const nonceMap = {} processed.forEach(({ digest, nonce, statement }) => { @@ -152,6 +156,7 @@ export function verifyDisclosedAttributes( nonces: Record hashes: string[] }, + attributes?: string[], options: Pick & { canonicalisation?: (content: PartialContent) => string[] } = {} @@ -162,8 +167,12 @@ export function verifyDisclosedAttributes( const { nonces } = proof // use canonicalisation algorithm to make hashable statement strings const statements = canonicalisation(content) + let filteredStatements = statements + if (attributes && attributes.length) { + filteredStatements = DataUtils.filterStatements(statements, attributes); + } // iterate over statements to produce salted hashes - const hashed = Crypto.hashStatements(statements, { ...options, nonces }) + const hashed = Crypto.hashStatements(filteredStatements, { ...options, nonces }) // check resulting hashes const digestsInProof = Object.keys(nonces) const { verified, errors } = hashed.reduce<{ diff --git a/packages/modules/src/document/Document.ts b/packages/modules/src/document/Document.ts index a110b33c..bf208ca6 100644 --- a/packages/modules/src/document/Document.ts +++ b/packages/modules/src/document/Document.ts @@ -20,7 +20,7 @@ import type { IRegistry, StreamId, RegistryId, - // DocumenentMetaData, + PresentationOptions, } from '@cord.network/types' import { Crypto, SDKErrors, DataUtils } from '@cord.network/utils' import * as Content from '../content/index.js' @@ -43,8 +43,6 @@ function getHashRoot(leaves: Uint8Array[]): Uint8Array { function getHashLeaves( contentHashes: Hash[], evidenceIds: IDocument[], - createdAt: string, - validUntil: string ): Uint8Array[] { const result = contentHashes.map((item) => Crypto.coToUInt8(item)) @@ -53,12 +51,6 @@ function getHashLeaves( result.push(Crypto.coToUInt8(evidence.identifier)) }) } - if (createdAt && createdAt !== '') { - result.push(Crypto.coToUInt8(createdAt)) - } - if (validUntil && validUntil !== '') { - result.push(Crypto.coToUInt8(validUntil)) - } return result } @@ -74,38 +66,20 @@ export function calculateDocumentHash(document: Partial): Hash { const hashes = getHashLeaves( document.contentHashes || [], document.evidenceIds || [], - document.createdAt || '', - document.validUntil || '' ) + if (document.issuanceDate) { + hashes.push(Crypto.coToUInt8(document.issuanceDate)) + } + if (document.validFrom) { + hashes.push(Crypto.coToUInt8(document.validFrom)) + } + if (document.validUntil) { + hashes.push(Crypto.coToUInt8(document.validUntil)) + } const root = getHashRoot(hashes) return Crypto.u8aToHex(root) } -/** - * Removes [[Content] properties from the [[Document]] object, provides anonymity and security when building the [[createPresentation]] method. - * - * @param document - The document object to remove properties from. - * @param properties - Properties to remove from the [[Content]] object. - * @returns A cloned Document with removed properties. - */ -export function removeContentProperties( - document: IDocument, - properties: string[] -): IDocument { - const presentation: IDocument = - // clone the credential because properties will be deleted later. - // TODO: find a nice way to clone stuff - JSON.parse(JSON.stringify(document)) - - properties.forEach((key) => { - delete presentation.content.contents[key] - }) - presentation.contentNonceMap = hashContents(presentation.content, { - nonces: presentation.contentNonceMap, - }).nonceMap - - return presentation -} /** * Prepares credential data for signing. @@ -135,18 +109,26 @@ export function verifyDocumentHash(input: IDocument): void { * @param input - The [[Stream]] for which to verify data. */ -export function verifyDataIntegrity(input: IDocument): void { +export function verifyDataIntegrity(input: IDocument, { selectedAttributes }: VerifyOptions = {}): void { // check document hash verifyDocumentHash(input) // verify properties against selective disclosure proof - Content.verifyDisclosedAttributes(input.content, { - nonces: input.contentNonceMap, - hashes: input.contentHashes, - }) + if (selectedAttributes) { + Content.verifyDisclosedAttributes(input.content, { + nonces: input.contentNonceMap, + hashes: input.contentHashes, + }, selectedAttributes) + } else { + Content.verifyDisclosedAttributes(input.content, { + nonces: input.contentNonceMap, + hashes: input.contentHashes, + }) + + } - // check evidences - input.evidenceIds.forEach(verifyDataIntegrity) + // TODO - check evidences + // input.evidenceIds.forEach(verifyDataIntegrity) } /** @@ -202,13 +184,13 @@ export function verifyAuthorization( * @param schema A [[Schema]] to verify the [[Content]] structure. */ -export function verifyAgainstSchema( - document: IDocument, - schema: ISchema -): void { - verifyDataStructure(document) - verifyContentAganistSchema(document.content.contents, schema) -} +// export function verifyAgainstSchema( +// document: IDocument, +// schema: ISchema +// ): void { +// verifyDataStructure(document) +// verifyContentAganistSchema(document.content.contents, schema) +// } /** * Verifies the signature of the [[IDocumentPresentation]]. @@ -290,7 +272,8 @@ export function getUriForStream( export type Options = { evidenceIds?: IDocument[] - expiresAt?: Date | null + validFrom?: Date + validUntil?: Date templates?: string[] labels?: string[] } @@ -311,35 +294,37 @@ export async function fromContent({ authorization, registry, signCallback, - // evidenceIds, options = {}, }: { content: IContent authorization: IRegistryAuthorization['identifier'] registry: IRegistry['identifier'] signCallback: SignCallback - // evidenceIds?: IDocument[] options: Options }): Promise { - const { evidenceIds, expiresAt, templates = [], labels } = options + const { evidenceIds, validFrom, validUntil, templates, labels } = options const { hashes: contentHashes, nonceMap: contentNonceMap } = Content.hashContents(content) - const issuanceDate = new Date() - const issuanceDateString = issuanceDate.toISOString() - const expiryDateString = expiresAt ? expiresAt.toISOString() : 'Infinity' + const issuanceDate = new Date().toISOString() + const validFromString = validFrom ? validFrom.toISOString() : undefined + const validUntilString = validUntil ? validUntil.toISOString() : undefined const metaData = { templates: templates || [], labels: labels || [], } + + const documentHash = calculateDocumentHash({ evidenceIds, contentHashes, - createdAt: issuanceDateString, - validUntil: expiryDateString, + issuanceDate, + validFrom: validFromString, + validUntil: validUntilString, }) + const registryIdentifier = Identifier.uriToIdentifier(registry) const streamId = getUriForStream( documentHash, @@ -361,8 +346,9 @@ export async function fromContent({ evidenceIds: evidenceIds || [], authorization: authorization, registry: registry, - createdAt: issuanceDateString, - validUntil: expiryDateString, + issuanceDate, + validFrom: validFromString, + validUntil: validUntilString, documentHash, issuerSignature: signatureToJson(issuerSignature), metadata: metaData, @@ -400,27 +386,54 @@ type VerifyOptions = { schema?: ISchema challenge?: string didResolveKey?: DidResolveKey + selectedAttributes?: string[] } /** - * Verifies data structure & data integrity of a credential object. + * Verifies data structure & data integrity of a document object. + * This combines all offline sanity checks that can be performed on an IDocument object. * * @param document - The object to check. * @param options - Additional parameter for more verification steps. * @param options.schema - Schema to be checked against. + * @param options.selectedAttributes - Selective disclosure attributes */ -export async function verifyDocument( +export function verifyWellFormed( document: IDocument, - { schema }: VerifyOptions = {} -): Promise { + { schema, selectedAttributes }: VerifyOptions = {} +): void { verifyDataStructure(document) - verifyDataIntegrity(document) + if (selectedAttributes && (selectedAttributes.length > 0 || selectedAttributes[0] !== '*')) { + verifyDataIntegrity(document, { selectedAttributes }) + } else { + verifyDataIntegrity(document) + } if (schema) { - verifyAgainstSchema(document, schema) + verifyContentAganistSchema(document.content.contents, schema) } } +/** + * Verifies data structure & data integrity of a credential object. + * + * @param document - The object to check. + * @param options - Additional parameter for more verification steps. + * @param options.schema - Schema to be checked against. + */ +export async function verifyDocument( + document: IDocument, + { schema, selectedAttributes }: VerifyOptions = {} +): Promise { + verifyWellFormed(document, { schema, selectedAttributes }) + // verifyDataStructure(document) + // verifyDataIntegrity(document) + + // if (schema) { + // verifyAgainstSchema(document, schema) + // } +} + /** * Verifies data structure, data integrity and the holder's signature of a document presentation. * @@ -436,7 +449,8 @@ export async function verifyPresentation( presentation: IDocumentPresentation, { schema, challenge, didResolveKey = resolveKey }: VerifyOptions = {} ): Promise { - await verifyDocument(presentation, { schema }) + const selectedAttributes = presentation.selectiveAttributes + await verifyDocument(presentation, { schema, selectedAttributes }) await verifySignature(presentation, { challenge, didResolveKey, @@ -483,16 +497,32 @@ export function getHash(document: IDocument): IStream['streamHash'] { return document.documentHash } -/** - * Gets names of the document's attributes. - * - * @param document The document. - * @returns The set of names. - */ -function getAttributes(document: IDocument): Set { - return new Set(Object.keys(document.content.contents)) +function filterNestedObject(obj: Record, keysToKeep: string[]): Record { + const result = {}; + + for (const key in obj) { + // Check if the key is in keysToKeep list. + if (keysToKeep.includes(key)) { + result[key] = obj[key]; + } else if (typeof obj[key] === 'object') { + // Process nested keys. + const nestedKeys = keysToKeep + .filter(k => k.startsWith(key + ".")) + .map(k => k.split('.').slice(1).join('.')); + + if (nestedKeys.length) { + const nestedObject = filterNestedObject(obj[key], nestedKeys); + if (Object.keys(nestedObject).length > 0) { + result[key] = nestedObject; + } + } + } + } + + return result; } + /** * Creates a public presentation which can be sent to a verifier. * This presentation is signed. @@ -510,33 +540,26 @@ export async function createPresentation({ signCallback, selectedAttributes, challenge, -}: { - document: IDocument - signCallback: SignCallback - selectedAttributes?: string[] - challenge?: string -}): Promise { - // filter attributes that are not in requested attributes - const excludedClaimProperties = selectedAttributes - ? Array.from(getAttributes(document)).filter( - (property) => !selectedAttributes.includes(property) - ) - : [] - - // remove these attributes - const presentation = removeContentProperties( - document, - excludedClaimProperties - ) +}: PresentationOptions): Promise { + let presentationDocument = document; + + if (selectedAttributes && selectedAttributes.length > 0) { + // Only keep selected attributes + presentationDocument.content.contents = filterNestedObject(document.content.contents, selectedAttributes); + } + presentationDocument.contentNonceMap = hashContents(presentationDocument.content, { + nonces: presentationDocument.contentNonceMap, selectedAttributes, + }).nonceMap const signature = await signCallback({ - data: makeSigningData(presentation, challenge), + data: makeSigningData(presentationDocument, challenge), did: document.content.holder, keyRelationship: 'authentication', }) return { - ...presentation, + ...presentationDocument, + selectiveAttributes: selectedAttributes || [], holderSignature: { ...signatureToJson(signature), ...(challenge && { challenge }), diff --git a/packages/modules/src/schema/Schema.types.ts b/packages/modules/src/schema/Schema.types.ts index 34e34092..d048f365 100644 --- a/packages/modules/src/schema/Schema.types.ts +++ b/packages/modules/src/schema/Schema.types.ts @@ -27,7 +27,6 @@ export const SchemaModelV1: JsonSchema.Schema & { $id: string } = { { $ref: '#/definitions/array' }, { $ref: '#/definitions/object' }, ], - type: 'object', }, }, type: 'object', @@ -125,10 +124,35 @@ export const SchemaModelV1: JsonSchema.Schema & { $id: string } = { required: ['type', 'items'], }, object: { - additionalProperties: true, + additionalProperties: false, properties: { - type: { - const: 'object', + type: { const: 'object' }, + properties: { + type: 'object', + patternProperties: { + '^.+$': { + oneOf: [ + { $ref: '#/definitions/string' }, + { $ref: '#/definitions/number' }, + { $ref: '#/definitions/boolean' }, + { $ref: '#/definitions/schemaReference' }, + { $ref: '#/definitions/array' }, + { $ref: '#/definitions/object' }, + ], + }, + }, + }, + patternProperties: { + '^.+$': { + oneOf: [ + { $ref: '#/definitions/string' }, + { $ref: '#/definitions/number' }, + { $ref: '#/definitions/boolean' }, + { $ref: '#/definitions/schemaReference' }, + { $ref: '#/definitions/array' }, + { $ref: '#/definitions/object' }, + ], + }, }, }, required: ['type'], @@ -136,69 +160,6 @@ export const SchemaModelV1: JsonSchema.Schema & { $id: string } = { }, } -// export const SchemaModelV1: JsonSchema.Schema & { $id: string } = { -// $id: 'http://cord.network/draft-01/schema#', -// $schema: 'http://json-schema.org/draft-07/schema#', -// title: 'CType Metaschema (draft-01)', -// description: `Describes a Schema, which is a JSON schema for validating stream types. This version has known issues, the use of schema ${SchemaModelV2.$id} is recommended instead.`, -// type: 'object', -// properties: { -// $id: { -// type: 'string', -// format: 'uri', -// pattern: '^schema:cord:5[0-9a-zA-Z]+$', -// }, -// $schema: { -// type: 'string', -// format: 'uri', -// const: 'http://json-schema.org/draft-07/schema#', -// }, -// title: { -// type: 'string', -// }, -// description: { -// type: 'string', -// }, -// type: { -// type: 'string', -// const: 'object', -// }, -// properties: { -// type: 'object', -// patternProperties: { -// '^.*$': { -// type: 'object', -// properties: { -// type: { -// type: 'string', -// enum: ['string', 'integer', 'number', 'boolean'], -// }, -// $ref: { -// type: 'string', -// format: 'uri', -// }, -// format: { -// type: 'string', -// enum: ['date', 'time', 'uri'], -// }, -// }, -// additionalProperties: false, -// oneOf: [ -// { -// required: ['type'], -// }, -// { -// required: ['$ref'], -// }, -// ], -// }, -// }, -// }, -// }, -// additionalProperties: false, -// required: ['$id', 'title', '$schema', 'properties', 'type'], -// } - export const SchemaModel: JsonSchema.Schema = { $schema: 'http://json-schema.org/draft-07/schema', allOf: [ diff --git a/packages/types/src/Document.ts b/packages/types/src/Document.ts index 14297b83..5e66dfcb 100644 --- a/packages/types/src/Document.ts +++ b/packages/types/src/Document.ts @@ -2,6 +2,7 @@ import type { HexString } from '@polkadot/util/types' import type { DidSignature } from './DidDocument' import type { IContent } from './Content.js' import type { IRegistryAuthorization } from './Registry' +import type { SignCallback } from './CryptoCallbacks' export type Hash = HexString @@ -23,13 +24,23 @@ export interface IDocument { evidenceIds: IDocument[] authorization: IRegistryAuthorization['identifier'] registry: string | null - createdAt: string - validUntil: string + issuanceDate: string + validFrom?: string + validUntil?: string documentHash: Hash issuerSignature: DidSignature metadata: DocumentMetaData } export interface IDocumentPresentation extends IDocument { + selectiveAttributes: string[] holderSignature: DidSignature & { challenge?: string } } + +export interface PresentationOptions { + document: IDocument; + signCallback: SignCallback; + selectedAttributes?: string[]; + challenge?: string; +} + diff --git a/packages/utils/src/Crypto.ts b/packages/utils/src/Crypto.ts index 6ac0a2a7..a9bed9d8 100644 --- a/packages/utils/src/Crypto.ts +++ b/packages/utils/src/Crypto.ts @@ -159,10 +159,10 @@ export function encodeObjectAsStr( ? JSON.stringify(jsonabc.sortObj(value)) : // eslint-disable-next-line no-nested-ternary typeof value === 'number' && value !== null - ? value.toString() - : typeof value === 'boolean' && value !== null - ? JSON.stringify(value) - : value + ? value.toString() + : typeof value === 'boolean' && value !== null + ? JSON.stringify(value) + : value return input.normalize('NFC') } @@ -288,6 +288,7 @@ export interface HashingOptions { nonces?: Record nonceGenerator?: (key: string) => string hasher?: Hasher + // selectedAttributes?: string[] } /** diff --git a/packages/utils/src/DataUtils.ts b/packages/utils/src/DataUtils.ts index 0b180eb7..6739a72a 100644 --- a/packages/utils/src/DataUtils.ts +++ b/packages/utils/src/DataUtils.ts @@ -14,6 +14,59 @@ import * as SDKErrors from './SDKErrors.js' import { checkIdentifier } from './Identifier.js' import { ss58Format } from './ss58Format.js' + +export function flattenObject(obj: Record, prefix = ''): Record { + const flatObject: Record = {}; + + Object.keys(obj).forEach(key => { + const newKey = `${prefix}${key}`; + + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + flatObject[newKey] = obj[key]; // Store the current object + const deeper = flattenObject(obj[key], `${newKey}.`); // Recurse deeper + for (let prop in deeper) { + flatObject[prop] = deeper[prop]; + } + } else { + flatObject[newKey] = obj[key]; + } + }); + + return flatObject; +} + +function extractKeyPartFromStatement(statement: string): string | null { + try { + const obj = JSON.parse(statement); + const keys = Object.keys(obj); + if (keys.length > 0) { + // Always retain 'issuer' and 'holder' + if (keys[0] === 'issuer' || keys[0] === 'holder') return keys[0]; + + const parts = keys[0].split("#"); + return parts.length > 1 ? parts[1] : null; + } + return null; + } catch (error) { + return null; // If parsing fails, return null + } +} + + +export function filterStatements(statements: string[], selectedAttributes: string[]): string[] { + return statements.filter(statement => { + const keyPart = extractKeyPartFromStatement(statement); + if (!keyPart) return false; // Omit if key extraction fails + + // Always include 'issuer' and 'holder' + if (keyPart === 'issuer' || keyPart === 'holder') return true; + + return selectedAttributes.includes(keyPart); + }); +} + + + /** * Validates the format of the given blake2b hash via regex. * diff --git a/packages/vc-export/src/constants.ts b/packages/vc-export/src/constants.ts index ab16345a..1ef9bbfc 100644 --- a/packages/vc-export/src/constants.ts +++ b/packages/vc-export/src/constants.ts @@ -25,4 +25,4 @@ export const CORD_CREDENTIAL_DIGEST_PROOF_TYPE = 'CordCredentialDigest2020' export const JSON_SCHEMA_TYPE = 'JsonSchemaValidator2018' -export const CORD_CREDENTIAL_IRI_PREFIX = 'cred:cord:' +export const CORD_CREDENTIAL_IRI_PREFIX = 'stream:cord:' diff --git a/packages/vc-export/src/exportToVerifiableCredential.ts b/packages/vc-export/src/exportToVerifiableCredential.ts index 57986016..5cee5f48 100644 --- a/packages/vc-export/src/exportToVerifiableCredential.ts +++ b/packages/vc-export/src/exportToVerifiableCredential.ts @@ -67,7 +67,7 @@ export function fromCredential( const issuer = input.content.issuer - const issuanceDate = input.createdAt + const issuanceDate = input.issuanceDate const expirationDate = input.validUntil // if schema is given, add as credential schema let credentialSchema: CredentialSchema | undefined @@ -114,7 +114,7 @@ export function fromCredential( } VC.proof.push(sSProof) } - + // add credential proof const streamProof: CordStreamProof = { type: CORD_ANCHORED_PROOF_TYPE, @@ -127,7 +127,7 @@ export function fromCredential( const cDProof: CredentialDigestProof = { type: CORD_CREDENTIAL_DIGEST_PROOF_TYPE, proofPurpose: 'assertionMethod', - nonces: {...input.contentNonceMap}, + nonces: { ...input.contentNonceMap }, contentHashes: [...contentHashes], } VC.proof.push(cDProof) diff --git a/packages/vc-export/src/types.ts b/packages/vc-export/src/types.ts index 12d6bb2d..5acd4115 100644 --- a/packages/vc-export/src/types.ts +++ b/packages/vc-export/src/types.ts @@ -71,7 +71,7 @@ export interface VerifiableCredential { // when the credential was issued issuanceDate: string // when the credential will expire - expirationDate: string + expirationDate?: string // streams about the subjects of the credential credentialSubject: Record // rootHash of the credential