From a8c0b21ef51a5aab07f44187b1514613de84ad75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rib=C3=B3?= Date: Tue, 11 Jun 2024 17:02:37 +0200 Subject: [PATCH] feat: Implement sd+jwt for issuance and verification flows with cloud agent (#228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Javier Ribó BREAKING CHANGE: Pollux instance now requires to have Apollo first constructor parameter (used internally) Deprecated internal function processJWTCredential, processAnoncredsCredential and extractCredentialFormatFromMessage. Internally, in order to process any type of credential offer just call pollux.processCredentialOffer instead. In order to extract the credentialFormat from a DIDComm message if available, use message.credentialFormat (will return known CredentialType or unknown) In order to extract the payload of whatever DIDComm message, use message.payload which will decode it into the right object instance JWT class now needs apollo and castor in constructor as they now instantiate from JWTCore (used internally) Derivable Private key is not deriving using the derivationPath as a string not the DerivationPath class (used internally) --- .eslintrc | 17 +- .husky/pre-commit | 1 + demos/next/src/components/Message.tsx | 2 +- demos/next/src/pages/credentials.tsx | 45 +- demos/next/src/reducers/app.ts | 6 +- package-lock.json | 244 ++++-- package.json | 10 +- src/apollo/Apollo.ts | 149 +++- src/apollo/utils/Ed25519PrivateKey.ts | 34 +- src/apollo/utils/Ed25519PublicKey.ts | 2 - src/apollo/utils/Secp256k1PrivateKey.ts | 28 +- src/apollo/utils/Secp256k1PublicKey.ts | 7 +- src/apollo/utils/derivation/DerivationAxis.ts | 42 - src/apollo/utils/derivation/DerivationPath.ts | 165 ++-- src/apollo/utils/derivation/ExtendedKey.ts | 42 - src/apollo/utils/derivation/KeyDerivation.ts | 15 - src/castor/Castor.ts | 92 +- src/castor/did/prismDID/PrismDIDPublicKey.ts | 240 +++--- .../resolver/LongFormPrismDIDResolver.ts | 57 +- src/domain/buildingBlocks/Apollo.ts | 5 +- src/domain/buildingBlocks/Castor.ts | 5 +- src/domain/buildingBlocks/Pollux.ts | 69 +- src/{config => domain/models}/ECConfig.ts | 0 src/domain/models/KeyProperties.ts | 5 + src/domain/models/Message.ts | 80 +- src/domain/models/MessageAttachment.ts | 7 + src/domain/models/VerifiableCredential.ts | 58 +- .../models/derivation/DerivationAxis.ts | 56 ++ src/domain/models/derivation/index.ts | 29 + .../schemas/DeprecatedDerivation.ts | 58 ++ .../derivation/schemas/PrismDerivation.ts | 88 ++ src/domain/models/errors/Apollo.ts | 8 + .../models/keyManagement/DerivableKey.ts | 3 +- src/domain/models/keyManagement/Key.ts | 122 +++ src/domain/models/keyManagement/KeyTypes.ts | 1 + .../models/keyManagement/StorableKey.ts | 2 - src/domain/utils/DER.ts | 108 +++ src/domain/utils/hash.ts | 43 + src/domain/utils/randomBytes.ts | 8 + src/edge-agent/Agent.Credentials.ts | 181 ++-- src/edge-agent/Agent.DIDHigherFunctions.ts | 59 +- src/edge-agent/Agent.PrismKeyPathIndexTask.ts | 15 +- src/edge-agent/Agent.ts | 4 +- .../connectionsManager/ConnectionsManager.ts | 4 +- .../mediator/BasicMediatorHandler.ts | 2 +- .../proofPresentation/RequestPresentation.ts | 2 +- .../revocation/RevocationNotfiication.ts | 4 +- src/edge-agent/protocols/types.ts | 1 - src/edge-agent/types/index.ts | 9 +- src/mercury/didcomm/Wrapper.ts | 20 +- src/pluto/Pluto.ts | 45 +- src/pluto/models/Key.ts | 3 +- .../repositories/CredentialRepository.ts | 12 + src/pluto/repositories/KeyRepository.ts | 6 +- .../repositories/LinkSecretRepository.ts | 2 +- src/pollux/Pollux.ts | 391 ++++----- src/pollux/models/JWTVerifiableCredential.ts | 6 +- src/pollux/models/PresentationRequest.ts | 40 +- .../models/SDJWTVerifiableCredential.ts | 204 +++++ src/pollux/utils/DescriptorPath.ts | 4 - src/pollux/utils/JWT.ts | 144 +--- src/pollux/utils/SDJWT.ts | 70 ++ src/pollux/utils/claims.ts | 2 +- src/pollux/utils/decodeJWS.ts | 21 +- src/pollux/utils/jwt/JWTCore.ts | 155 ++++ src/pollux/utils/jwt/config.ts | 26 + src/pollux/utils/jwt/types.ts | 54 ++ tests/agent/Agent.functional.test.ts | 42 +- tests/agent/Agent.test.ts | 162 +++- tests/apollo/Apollo.createPrivateKey.test.ts | 17 +- tests/apollo/Apollo.test.ts | 196 +++-- tests/apollo/keys/Ed25519.test.ts | 73 +- tests/apollo/keys/Secp256k1.test.ts | 34 +- tests/castor/PrismDID.test.ts | 141 ++- tests/fixtures/credentials/index.ts | 1 + tests/fixtures/credentials/jwt.ts | 44 +- tests/fixtures/credentials/sdjwt.ts | 62 ++ tests/fixtures/keys.ts | 6 + tests/pluto/Pluto.Migrations.test.ts | 53 +- tests/pluto/Pluto.test.ts | 28 +- tests/pollux/Pollux.test.ts | 807 +++++++++--------- 81 files changed, 3453 insertions(+), 1652 deletions(-) create mode 100644 .husky/pre-commit delete mode 100644 src/apollo/utils/derivation/DerivationAxis.ts delete mode 100644 src/apollo/utils/derivation/ExtendedKey.ts delete mode 100644 src/apollo/utils/derivation/KeyDerivation.ts rename src/{config => domain/models}/ECConfig.ts (100%) create mode 100644 src/domain/models/derivation/DerivationAxis.ts create mode 100644 src/domain/models/derivation/index.ts create mode 100644 src/domain/models/derivation/schemas/DeprecatedDerivation.ts create mode 100644 src/domain/models/derivation/schemas/PrismDerivation.ts create mode 100644 src/domain/utils/DER.ts create mode 100644 src/domain/utils/hash.ts create mode 100644 src/domain/utils/randomBytes.ts create mode 100644 src/pollux/models/SDJWTVerifiableCredential.ts create mode 100644 src/pollux/utils/SDJWT.ts create mode 100644 src/pollux/utils/jwt/JWTCore.ts create mode 100644 src/pollux/utils/jwt/config.ts create mode 100644 src/pollux/utils/jwt/types.ts create mode 100644 tests/fixtures/credentials/sdjwt.ts diff --git a/.eslintrc b/.eslintrc index 827250c9d..9a1c048db 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,8 @@ ], "plugins": [ "@typescript-eslint", - "react" + "react", + "unused-imports" ], "extends": [ "eslint:recommended", @@ -34,10 +35,18 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-namespace": "off", - "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ - "warn" - ] + "warn", + { + "vars": "all", + "varsIgnorePattern": "^_", + "args": "after-used", + "argsIgnorePattern": "^_" + } + ], + "no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": "off" }, "settings": { "react": { diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..59c1d4d6b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx eslint . --fix \ No newline at end of file diff --git a/demos/next/src/components/Message.tsx b/demos/next/src/components/Message.tsx index 6c42ce8d2..2b2b6fc75 100644 --- a/demos/next/src/components/Message.tsx +++ b/demos/next/src/components/Message.tsx @@ -484,7 +484,7 @@ export function Message({ message }) { if (SDK.isPresentationDefinitionRequestType(requestPresentation, SDK.Domain.CredentialType.AnonCreds)) { const credentials = app.credentials; const fields = - Object.keys(requestPresentation.requested_attributes).reduce( + Object.keys(requestPresentation.requested_attributes || []).reduce( (_, key) => ([ ..._, { diff --git a/demos/next/src/pages/credentials.tsx b/demos/next/src/pages/credentials.tsx index c17eef03a..7d0785032 100644 --- a/demos/next/src/pages/credentials.tsx +++ b/demos/next/src/pages/credentials.tsx @@ -28,38 +28,25 @@ function Credential(props) { const [claims, setClaims] = useState(protect(credential)); function revealAttributes(credential: SDK.Domain.Credential, claimIndex: number, field: string) { - if (credential.credentialType === SDK.Domain.CredentialType.JWT) { - const revealed = claims.map((claim, index) => { - if (claimIndex === index) { - return { - ...claim, - [field]: credential.claims[index][field] - } - } - return claim - }) - setClaims(revealed) - } else { - app.agent.instance?.pluto.getLinkSecret() - .then((linkSecret) => { - app.agent.instance?.revealCredentialFields( - credential, - [field], - linkSecret!.secret - ).then((revealedFields) => { - const revealed = claims.map((claim, index) => { - if (claimIndex === index) { - return { - ...claim, - [field]: revealedFields[field] - } + app.agent.instance?.pluto.getLinkSecret() + .then((linkSecret) => { + app.agent.instance?.revealCredentialFields( + credential, + [field], + linkSecret!.secret + ).then((revealedFields) => { + const revealed = claims.map((claim, index) => { + if (claimIndex === index) { + return { + ...claim, + [field]: revealedFields[field] } - return claim - }) - setClaims(revealed) + } + return claim }) + setClaims(revealed) }) - } + }) } return
diff --git a/demos/next/src/reducers/app.ts b/demos/next/src/reducers/app.ts index 1f9f0fe31..3c845bc0e 100644 --- a/demos/next/src/reducers/app.ts +++ b/demos/next/src/reducers/app.ts @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from "uuid"; import { DBPreload, Message, Credential } from "@/actions/types"; import { acceptCredentialOffer, acceptPresentationRequest, connectDatabase, initAgent, rejectCredentialOffer, sendMessage, startAgent, stopAgent } from "../actions"; -const defaultMediatorDID = "did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vc2l0LXByaXNtLW1lZGlhdG9yLmF0YWxhcHJpc20uaW8iLCJhIjpbImRpZGNvbW0vdjIiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3NpdC1wcmlzbS1tZWRpYXRvci5hdGFsYXByaXNtLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIl19fQ"; +const defaultMediatorDID = "did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuNDQ6ODA4MCIsImEiOlsiZGlkY29tbS92MiJdfX0.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzOi8vMTkyLjE2OC4xLjQ0OjgwODAvd3MiLCJhIjpbImRpZGNvbW0vdjIiXX19"; class TraceableError extends Error { @@ -137,7 +137,9 @@ const appSlice = createSlice({ ...action.meta.arg.message, isAnswering: true, hasAnswered: false, - error: null + error: null, + safeBody: action.meta.arg.message.safeBody, + credentialFormat: action.meta.arg.message.credentialFormat }) }) diff --git a/package-lock.json b/package-lock.json index 2b1db7238..d0db29cac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,10 @@ "version": "3.1.0", "license": "Apache-2.0", "dependencies": { - "@atala/apollo": "^1.3.4", + "@atala/apollo": "^1.3.5", "@scure/bip32": "^1.3.0", "@scure/bip39": "^1.1.1", + "@sd-jwt/sd-jwt-vc": "^0.7.1", "@sinclair/typebox": "^0.32.31", "@stablelib/base64": "^1.0.1", "@stablelib/sha256": "^1.0.1", @@ -89,7 +90,9 @@ "eslint-plugin-n": "^15.6.1", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.32.2", + "eslint-plugin-unused-imports": "^2.0.0", "get-func-name": "^3.0.0", + "husky": "^9.0.11", "jest": "^29.5.0", "jest-junit": "^16.0.0", "prettier": "^3.0.2", @@ -117,16 +120,6 @@ } } }, - "../anoncreds-rs/wasm/pkg": { - "name": "anoncreds-wasm", - "version": "0.1.0", - "extraneous": true - }, - "../anoncreds-rs/wasm/pkg2": { - "name": "anoncreds-wasm", - "version": "0.1.0", - "extraneous": true - }, "externals/generated/anoncreds-wasm-browser": { "name": "anoncreds-wasm", "version": "0.1.0", @@ -149,30 +142,6 @@ "dev": true, "license": "Apache-2.0" }, - "generated/anoncreds-wasm-browser": { - "name": "anoncreds", - "version": "0.1.0-dev.16", - "extraneous": true, - "license": "Apache-2.0" - }, - "generated/anoncreds-wasm-node": { - "name": "anoncreds", - "version": "0.1.0-dev.16", - "extraneous": true, - "license": "Apache-2.0" - }, - "generated/didcomm-wasm-browser": { - "name": "didcomm-js", - "version": "0.4.1", - "extraneous": true, - "license": "Apache-2.0" - }, - "generated/didcomm-wasm-node": { - "name": "didcomm-js", - "version": "0.4.1", - "extraneous": true, - "license": "Apache-2.0" - }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "dev": true, @@ -194,9 +163,9 @@ } }, "node_modules/@atala/apollo": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@atala/apollo/-/apollo-1.3.4.tgz", - "integrity": "sha512-yW163q4HSRGV8qdQ9Xw0k3WzOK4eASKCL5/JizSvWRyST9pxyC/m5kU/xS2y90hSPSPELPA14Sw8uTTGeoqCVQ==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@atala/apollo/-/apollo-1.3.5.tgz", + "integrity": "sha512-sVaS1G7wBTDXEYcRl9ZhOqIRpgo5BshKr1IoOmx+f/BTuQxqN8Z0TRRbGPFnvVG6ffq6s0GNEOGyqUlOtRnCmQ==", "dependencies": { "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", @@ -3349,9 +3318,8 @@ }, "node_modules/@jest/schemas/node_modules/@sinclair/typebox": { "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@jest/source-map": { "version": "29.6.3", @@ -3648,9 +3616,8 @@ }, "node_modules/@noble/ciphers": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.4.1.tgz", - "integrity": "sha512-QCOA9cgf3Rc33owG0AYBB9wszz+Ul2kramWN8tXG44Gyciud/tbkEqvxRF/IpqQaBpRBNi9f4jdNxqB2CQCIXg==", "dev": true, + "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } @@ -3854,9 +3821,8 @@ }, "node_modules/@pluto-encrypted/encryption": { "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@pluto-encrypted/encryption/-/encryption-1.11.0.tgz", - "integrity": "sha512-f3dyJGod0CMqBZP/UCeP8hZBH278BP1Wquq/aq+16/z5t1UZMQq+wTG3JnMx2Yn+ivx70WjTaPlmKNrw4mjUqw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@noble/ciphers": "^0.4.1", "@noble/curves": "^1.3.0", @@ -3869,9 +3835,8 @@ }, "node_modules/@pluto-encrypted/encryption/node_modules/@noble/curves": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", "dev": true, + "license": "MIT", "dependencies": { "@noble/hashes": "1.3.3" }, @@ -3881,9 +3846,8 @@ }, "node_modules/@pluto-encrypted/encryption/node_modules/@noble/hashes": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 16" }, @@ -3893,9 +3857,8 @@ }, "node_modules/@pluto-encrypted/inmemory": { "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@pluto-encrypted/inmemory/-/inmemory-1.12.3.tgz", - "integrity": "sha512-OBilhIBKrqZkxKx7F98UmVU2rxXbyLwv3R7R8/uz0+7n+QmJ1tR6WEaCQN/WkxG95Vx07lLxCIt35l4Iw/681A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@pluto-encrypted/encryption": "1.11.0", "@pluto-encrypted/shared": "1.11.3", @@ -3906,15 +3869,13 @@ }, "node_modules/@pluto-encrypted/inmemory/node_modules/array-push-at-sort-position": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/array-push-at-sort-position/-/array-push-at-sort-position-4.0.1.tgz", - "integrity": "sha512-KdtdxZmp+j6n+jiekMbBRO/TOVP7oEadrJ+M4jB0Oe1VHZHS1Uwzx5lsvFN4juNZtHNA1l1fvcEs/SDmdoXL3w==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@pluto-encrypted/shared": { "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@pluto-encrypted/shared/-/shared-1.11.3.tgz", - "integrity": "sha512-/Ud10UeA7l+E9dM5rrfo5ED0U7kKwVgatsI+vL1EGs8/4W7OZXECP3DsDl/JnjH8GC/gH3ddql8U9PviWFi2ig==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@pluto-encrypted/encryption": "1.11.0", "rxdb": "^14.17.0", @@ -4236,6 +4197,88 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@sd-jwt/core": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/decode": "0.7.1", + "@sd-jwt/present": "0.7.1", + "@sd-jwt/types": "0.7.1", + "@sd-jwt/utils": "0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/decode": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.7.1", + "@sd-jwt/utils": "0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/jwt-status-list": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.7.1", + "base64url": "^3.0.1", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/present": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/decode": "0.7.1", + "@sd-jwt/types": "0.7.1", + "@sd-jwt/utils": "0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/core": "0.7.1", + "@sd-jwt/jwt-status-list": "0.7.1", + "@sd-jwt/utils": "0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/types": { + "version": "0.7.1", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/utils": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "@sd-jwt/types": "0.7.1", + "js-base64": "^3.7.6" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/utils/node_modules/js-base64": { + "version": "3.7.7", + "license": "BSD-3-Clause" + }, "node_modules/@semantic-release/changelog": { "version": "6.0.3", "dev": true, @@ -4820,8 +4863,7 @@ }, "node_modules/@sinclair/typebox": { "version": "0.32.31", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.32.31.tgz", - "integrity": "sha512-rYB0tgGHawpom3ZwwsGidvI0NI+W/rRHu1dyyO1KlIoH8iMdg3esSnYQxQtyJ8eflhqxmzEV7Nu8zT4JY7CHKw==" + "license": "MIT" }, "node_modules/@sindresorhus/merge-streams": { "version": "1.0.0", @@ -6559,6 +6601,13 @@ ], "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bech32": { "version": "2.0.0", "license": "MIT" @@ -6591,11 +6640,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -7378,8 +7428,7 @@ }, "node_modules/crypto-js": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + "license": "MIT" }, "node_modules/crypto-random-string": { "version": "4.0.0", @@ -8572,6 +8621,36 @@ "eslint": "^7.5.0 || ^8.0.0" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "dev": true, @@ -8988,9 +9067,10 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -9142,14 +9222,13 @@ }, "node_modules/follow-redirects": { "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -9716,6 +9795,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/idb": { "version": "7.1.1", "license": "ISC" @@ -10147,8 +10241,9 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -12077,8 +12172,7 @@ }, "node_modules/jose": { "version": "4.15.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", - "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -13035,11 +13129,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, - "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -19974,8 +20069,9 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index 1eecdbdda..7ab6452df 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "test": "jest", "coverage": "npm run test -- --coverage", "lint": "npx eslint .", - "docs": "npx typedoc --options typedoc.js" + "docs": "npx typedoc --options typedoc.js", + "prepare": "npx husky" }, "author": "IOHK", "repository": { @@ -104,7 +105,9 @@ "eslint-plugin-n": "^15.6.1", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.32.2", + "eslint-plugin-unused-imports": "^2.0.0", "get-func-name": "^3.0.0", + "husky": "^9.0.11", "jest": "^29.5.0", "jest-junit": "^16.0.0", "prettier": "^3.0.2", @@ -180,9 +183,10 @@ "outputName": "junit.xml" }, "dependencies": { - "@atala/apollo": "^1.3.4", + "@atala/apollo": "^1.3.5", "@scure/bip32": "^1.3.0", "@scure/bip39": "^1.1.1", + "@sd-jwt/sd-jwt-vc": "^0.7.1", "@sinclair/typebox": "^0.32.31", "@stablelib/base64": "^1.0.1", "@stablelib/sha256": "^1.0.1", @@ -211,4 +215,4 @@ "overrides": { "crypto-js": "^4.2.0" } -} +} \ No newline at end of file diff --git a/src/apollo/Apollo.ts b/src/apollo/Apollo.ts index 052971d03..cca6c1f7c 100644 --- a/src/apollo/Apollo.ts +++ b/src/apollo/Apollo.ts @@ -30,9 +30,10 @@ import { Secp256k1PublicKey } from "./utils/Secp256k1PublicKey"; import { Ed25519PublicKey } from "./utils/Ed25519PublicKey"; import { X25519PublicKey } from "./utils/X25519PublicKey"; -import { DerivationPath } from "./utils/derivation/DerivationPath"; import { notEmptyString } from "../utils"; import ApolloPKG from "@atala/apollo"; +import { PrismDerivationPath } from "../domain/models/derivation/schemas/PrismDerivation"; + const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; const Mnemonic = ApolloSDK.derivation.Mnemonic.Companion; const HDKey = ApolloSDK.derivation.HDKey; @@ -113,10 +114,12 @@ const BigIntegerWrapper = ApolloSDK.derivation.BigIntegerWrapper; * @typedef {Apollo} */ export default class Apollo implements ApolloInterface, KeyRestoration { + static Secp256k1PrivateKey = Secp256k1PrivateKey; static Ed25519PrivateKey = Ed25519PrivateKey; static X25519PrivateKey = X25519PrivateKey; + /** * Creates a random set of mnemonic phrases that can be used as a seed for generating a private key. * @@ -191,6 +194,81 @@ export default class Apollo implements ApolloInterface, KeyRestoration { }; } + /** + * Creates a public key based on the current cryptographic abstraction + * + * + * @example + * Create an EC Key with secp256k1 curve + * + * ```ts + * const privateKey = apollo.createPublicKey({ + * type: KeyTypes.EC, + * curve: Curve.SECP256K1, + * raw: Buffer.from(new Arra(64).fill(1)), + * }); + * ``` + * + * @param {PrivateKey} privateKey + * @returns {KeyPair} + */ + createPublicKey(parameters: { + [name: KeyProperties | string]: any; + }): PublicKey { + if (!parameters[KeyProperties.type]) { + throw new ApolloError.InvalidKeyType( + parameters[KeyProperties.type], + Object.values(KeyTypes) + ); + } + if (!parameters[KeyProperties.curve]) { + throw new ApolloError.InvalidKeyCurve( + parameters[KeyProperties.curve], + Object.values(Curve) + ); + } + const keyType = parameters[KeyProperties.type]; + const { curve } = getKeyCurveByNameAndIndex( + parameters[KeyProperties.curve] + ); + const keyData = parameters[KeyProperties.rawKey]; + + if (keyType === KeyTypes.EC) { + if (curve === Curve.ED25519) { + if (keyData) { + return new Ed25519PublicKey(keyData); + } + throw new ApolloError.InvalidPrivateKey("Missing raw bytes") + } + if (curve === Curve.SECP256K1) { + if (keyData) { + return new Secp256k1PublicKey(keyData); + } else { + const xData = parameters[KeyProperties.curvePointX]; + const yData = parameters[KeyProperties.curvePointY]; + if (xData && yData) { + return Secp256k1PublicKey.secp256k1FromByteCoordinates( + xData, + yData + ) + } + } + throw new ApolloError.InvalidPrivateKey("Missing raw bytes or coordinates") + } + } + + if (keyType === KeyTypes.Curve25519) { + if (curve === Curve.X25519) { + if (keyData) { + return new X25519PublicKey(keyData); + } + throw new ApolloError.InvalidPrivateKey("Missing raw bytes") + } + } + + throw new ApolloError.InvalidKeyType(keyType, Object.values(KeyTypes)); + } + /** * Creates a private key based on the current cryptographic abstraction * @@ -278,33 +356,55 @@ export default class Apollo implements ApolloInterface, KeyRestoration { const seedHex = parameters[KeyProperties.seed]; if (notEmptyString(seedHex)) { - const derivationParam = parameters[KeyProperties.derivationPath] ?? "m/0'/0'/0'"; - const derivationPath = DerivationPath.from(derivationParam); + + const derivationIndex = parameters[KeyProperties.index] ?? "0"; + const derivationParam = parameters[KeyProperties.derivationPath] + const defaultPath: string = derivationParam ?? PrismDerivationPath.init(derivationIndex).toString() + + const seed = Int8Array.from(Buffer.from(seedHex, "hex")); - const hdKey = ApolloSDK.derivation.EdHDKey.Companion.initFromSeed(seed).derive(derivationPath.toString()); - const edKey = Ed25519PrivateKey.from.Buffer(Buffer.from(hdKey.privateKey)); - return edKey; + const hdKey = ApolloSDK.derivation.EdHDKey.Companion.initFromSeed(seed); + const baseKey = new Ed25519PrivateKey(Uint8Array.from(hdKey.privateKey)) + + baseKey.keySpecification.set(KeyProperties.chainCode, Buffer.from(Uint8Array.from(hdKey.chainCode)).toString("hex")); + baseKey.keySpecification.set(KeyProperties.derivationPath, Buffer.from(defaultPath).toString("hex")); + baseKey.keySpecification.set(KeyProperties.index, derivationIndex); + + if (derivationParam) { + const privateKey = baseKey.derive(defaultPath); + return privateKey; + } + return baseKey; } const keyPair = Ed25519KeyPair.generateKeyPair(); return keyPair.privateKey; + } if (curve === Curve.SECP256K1) { - //TODO: Instanciating from a raw value does not include chainCode - //This keys will not be derivable until we export them as extendedKeys if (keyData) { return new Secp256k1PrivateKey(keyData); } const seedHex = parameters[KeyProperties.seed]; if (!seedHex) { - throw new ApolloError.MissingKeyParameters([KeyProperties.seed]); + throw new ApolloError.MissingKeyParameters(["seed"]) } - const seed = Buffer.from(seedHex, "hex"); - const hdKey = HDKey.InitFromSeed(Int8Array.from(seed), 0, BigIntegerWrapper.initFromInt(0)); + + const derivationIndex = parameters[KeyProperties.index] ?? "0"; + const derivationParam = parameters[KeyProperties.derivationPath]; + const defaultPath: string = derivationParam ?? PrismDerivationPath.init( + derivationIndex + ).toString() + + const hdKey = HDKey.InitFromSeed( + Int8Array.from(seed), + defaultPath.split("/").slice(1).length, + BigIntegerWrapper.initFromInt(0) + ); if (hdKey.privateKey == null) { throw new ApolloError.MissingPrivateKey(); @@ -314,19 +414,18 @@ export default class Apollo implements ApolloInterface, KeyRestoration { throw new ApolloError.MissingChainCode(); } - const baseKey = new Secp256k1PrivateKey(Buffer.from(hdKey.privateKey)); - baseKey.keySpecification.set(KeyProperties.chainCode, Buffer.from(hdKey.chainCode).toString("hex")); - baseKey.keySpecification.set(KeyProperties.derivationPath, Buffer.from(`m/0'/0'/0'`).toString("hex")); - baseKey.keySpecification.set(KeyProperties.index, "0"); + const baseKey = new Secp256k1PrivateKey(Uint8Array.from(hdKey.privateKey)); + baseKey.keySpecification.set(KeyProperties.chainCode, Buffer.from(Uint8Array.from(hdKey.chainCode)).toString("hex")); + baseKey.keySpecification.set(KeyProperties.derivationPath, Buffer.from(defaultPath).toString("hex")); + baseKey.keySpecification.set(KeyProperties.index, derivationIndex); - const derivationParam = parameters[KeyProperties.derivationPath]; if (derivationParam) { - const derivationPath = DerivationPath.from(derivationParam); - const privateKey = baseKey.derive(derivationPath); + const privateKey = baseKey.derive(defaultPath); return privateKey; } return baseKey; + } } @@ -338,25 +437,31 @@ export default class Apollo implements ApolloInterface, KeyRestoration { const seedHex = parameters[KeyProperties.seed]; if (notEmptyString(seedHex)) { - const derivationParam = parameters[KeyProperties.derivationPath] ?? "m/0'/0'/0'"; - const derivationPath = DerivationPath.from(derivationParam); + const derivationIndex = parameters[KeyProperties.index] ?? "0"; + const derivationParam: string = parameters[KeyProperties.derivationPath] ?? PrismDerivationPath.init(derivationIndex).toString(); + const seed = Int8Array.from(Buffer.from(seedHex, "hex")); - const hdKey = ApolloSDK.derivation.EdHDKey.Companion.initFromSeed(seed).derive(derivationPath.toString()); + const hdKey = ApolloSDK.derivation.EdHDKey.Companion.initFromSeed(seed).derive(derivationParam); const edKey = Ed25519PrivateKey.from.Buffer(Buffer.from(hdKey.privateKey)); const xKey = edKey.x25519(); + xKey.keySpecification.set(KeyProperties.chainCode, Buffer.from(hdKey.chainCode).toString("hex")); + xKey.keySpecification.set(KeyProperties.derivationPath, Buffer.from(derivationParam).toString("hex")); + xKey.keySpecification.set(KeyProperties.index, derivationIndex); + return xKey; } const keyPair = X25519KeyPair.generateKeyPair(); return keyPair.privateKey; + + } } throw new ApolloError.InvalidKeyType(keyType, Object.values(KeyTypes)); } - restorePrivateKey(key: StorableKey): PrivateKey { switch (key.recoveryId) { case "secp256k1+priv": diff --git a/src/apollo/utils/Ed25519PrivateKey.ts b/src/apollo/utils/Ed25519PrivateKey.ts index 7067db637..12aaeeaf3 100644 --- a/src/apollo/utils/Ed25519PrivateKey.ts +++ b/src/apollo/utils/Ed25519PrivateKey.ts @@ -8,16 +8,19 @@ import { KeyTypes, PrivateKey, StorableKey, - SignableKey + SignableKey, + DerivableKey, + ApolloError } from "../../domain"; import ApolloPKG from "@atala/apollo"; const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; - +const BigIntegerWrapper = ApolloSDK.derivation.BigIntegerWrapper; +const EdHDKey = ApolloSDK.derivation.EdHDKey /** * @ignore */ -export class Ed25519PrivateKey extends PrivateKey implements ExportableKey, SignableKey, StorableKey { +export class Ed25519PrivateKey extends PrivateKey implements DerivableKey, ExportableKey, SignableKey, StorableKey { public readonly recoveryId = StorableKey.recoveryId("ed25519", "priv"); @@ -37,6 +40,31 @@ export class Ed25519PrivateKey extends PrivateKey implements ExportableKey, Sign this.keySpecification.set(KeyProperties.curve, Curve.ED25519); } + derive(derivationPath: string): PrivateKey { + const chainCodeHex = this.getProperty(KeyProperties.chainCode); + if (!chainCodeHex) { + throw new ApolloError.MissingKeyParameters([KeyProperties.chainCode]); + } + const derivationPathStr = derivationPath.toString(); + const skRaw = Int8Array.from(this.raw); + const chaincode = Int8Array.from(Buffer.from(chainCodeHex, "hex")); + const hdKey = new EdHDKey( + skRaw, + chaincode, + derivationPathStr.split("/").slice(1).length, + BigIntegerWrapper.initFromInt(0) + ); + const derived = hdKey.derive(derivationPathStr) + const sk = new Ed25519PrivateKey(Uint8Array.from(derived.privateKey)) + sk.keySpecification.set(KeyProperties.derivationPath, Buffer.from(derivationPathStr).toString("hex")); + sk.keySpecification.set(KeyProperties.index, `${this.index ?? 0}`); + if (derived.chainCode) { + sk.keySpecification.set(KeyProperties.chainCode, Buffer.from(derived.chainCode).toString("hex")); + } + + return sk + } + publicKey() { return new Ed25519PublicKey(this.getInstance().publicKey().raw); } diff --git a/src/apollo/utils/Ed25519PublicKey.ts b/src/apollo/utils/Ed25519PublicKey.ts index 8bf574420..861fb3da5 100644 --- a/src/apollo/utils/Ed25519PublicKey.ts +++ b/src/apollo/utils/Ed25519PublicKey.ts @@ -17,8 +17,6 @@ const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; */ export class Ed25519PublicKey extends PublicKey implements ExportableKey, StorableKey, VerifiableKey { public readonly recoveryId = StorableKey.recoveryId("ed25519", "pub"); - - public keySpecification: Map = new Map(); public raw: Buffer; public size: number; diff --git a/src/apollo/utils/Secp256k1PrivateKey.ts b/src/apollo/utils/Secp256k1PrivateKey.ts index 8e90aa556..9f85c12ca 100644 --- a/src/apollo/utils/Secp256k1PrivateKey.ts +++ b/src/apollo/utils/Secp256k1PrivateKey.ts @@ -1,8 +1,7 @@ import BN from "bn.js"; -import * as ECConfig from "../../config/ECConfig"; +import * as ECConfig from "../../domain/models/ECConfig"; import { Secp256k1PublicKey } from "./Secp256k1PublicKey"; -import { DerivationPath } from "./derivation/DerivationPath"; import { ApolloError, Curve, KeyTypes, KeyProperties, } from "../../domain"; import { PrivateKey, @@ -14,6 +13,7 @@ import { } from "../../domain/models/keyManagement"; import ApolloPKG from "@atala/apollo"; +import { normaliseDER } from "../../domain/utils/DER"; const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; const HDKey = ApolloSDK.derivation.HDKey; const BigIntegerWrapper = ApolloSDK.derivation.BigIntegerWrapper; @@ -23,8 +23,7 @@ const BigIntegerWrapper = ApolloSDK.derivation.BigIntegerWrapper; */ export class Secp256k1PrivateKey extends PrivateKey - implements DerivableKey, ExportableKey, SignableKey, StorableKey -{ + implements DerivableKey, ExportableKey, SignableKey, StorableKey { public readonly recoveryId = StorableKey.recoveryId("secp256k1", "priv"); public keySpecification: Map = new Map(); @@ -52,35 +51,27 @@ export class Secp256k1PrivateKey this.size = this.raw.length; } - derive(derivationPath: DerivationPath): Secp256k1PrivateKey { + derive(derivationPath: string): Secp256k1PrivateKey { const chainCodeHex = this.getProperty(KeyProperties.chainCode); - if (!chainCodeHex) { throw new ApolloError.MissingKeyParameters([KeyProperties.chainCode]); } - const chaincode = Buffer.from(chainCodeHex, "hex"); const derivationPathStr = derivationPath.toString(); - const hdKey = new HDKey( Int8Array.from(this.raw), null, Int8Array.from(chaincode), - 0, - BigIntegerWrapper.initFromInt(this.index ?? 0) + derivationPathStr.split("/").slice(1).length, + BigIntegerWrapper.initFromInt(0) ); - const derivedKey = hdKey.derive(derivationPathStr); - if (derivedKey.privateKey == null) { throw new ApolloError.MissingPrivateKey(); } - const privateKey = new Secp256k1PrivateKey(Buffer.from(derivedKey.privateKey)); - // TODO(BR) dont keep derivationPath as hex privateKey.keySpecification.set(KeyProperties.derivationPath, Buffer.from(derivationPathStr).toString("hex")); - privateKey.keySpecification.set(KeyProperties.index, `${derivationPath.index}`); - + privateKey.keySpecification.set(KeyProperties.index, `${this.index ?? 0}`); if (derivedKey.chainCode) { privateKey.keySpecification.set(KeyProperties.chainCode, Buffer.from(derivedKey.chainCode).toString("hex")); } @@ -102,9 +93,8 @@ export class Secp256k1PrivateKey } sign(message: Buffer) { - return Buffer.from( - Uint8Array.from(this.native.sign(Int8Array.from(message))) - ); + const normalised = normaliseDER(Buffer.from(Uint8Array.from(this.native.sign(Int8Array.from(message))))) + return normalised; } // ?? move to `from` property diff --git a/src/apollo/utils/Secp256k1PublicKey.ts b/src/apollo/utils/Secp256k1PublicKey.ts index 07fceb99a..454c4852a 100644 --- a/src/apollo/utils/Secp256k1PublicKey.ts +++ b/src/apollo/utils/Secp256k1PublicKey.ts @@ -1,7 +1,7 @@ import BN from "bn.js"; import BigInteger from "bn.js"; -import * as ECConfig from "../../config/ECConfig"; +import * as ECConfig from "../../domain/models/ECConfig"; import { ECPoint } from "./ec/ECPoint"; import { ApolloError, Curve, KeyProperties, KeyTypes, } from "../../domain"; import { @@ -13,6 +13,7 @@ import { } from "../../domain/models/keyManagement"; import ApolloPKG from "@atala/apollo"; +import { rawToDER } from "../../domain/utils/DER"; const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; /** @@ -93,7 +94,6 @@ export class Secp256k1PublicKey extends PublicKey implements StorableKey, Export `Compressed byte array's expected length is ${ECConfig.PUBLIC_KEY_COMPRESSED_BYTE_SIZE}, but got ${this.raw.length}` ); } - return Uint8Array.from(this.native.raw); } return this.raw; @@ -188,8 +188,9 @@ export class Secp256k1PublicKey extends PublicKey implements StorableKey, Export } verify(message: Buffer, signature: Buffer) { + const normalised = rawToDER(signature) return this.native.verify( - Int8Array.from(signature), + Int8Array.from(normalised), Int8Array.from(message) ); } diff --git a/src/apollo/utils/derivation/DerivationAxis.ts b/src/apollo/utils/derivation/DerivationAxis.ts deleted file mode 100644 index 79114d072..000000000 --- a/src/apollo/utils/derivation/DerivationAxis.ts +++ /dev/null @@ -1,42 +0,0 @@ -export class DerivationAxis { - constructor(private readonly i: number) {} - - /** - * Represents if the axis is hardened - */ - get hardened(): boolean { - return ((this.i >> 31) & 1) == 1; - } - - /** - * Number corresponding to the axis (different for index), always between 0 and 2^31^ - */ - get number(): number { - return this.i & ~(1 << 31); - } - - /** - * Renders axis as number with optional ' for hardened path, e.g. 1 or 7' - */ - toString(): string { - return this.hardened ? `${this.number}'` : `${this.i}`; - } - - static normal(num: number): DerivationAxis { - if (num < 0) { - throw new Error( - "number corresponding to the axis should be a positive number" - ); - } - return new DerivationAxis(num); - } - - static hardened(num: number): DerivationAxis { - if (num < 0) { - throw new Error( - "number corresponding to the axis should be a positive number" - ); - } - return new DerivationAxis(num | (1 << 31)); - } -} diff --git a/src/apollo/utils/derivation/DerivationPath.ts b/src/apollo/utils/derivation/DerivationPath.ts index da480533e..3dc39e605 100644 --- a/src/apollo/utils/derivation/DerivationPath.ts +++ b/src/apollo/utils/derivation/DerivationPath.ts @@ -1,82 +1,62 @@ -import { DerivationAxis } from "./DerivationAxis"; +import { ApolloError } from "../../../domain"; +import { DerivationClass, DerivationPathBase } from "../../../domain/models/derivation"; +import { DerivationAxis } from "../../../domain/models/derivation/DerivationAxis"; export class DerivationPath { - axes: DerivationAxis[]; - - constructor(axes: DerivationAxis[]) { - this.axes = axes; + constructor( + private paths: number[], + private derivations: DerivationClass[] + ) { } - /** - * get the index at depth 0. - * default to 0 if no value found - * - * @returns {number} - the value at depth 0 - */ - get index(): number { - const first = this.axes.at(0); - const index = first?.number ?? 0; - return index; + get axes(): DerivationAxis[] { + return this.paths.map((path) => new DerivationAxis(path)) } - /** - * Creates child derivation path for given index, hardened or not - */ - derive(axis: DerivationAxis): DerivationPath { - return new DerivationPath([...this.axes, axis]); + get index() { + return DerivationPath.callBackOrThrow( + this.derivations, + this.paths, + (path) => path.index + ) } - toString(): string { - return ["m", ...this.axes.map((axis) => axis.toString())].join("/"); + at(index: number): number { + const num = this.paths.at(index); + if (num !== undefined) { + return num + } + throw new ApolloError.InvalidDerivationPath("DerivationPathErr Incompatible Derivation schema"); } - static empty(): DerivationPath { - return new DerivationPath([]); + get schema() { + return DerivationPath.callBackOrThrow( + this.derivations, + this.paths, + (path) => path.schema + ) } - /** - * Attempt to create a DerivationPath from a value - * - * @param value - * @returns {DerivationPath} - * @throws - if the value cannot be converted to a DerivationPath - */ - static from(value: unknown): DerivationPath { - if (value instanceof DerivationPath) { - return value; - } - - if (typeof value === "string") { - if (value.at(0) === "m") { - return DerivationPath.fromPath(value); - } - } - - if (typeof value === "number") { - return DerivationPath.fromIndex(value); - } - - throw new Error(`DerivationPath value not valid [${value}]`); + derive(axis: DerivationAxis): DerivationPath { + return new DerivationPath([ + ...this.paths, + axis.number + ], this.derivations); } + static empty(derivations: DerivationClass[]): DerivationPath { + return new DerivationPath([], derivations); + } - /** - * Create a DerivationPath from an index - * - * index is hardened and used at depth 1, with two subsequent hardened 0s - * Example: index: 3 = DerivationPath: `m/3'/0'/0'` - * - * equivalent of Swift DerivableKey.keyPathString - * - * @param index - hardened index for depth 1 - * @returns {DerivationPath} - */ - static fromIndex(index: number): DerivationPath { - return new DerivationPath([ - DerivationAxis.hardened(index), - DerivationAxis.hardened(0), - DerivationAxis.hardened(0), - ]); + toString(): string { + if (!this.axes.length) { + throw new ApolloError.InvalidDerivationPath("DerivationPathErr Derivation path is empty"); + } + return DerivationPath.callBackOrThrow( + this.derivations, + this.paths, + (path) => `m/${path.axes.map((axis) => axis.toString()).join("/")}` + ) } /** @@ -85,15 +65,24 @@ export class DerivationPath { * @param path Path to parse in format m/axis1/axis2/.../axisn where all axes are number between 0 and 2^31^ - 1 and * optionally a ' added after to mark hardened axis e.g. m/21/37'/0 */ - static fromPath(path: string): DerivationPath { - const splitPath = path.split("/"); - if (splitPath.at(0)?.trim().toLowerCase() !== "m") { - throw new Error("Path needs to start with m or M"); + static fromPath(path: string, derivations: DerivationClass[]): DerivationPath { + try { + if (typeof path === "string") { + const splitPath = path.split("/"); + if (splitPath.at(0)?.trim().toLowerCase() !== "m") { + throw new ApolloError.InvalidDerivationPath("Path needs to start with m or M"); + } + const paths = splitPath.slice(1).map(DerivationPath.parseAxis).map((a) => a.number); + return DerivationPath.callBackOrThrow( + derivations, + paths, + (path) => new DerivationPath(path.axes.map((a) => a.number), derivations) + ) + } + throw new ApolloError.InvalidDerivationPath(`Derivation path should be string`) + } catch (err) { + throw new ApolloError.InvalidDerivationPath(`DerivationPathErr ${(err as Error).message}`) } - - return new DerivationPath( - splitPath.slice(1).map(DerivationPath.parseAxis) - ); } private static parseAxis(axis: string): DerivationAxis { @@ -104,4 +93,34 @@ export class DerivationPath { ? DerivationAxis.hardened(axisNum) : DerivationAxis.normal(axisNum); } -} + + + private static create( + DerivationClass: DerivationClass, + paths: number[] + ): DerivationPathBase | undefined { + try { + const path = new DerivationClass(paths); + return path; + } catch (err) { + if (!(err instanceof ApolloError.InvalidDerivationPath)) { + throw err + } + } + } + + private static callBackOrThrow( + derivations: DerivationClass[], + paths: number[], + cb: (path: DerivationPathBase) => any + ) { + for (const DerivationClass of derivations) { + const path = this.create(DerivationClass, paths); + if (path) { + return cb(path); + } + } + throw new ApolloError.InvalidDerivationPath("DerivationPathErr Incompatible Derivation schema"); + } + +} \ No newline at end of file diff --git a/src/apollo/utils/derivation/ExtendedKey.ts b/src/apollo/utils/derivation/ExtendedKey.ts deleted file mode 100644 index 955e99748..000000000 --- a/src/apollo/utils/derivation/ExtendedKey.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { HDKey } from "@scure/bip32"; -import { Secp256k1KeyPair } from "../Secp256k1KeyPair"; -import { Secp256k1PrivateKey } from "../Secp256k1PrivateKey"; -import { Secp256k1PublicKey } from "../Secp256k1PublicKey"; -import { DerivationAxis } from "./DerivationAxis"; -import { DerivationPath } from "./DerivationPath"; - -export class ExtendedKey { - constructor(private bip32: HDKey, private path: DerivationPath) {} - - /** - * Public key for this extended key - */ - public publicKey(): Secp256k1PublicKey { - return this.privateKey().publicKey(); - } - - /** - * Private key for this extended key - */ - public privateKey(): Secp256k1PrivateKey { - if (!this.bip32.privateKey) { - throw new Error("Error Bip32 PrivateKey not found"); - } - return Secp256k1PrivateKey.secp256k1FromBytes(this.bip32.privateKey); - } - - /** - * KeyPair for this extended key - */ - public keyPair(): Secp256k1KeyPair { - return new Secp256k1KeyPair(this.privateKey(), this.publicKey()); - } - - /** - * Generates child extended key for given index - */ - public derive(axis: DerivationAxis): ExtendedKey { - const derivedBip32 = this.bip32.deriveChild(axis.number); - return new ExtendedKey(derivedBip32, this.path.derive(axis)); - } -} diff --git a/src/apollo/utils/derivation/KeyDerivation.ts b/src/apollo/utils/derivation/KeyDerivation.ts deleted file mode 100644 index faa10e23c..000000000 --- a/src/apollo/utils/derivation/KeyDerivation.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HDKey } from "@scure/bip32"; - -import { DerivationAxis } from "./DerivationAxis"; -import { DerivationPath } from "./DerivationPath"; -import { ExtendedKey } from "./ExtendedKey"; - -export class KeyDerivation { - static deriveKey(seed: Uint8Array, path: DerivationPath) { - const bip32Instance = HDKey.fromMasterSeed(Buffer.from(seed)); - return path.axes.reduce( - (key: ExtendedKey, axis: DerivationAxis) => key.derive(axis), - new ExtendedKey(bip32Instance, DerivationPath.empty()) - ); - } -} diff --git a/src/castor/Castor.ts b/src/castor/Castor.ts index 6d52eb9c4..c10215e4a 100644 --- a/src/castor/Castor.ts +++ b/src/castor/Castor.ts @@ -13,11 +13,7 @@ import { DIDResolver, KeyPair, } from "../domain/models"; -import { - getUsageId, - PrismDIDPublicKey, - Usage, -} from "./did/prismDID/PrismDIDPublicKey"; + import * as DIDParser from "./parser/DIDParser"; import * as Protos from "./protos/node_models"; @@ -28,6 +24,8 @@ import { CastorError } from "../domain/models/Errors"; import { VerificationMethod as DIDDocumentVerificationMethod, VerificationMethods as DIDDocumentVerificationMethods, + getUsageId, + Usage, } from "../domain"; import { JWKHelper } from "../peer-did/helpers/JWKHelper"; @@ -43,6 +41,7 @@ import { Secp256k1PublicKey } from "../apollo/utils/Secp256k1PublicKey"; import { PublicKey, Curve } from "../domain/models"; import { X25519PublicKey } from "../apollo/utils/X25519PublicKey"; import { Ed25519PublicKey } from "../apollo/utils/Ed25519PublicKey"; +import { PrismDIDPublicKey } from "./did/prismDID/PrismDIDPublicKey"; type ExtraResolver = new (apollo: Apollo) => DIDResolver /** @@ -121,28 +120,40 @@ export default class Castor implements CastorInterface { */ async createPrismDID( key: PublicKey | KeyPair, - services?: Service[] | undefined + services?: Service[] | undefined, + issuingKeys: (PublicKey | KeyPair)[] = [] ): Promise { + const didPublicKeys: Protos.io.iohk.atala.prism.protos.PublicKey[] = []; const masterPublicKey = "publicKey" in key ? key.publicKey : key; - if (!masterPublicKey.isCurve(Curve.SECP256K1)) { - throw new CastorError.InvalidKeyError(); - } - - const publicKey = new PrismDIDPublicKey( + const masterPk = new PrismDIDPublicKey( getUsageId(Usage.MASTER_KEY), Usage.MASTER_KEY, - masterPublicKey - ); - const authenticateKey = new PrismDIDPublicKey( + masterPublicKey, + ).toProto(); + + const authenticationPk = new PrismDIDPublicKey( getUsageId(Usage.AUTHENTICATION_KEY), Usage.AUTHENTICATION_KEY, - masterPublicKey - ); + masterPublicKey, + + ).toProto(); + + if (issuingKeys.length) { + didPublicKeys.push(...issuingKeys.map((issuingKey) => new PrismDIDPublicKey( + getUsageId(Usage.ISSUING_KEY), + Usage.ISSUING_KEY, + "publicKey" in issuingKey ? issuingKey.publicKey : issuingKey, + + ).toProto())) + } + + didPublicKeys.push(authenticationPk) + didPublicKeys.push(masterPk) const didCreationData = new Protos.io.iohk.atala.prism.protos.CreateDIDOperation.DIDCreationData({ - public_keys: [authenticateKey.toProto(), publicKey.toProto()], + public_keys: didPublicKeys, services: services?.map((service) => { return new Protos.io.iohk.atala.prism.protos.Service({ service_endpoint: [service.serviceEndpoint.uri], @@ -293,27 +304,44 @@ export default class Castor implements CastorInterface { if (did.method == "prism") { const methods = verificationMethods.filter( - (method) => method.type == Curve.SECP256K1 + (method) => method.type == Curve.SECP256K1 || method.type === Curve.ED25519 ); if (methods.length <= 0) { throw new Error("No verification methods for Prism DID"); } for (const method of methods) { - if (!method.publicKeyMultibase) { - throw new Error( - "PrismDID VerificationMethod does not have multibase Key in it" - ); - } - const publicKeyEncoded = Secp256k1PublicKey.secp256k1FromBytes( - Buffer.from(base58.base58btc.decode(method.publicKeyMultibase)) - ).getEncoded(); + try { + if (!method.publicKeyMultibase) { + throw new Error( + "PrismDID VerificationMethod does not have multibase Key in it" + ); + } + if (method.type === Curve.SECP256K1) { + const publicKey = Secp256k1PublicKey.secp256k1FromBytes( + Buffer.from(base58.base58btc.decode(method.publicKeyMultibase)) + ); - publicKey = new Secp256k1PublicKey(publicKeyEncoded); - if ( - publicKey.canVerify() && - publicKey.verify(Buffer.from(challenge), Buffer.from(signature)) - ) { - return true; + if ( + publicKey.canVerify() && + publicKey.verify(Buffer.from(challenge), Buffer.from(signature)) + ) { + return true; + } + + } + if (method.type === Curve.ED25519) { + const publicKey = new Ed25519PublicKey( + Buffer.from(base58.base58btc.decode(method.publicKeyMultibase)) + ) + if ( + publicKey.canVerify() && + publicKey.verify(Buffer.from(challenge), Buffer.from(signature)) + ) { + return true; + } + } + } catch (err) { + console.debug("checking next key for verification") } } } else if (did.method == "peer") { diff --git a/src/castor/did/prismDID/PrismDIDPublicKey.ts b/src/castor/did/prismDID/PrismDIDPublicKey.ts index bcaadb9b8..7c36b30cc 100644 --- a/src/castor/did/prismDID/PrismDIDPublicKey.ts +++ b/src/castor/did/prismDID/PrismDIDPublicKey.ts @@ -1,171 +1,125 @@ -import { Secp256k1PublicKey } from "../../../apollo/utils/Secp256k1PublicKey"; -import * as ECConfig from "../../../config/ECConfig"; -import { Apollo } from "../../../domain/buildingBlocks/Apollo"; -import { Curve } from "../../../domain/models"; -import { CastorError } from "../../../domain/models/Errors"; +import * as ECConfig from "../../../domain/models/ECConfig"; +import { Curve, getProtosUsage, getUsage, PublicKey, Usage } from "../../../domain/models"; +import { ApolloError, CastorError } from "../../../domain/models/Errors"; import * as Protos from "../../protos/node_models"; -import ApolloPKG from "@atala/apollo"; -const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; -export enum Usage { - MASTER_KEY = "masterKey", - ISSUING_KEY = "issuingKey", - AUTHENTICATION_KEY = "authenticationKey", - REVOCATION_KEY = "revocationKey", - CAPABILITY_DELEGATION_KEY = "capabilityDelegationKey", - CAPABILITY_INVOCATION_KEY = "capabilityInvocationKey", - KEY_AGREEMENT_KEY = "keyAgreementKey", - UNKNOWN_KEY = "unknownKey", -} - -export function getProtosUsage( - usage: Usage -): Protos.io.iohk.atala.prism.protos.KeyUsage { - switch (usage) { - case Usage.UNKNOWN_KEY: - return Protos.io.iohk.atala.prism.protos.KeyUsage.UNKNOWN_KEY; - case Usage.MASTER_KEY: - return Protos.io.iohk.atala.prism.protos.KeyUsage.MASTER_KEY; - case Usage.ISSUING_KEY: - return Protos.io.iohk.atala.prism.protos.KeyUsage.ISSUING_KEY; - case Usage.KEY_AGREEMENT_KEY: - return Protos.io.iohk.atala.prism.protos.KeyUsage.KEY_AGREEMENT_KEY; - case Usage.AUTHENTICATION_KEY: - return Protos.io.iohk.atala.prism.protos.KeyUsage.AUTHENTICATION_KEY; - case Usage.REVOCATION_KEY: - return Protos.io.iohk.atala.prism.protos.KeyUsage.REVOCATION_KEY; - case Usage.CAPABILITY_INVOCATION_KEY: - return Protos.io.iohk.atala.prism.protos.KeyUsage - .CAPABILITY_INVOCATION_KEY; - case Usage.CAPABILITY_DELEGATION_KEY: - return Protos.io.iohk.atala.prism.protos.KeyUsage - .CAPABILITY_DELEGATION_KEY; - default: - return Protos.io.iohk.atala.prism.protos.KeyUsage.UNKNOWN_KEY; - } -} - -export function getUsageId(index: Usage): string { - switch (index) { - case Usage.MASTER_KEY: - return `master${index}`; - case Usage.ISSUING_KEY: - return `issuing${index}`; - case Usage.KEY_AGREEMENT_KEY: - return `agreement${index}`; - case Usage.AUTHENTICATION_KEY: - return `authentication${index}`; - case Usage.REVOCATION_KEY: - return `revocation${index}`; - case Usage.CAPABILITY_DELEGATION_KEY: - return `delegation${index}`; - case Usage.CAPABILITY_INVOCATION_KEY: - return `invocation${index}`; - default: - return `unknown${index}`; - } -} - -export function getUsage( - protosUsage: Protos.io.iohk.atala.prism.protos.KeyUsage -): Usage { - let usage: Usage; - switch (protosUsage) { - case 0: - usage = Usage.UNKNOWN_KEY; - break; - case 1: - usage = Usage.MASTER_KEY; - break; - case 2: - usage = Usage.ISSUING_KEY; - break; - case 3: - usage = Usage.KEY_AGREEMENT_KEY; - break; - case 4: - usage = Usage.AUTHENTICATION_KEY; - break; - case 5: - usage = Usage.REVOCATION_KEY; - break; - case 6: - usage = Usage.CAPABILITY_INVOCATION_KEY; - break; - case 7: - usage = Usage.CAPABILITY_DELEGATION_KEY; - break; - default: - usage = Usage.UNKNOWN_KEY; - break; - } - return usage; -} +import { Apollo, KeyProperties, KeyTypes } from "../../../domain"; export class PrismDIDPublicKey { - id: string; - usage: Usage; - keyData: Secp256k1PublicKey; - constructor(id: string, usage: Usage, keyData: Secp256k1PublicKey) { + constructor( + public id: string, + public usage: Usage, + public keyData: PublicKey + ) { this.id = id; this.usage = usage; this.keyData = keyData; } - static fromProto( + private static getProtoCurve(proto: Protos.io.iohk.atala.prism.protos.PublicKey) { + return proto.compressed_ec_key_data?.curve ?? proto.ec_key_data.curve + } + + private static fromSecp256k1Proto( apollo: Apollo, proto: Protos.io.iohk.atala.prism.protos.PublicKey - ): PrismDIDPublicKey { - const id = proto.id; - const usage = proto.usage; - let keyData: Secp256k1PublicKey; - + ) { switch (proto.key_data) { case "compressed_ec_key_data": - keyData = new Secp256k1PublicKey( - Uint8Array.from( - ApolloSDK.utils.KMMECSecp256k1PublicKey.Companion.secp256k1FromBytes( - Int8Array.from(proto.compressed_ec_key_data.data) - ).raw - ) - ); - break; + return apollo.createPublicKey({ + [KeyProperties.type]: KeyTypes.EC, + [KeyProperties.curve]: Curve.SECP256K1, + [KeyProperties.rawKey]: proto.compressed_ec_key_data.data + }) case "ec_key_data": - keyData = new Secp256k1PublicKey( - Uint8Array.from( - ApolloSDK.utils.KMMECSecp256k1PublicKey.Companion.secp256k1FromByteCoordinates( - Int8Array.from(proto.ec_key_data.x), - Int8Array.from(proto.ec_key_data.y) - ).raw - ) - ); - break; + return apollo.createPublicKey({ + [KeyProperties.type]: KeyTypes.EC, + [KeyProperties.curve]: Curve.SECP256K1, + [KeyProperties.curvePointX]: proto.ec_key_data.x, + [KeyProperties.curvePointY]: proto.ec_key_data.y, + }) default: throw new CastorError.InvalidPublicKeyEncoding(); } + } + + private static fromEd25519ORX25519Proto( + apollo: Apollo, + proto: Protos.io.iohk.atala.prism.protos.PublicKey + ) { + const curve = this.getProtoCurve(proto); + if (proto.has_compressed_ec_key_data) { + if (curve === Curve.ED25519) { + return apollo.createPublicKey({ + [KeyProperties.type]: KeyTypes.EC, + [KeyProperties.curve]: Curve.ED25519, + [KeyProperties.rawKey]: proto.compressed_ec_key_data.data + }) - return new PrismDIDPublicKey(id, getUsage(usage), keyData); + } + if (curve === Curve.X25519) { + return apollo.createPublicKey({ + [KeyProperties.type]: KeyTypes.Curve25519, + [KeyProperties.curve]: Curve.X25519, + [KeyProperties.rawKey]: proto.compressed_ec_key_data.data + }) + } + } + throw new CastorError.InvalidPublicKeyEncoding(); + } + + static fromProto( + apollo: Apollo, + proto: Protos.io.iohk.atala.prism.protos.PublicKey + ): PrismDIDPublicKey { + const id = proto.id; + const usage = getUsage(proto.usage); + const curve = this.getProtoCurve(proto); + if (curve === Curve.SECP256K1.toLocaleLowerCase()) { + return new PrismDIDPublicKey( + id, + usage, + this.fromSecp256k1Proto(apollo, proto) + ); + } else if (curve === Curve.ED25519 || curve === Curve.X25519) { + return new PrismDIDPublicKey( + id, + usage, + this.fromEd25519ORX25519Proto(apollo, proto) + ); + } else { + throw new ApolloError.InvalidKeyCurve(curve, Object.values(Curve)) + } } toProto(): Protos.io.iohk.atala.prism.protos.PublicKey { - const encoded = this.keyData.getEncoded(); - const xBytes = encoded.slice(1, 1 + ECConfig.PRIVATE_KEY_BYTE_SIZE); - const yBytes = encoded.slice( - 1 + ECConfig.PRIVATE_KEY_BYTE_SIZE, - encoded.length - ); - const ecKeyData = new Protos.io.iohk.atala.prism.protos.ECKeyData({ - curve: Curve.SECP256K1.toLocaleLowerCase(), - x: xBytes, - y: yBytes, - }); + const curve = this.keyData.curve; const usage = getProtosUsage(this.usage); - const publicKey = new Protos.io.iohk.atala.prism.protos.PublicKey({ + const encoded = this.keyData.getEncoded() + if (curve === Curve.SECP256K1) { + const xBytes = encoded.slice(1, 1 + ECConfig.PRIVATE_KEY_BYTE_SIZE); + const yBytes = encoded.slice( + 1 + ECConfig.PRIVATE_KEY_BYTE_SIZE, + encoded.length + ); + return new Protos.io.iohk.atala.prism.protos.PublicKey({ + id: this.id, + usage: usage, + ec_key_data: new Protos.io.iohk.atala.prism.protos.ECKeyData({ + curve: this.keyData.curve.toLocaleLowerCase(), + x: xBytes, + y: yBytes, + }), + }); + } + return new Protos.io.iohk.atala.prism.protos.PublicKey({ id: this.id, usage: usage, - ec_key_data: ecKeyData, + compressed_ec_key_data: new Protos.io.iohk.atala.prism.protos.CompressedECKeyData({ + curve: this.keyData.curve, + data: encoded, + }), }); - return publicKey; } } diff --git a/src/castor/resolver/LongFormPrismDIDResolver.ts b/src/castor/resolver/LongFormPrismDIDResolver.ts index 520a4e7d8..29b5a1031 100644 --- a/src/castor/resolver/LongFormPrismDIDResolver.ts +++ b/src/castor/resolver/LongFormPrismDIDResolver.ts @@ -14,24 +14,26 @@ import { DID, DIDUrl, DIDDocumentCoreProperty, + PublicKey, + Curve, + getUsage, + getUsageId, } from "../../domain/models"; import * as DIDParser from "../parser/DIDParser"; -import * as Protos from "../../castor/protos/node_models"; -import { - getUsage, - getUsageId, - PrismDIDPublicKey, -} from "../../castor/did/prismDID/PrismDIDPublicKey"; +import * as Protos from "../protos/node_models"; import * as base64 from "multiformats/bases/base64"; import * as base58 from "multiformats/bases/base58"; import { Secp256k1PublicKey } from "../../apollo/utils/Secp256k1PublicKey"; import { KeyProperties } from "../../domain/models/KeyProperties"; +import { Ed25519PublicKey } from "../../apollo/utils/Ed25519PublicKey"; +import { X25519PublicKey } from "../../apollo/utils/X25519PublicKey"; +import { PrismDIDPublicKey } from "../did/prismDID/PrismDIDPublicKey"; export class LongFormPrismDIDResolver implements DIDResolver { method = "prism"; - constructor(private apollo: Apollo) {} + constructor(private apollo: Apollo) { } async resolve(didString: string): Promise { const did = DIDParser.parse(didString); @@ -61,6 +63,11 @@ export class LongFormPrismDIDResolver implements DIDResolver { return new DIDDocument(did, coreProperties); } + private getProtoCurve(proto: Protos.io.iohk.atala.prism.protos.PublicKey) { + return proto.compressed_ec_key_data?.curve ?? proto.ec_key_data.curve + } + + private decodeState( did: DID, stateHash: string, @@ -84,20 +91,40 @@ export class LongFormPrismDIDResolver implements DIDResolver { const publicKeys: PrismDIDPublicKey[] = operation.create_did?.did_data?.public_keys?.map( (key: Protos.io.iohk.atala.prism.protos.PublicKey) => { - const publicKey = key.has_compressed_ec_key_data - ? Secp256k1PublicKey.secp256k1FromBytes( + const curve = this.getProtoCurve(key).toLocaleLowerCase() + let pk: PublicKey; + if (curve === Curve.SECP256K1.toLocaleLowerCase()) { + pk = key.has_compressed_ec_key_data + ? Secp256k1PublicKey.secp256k1FromBytes( key.compressed_ec_key_data.data ) - : Secp256k1PublicKey.secp256k1FromByteCoordinates( + : Secp256k1PublicKey.secp256k1FromByteCoordinates( key.ec_key_data.x, key.ec_key_data.y ); - + } else if (curve === Curve.ED25519.toLocaleLowerCase()) { + if (!key.has_compressed_ec_key_data) { + throw new Error("Expected compressed compressed key") + } + pk = Ed25519PublicKey.from.Buffer( + Buffer.from(key.compressed_ec_key_data.data) + ) + } else if (curve === Curve.X25519.toLocaleLowerCase()) { + if (!key.has_compressed_ec_key_data) { + throw new Error("Expected compressed compressed key") + } + pk = X25519PublicKey.from.Buffer( + Buffer.from(key.compressed_ec_key_data.data) + ) + } else { + throw new Error("Unsupported key type") + } + const usage = getUsage(key.usage) return new PrismDIDPublicKey( - getUsageId(getUsage(key.usage)), - getUsage(key.usage), - publicKey - ); + getUsageId(usage), + usage, + pk, + ) } ) || []; diff --git a/src/domain/buildingBlocks/Apollo.ts b/src/domain/buildingBlocks/Apollo.ts index a7438f7ae..8aa7cdd7f 100644 --- a/src/domain/buildingBlocks/Apollo.ts +++ b/src/domain/buildingBlocks/Apollo.ts @@ -1,4 +1,4 @@ -import { PrivateKey, Seed, SeedWords } from "../models"; +import { PrivateKey, PublicKey, Seed, SeedWords } from "../models"; import { KeyProperties } from "../models/KeyProperties"; import { MnemonicWordList } from "../models/WordList"; @@ -10,4 +10,7 @@ export interface Apollo { createPrivateKey(parameters: { [name: KeyProperties | string]: any; }): PrivateKey; + createPublicKey(parameters: { + [name: KeyProperties | string]: any; + }): PublicKey; } diff --git a/src/domain/buildingBlocks/Castor.ts b/src/domain/buildingBlocks/Castor.ts index 1682c49b4..a365dec59 100644 --- a/src/domain/buildingBlocks/Castor.ts +++ b/src/domain/buildingBlocks/Castor.ts @@ -1,11 +1,12 @@ -import { DID, DIDDocument, Service as DIDDocumentService } from "../models"; +import { DID, DIDDocument, Service as DIDDocumentService, KeyPair } from "../models"; import { PublicKey } from "../models"; export interface Castor { parseDID(did: string): DID; createPrismDID( masterPublicKey: PublicKey, - services?: DIDDocumentService[] + services?: DIDDocumentService[], + authenticationKeys?: (PublicKey | KeyPair)[] ): Promise; createPeerDID( publicKeys: PublicKey[], diff --git a/src/domain/buildingBlocks/Pollux.ts b/src/domain/buildingBlocks/Pollux.ts index 8db0aa9a1..80232238d 100644 --- a/src/domain/buildingBlocks/Pollux.ts +++ b/src/domain/buildingBlocks/Pollux.ts @@ -1,22 +1,54 @@ import { AnonCredsCredential } from "../../pollux/models/AnonCredsVerifiableCredential"; import { PresentationRequest } from "../../pollux/models/PresentationRequest"; import { JWTCredential } from "../../pollux/models/JWTVerifiableCredential"; -import { AttributeType, CredentialType, DID, LinkSecret, Message, PresentationClaims, PresentationDefinitionRequest, PresentationOptions, PresentationSubmission, PrivateKey, PublicKey } from "../models"; +import { AttachmentFormats, CredentialType, DID, LinkSecret, PresentationClaims, PresentationDefinitionRequest, PresentationOptions, PresentationSubmission, PrivateKey } from "../models"; import { Credential, CredentialRequestOptions } from "../models/Credential"; import type * as Anoncreds from "anoncreds-browser"; +import { SDJWTCredential } from "../../pollux/models/SDJWTVerifiableCredential"; export type CredentialRequestTuple< T1 = Anoncreds.CredentialRequestType, T2 = Anoncreds.CredentialRequestMetadataType > = [T1, T2]; + + +export type CredentialOfferJWTBasePayload = { + options: { + challenge: string; + domain: string; + } +} + +export type CredentialOfferPayloads = { + [CredentialType.AnonCreds]: Anoncreds.CredentialOfferType; + [CredentialType.JWT]: CredentialOfferJWTBasePayload; + [CredentialType.SDJWT]: CredentialOfferJWTBasePayload; + + [CredentialType.Unknown]: unknown; + [CredentialType.W3C]: unknown; +}; + +export type CredentialOfferTypes = + CredentialType.AnonCreds | + CredentialType.JWT | + CredentialType.SDJWT; + +export type ProcessedCredentialOfferPayloads = { + [CredentialType.AnonCreds]: CredentialRequestTuple; + [CredentialType.JWT]: string; + [CredentialType.SDJWT]: string; + [CredentialType.Unknown]: unknown; + [CredentialType.W3C]: unknown; +}; + + /** * Pollux * handle Credential related tasks */ export interface Pollux { - revealCredentialFields: (credential: Credential, fields: string[], linkSecret: string) => Promise<{ [name: string]: any }>; @@ -25,15 +57,13 @@ export interface Pollux { credentialBuffer: Uint8Array, options?: { type: CredentialType;[name: string]: any; } ) => Promise; - processJWTCredential( - offer: Message, - options: CredentialRequestOptions - ): Promise; - processAnonCredsCredential( - offer: Message, + + processCredentialOffer< + Types extends CredentialOfferTypes + >( + offer: CredentialOfferPayloads[Types], options: CredentialRequestOptions - ): Promise; - extractCredentialFormatFromMessage(message: Message): CredentialType; + ): Promise; createPresentationSubmission( presentationDefinition: PresentationDefinitionRequest, @@ -89,14 +119,15 @@ export interface Pollux { * @returns dependent on the CredentialType * @throws */ - createPresentationProof(presentationRequest: PresentationRequest, credential: AnonCredsCredential, options: Pollux.createPresentationProof.options.Anoncreds): Promise; - createPresentationProof(presentationRequest: PresentationRequest, credential: JWTCredential, options: Pollux.createPresentationProof.options.JWT): Promise; - createPresentationProof(presentationRequest: PresentationRequest, credential: Credential, options?: Record): Promise; + createPresentationProof(presentationRequest: PresentationRequest, credential: AnonCredsCredential, options: Pollux.createPresentationProof.options.Anoncreds): Promise; + createPresentationProof(presentationRequest: PresentationRequest, credential: JWTCredential, options: Pollux.createPresentationProof.options.JWT): Promise; + createPresentationProof(presentationRequest: PresentationRequest, credential: SDJWTCredential, options: Pollux.createPresentationProof.options.SDJWT): Promise; + createPresentationProof(presentationRequest: PresentationRequest, credential: Credential, options?: Record): Promise; } export namespace Pollux { export namespace verifyPresentationSubmission { - export type options = options.Anoncreds | options.JWT; + export type options = options.Anoncreds | options.JWT | options.SDJWT; export namespace options { export interface Anoncreds { [name: string | number | symbol]: any; @@ -105,10 +136,13 @@ export namespace Pollux { export interface JWT { presentationDefinitionRequest: PresentationDefinitionRequest, } + export interface SDJWT { + presentationDefinitionRequest: PresentationDefinitionRequest, + } } } export namespace createPresentationProof { - export type options = options.Anoncreds | options.JWT; + export type options = options.Anoncreds | options.JWT | options.SDJWT; export namespace options { export interface Anoncreds { linkSecret: LinkSecret; @@ -117,6 +151,11 @@ export namespace Pollux { did: DID; privateKey: PrivateKey; } + export interface SDJWT { + did: DID; + privateKey: PrivateKey; + + } } } } diff --git a/src/config/ECConfig.ts b/src/domain/models/ECConfig.ts similarity index 100% rename from src/config/ECConfig.ts rename to src/domain/models/ECConfig.ts diff --git a/src/domain/models/KeyProperties.ts b/src/domain/models/KeyProperties.ts index f53b6ee42..2f63f5d41 100644 --- a/src/domain/models/KeyProperties.ts +++ b/src/domain/models/KeyProperties.ts @@ -14,6 +14,11 @@ export enum KeyProperties { */ seed = "seed", + /** + * The 'derivationSchema' corresponds to derivationSchema used. + */ + derivationSchema = "derivationSchema", + /** * The 'chainCode' used for key derivation. * hex encoded value. diff --git a/src/domain/models/Message.ts b/src/domain/models/Message.ts index 8c1f223b7..8df5796ba 100644 --- a/src/domain/models/Message.ts +++ b/src/domain/models/Message.ts @@ -1,16 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { v4 as uuidv4 } from "uuid"; import { DID } from "./DID"; import { AttachmentBase64, AttachmentData, AttachmentDescriptor, + AttachmentFormats, AttachmentJsonData, } from "./MessageAttachment"; import { AgentError } from "./Errors"; -import { JsonString } from "."; +import { CredentialType, JsonString } from "."; import { Pluto } from "../buildingBlocks/Pluto"; import { isObject } from "../../utils"; +import { base64 } from "multiformats/bases/base64"; export enum MessageDirection { SENT = 0, @@ -18,11 +19,11 @@ export enum MessageDirection { } export class Message implements Pluto.Storable { - public readonly uuid = Pluto.makeUUID(); + public uuid: string; constructor( public readonly body: string, - public readonly id: string = uuidv4(), + public readonly id: string = Pluto.makeUUID(), public readonly piuri: string, public readonly from?: DID, public readonly to?: DID, @@ -39,10 +40,46 @@ export class Message implements Pluto.Storable { public direction: MessageDirection = MessageDirection.RECEIVED, public readonly fromPrior?: string, public readonly pthid?: string - ) {} + ) { + this.uuid = Pluto.makeUUID(); + } + + get safeBody() { + try { + return JSON.parse(this.body) + } catch (err) { + return {} + } + } + + get credentialFormat() { + const [attachment] = this.attachments; + if (!attachment) { + throw new Error("Required Attachment"); + } + const body = this.safeBody; + const format = body.formats?.find((format: any) => format.attach_id === attachment.id)?.format ?? attachment.format; + if ( + format === AttachmentFormats.AnonCreds || + format === AttachmentFormats.ANONCREDS_PROOF_REQUEST || + format === AttachmentFormats.ANONCREDS_OFFER || + format === AttachmentFormats.ANONCREDS_ISSUE || + format === AttachmentFormats.ANONCREDS_REQUEST + ) { + return CredentialType.AnonCreds; + } + if (format === CredentialType.JWT) { + return CredentialType.JWT; + } + if (format === CredentialType.SDJWT) { + return CredentialType.SDJWT; + } + return CredentialType.Unknown; + } + + static fromJson(jsonString: JsonString | any): Message { + const messageObj = typeof jsonString === "object" ? jsonString : JSON.parse(jsonString); - static fromJson(jsonString: JsonString): Message { - const messageObj = JSON.parse(jsonString); if (!messageObj.body || typeof messageObj.body !== "string") { throw new AgentError.InvalidMessageError("undefined or wrong body"); } @@ -103,10 +140,13 @@ export class Message implements Pluto.Storable { const fromPrior = messageObj.fromPrior; const pthid = messageObj.pthid; - const fromDID = messageObj.from - ? DID.fromString(messageObj.from) - : undefined; - const toDID = messageObj.to ? DID.fromString(messageObj.to) : undefined; + const fromDID = messageObj.from ? + messageObj.from instanceof DID ? messageObj.from : + DID.fromString(messageObj.from) : undefined; + + const toDID = messageObj.to ? + messageObj.to instanceof DID ? messageObj.to : + DID.fromString(messageObj.to) : undefined; return new Message( body, @@ -145,17 +185,19 @@ export namespace Message { */ export const extractJSON = (attachment: AttachmentDescriptor) => { if (isBase64(attachment.data)) { - const decoded = Buffer.from(attachment.data.base64, "base64").toString(); - const json = JSON.parse(decoded); - - return json; + const decoded = Buffer.from(base64.baseDecode(attachment.data.base64)).toString(); + try { + return JSON.parse(decoded) + } catch (err) { + return decoded + } } if (isJson(attachment.data)) { - // TODO wrong Types - attachment has already been parsed from string to json (by didcomm lib?) - return typeof attachment.data.data === "object" - ? attachment.data.data - : JSON.parse(attachment.data.data); + const decoded = attachment.data.data + return typeof decoded === "object" + ? decoded + : JSON.parse(decoded); } // TODO better error diff --git a/src/domain/models/MessageAttachment.ts b/src/domain/models/MessageAttachment.ts index 1ca71772f..28fbb076a 100644 --- a/src/domain/models/MessageAttachment.ts +++ b/src/domain/models/MessageAttachment.ts @@ -1,5 +1,6 @@ import { uuid } from "@stablelib/uuid"; import { base64 } from "multiformats/bases/base64"; +import { Message } from ".."; export interface AttachmentHeader { children: string; } @@ -48,6 +49,10 @@ export class AttachmentDescriptor { public readonly description?: string ) { } + get payload() { + return Message.Attachment.extractJSON(this) + } + static build( payload: T, id: string = uuid(), @@ -87,5 +92,7 @@ export enum AttachmentFormats { PRESENTATION_EXCHANGE_DEFINITIONS = "dif/presentation-exchange/definitions@v1.0", PRESENTATION_EXCHANGE_SUBMISSION = "dif/presentation-exchange/submission@v1.0", JWT = "prism/jwt", + SDJWT = "vc+sd-jwt", AnonCreds = "AnonCreds", } + diff --git a/src/domain/models/VerifiableCredential.ts b/src/domain/models/VerifiableCredential.ts index 945abd94f..3330fa14d 100644 --- a/src/domain/models/VerifiableCredential.ts +++ b/src/domain/models/VerifiableCredential.ts @@ -2,6 +2,7 @@ import type * as Anoncreds from "anoncreds-browser"; export enum CredentialType { JWT = "prism/jwt", + SDJWT = "vc+sd-jwt", W3C = "w3c", AnonCreds = "AnonCreds", Unknown = "Unknown" @@ -28,6 +29,19 @@ export enum W3CVerifiableCredentialType { credential = "VerifiableCredential" } +export enum SDJWTVerifiableCredentialProperties { + iss = "iss", + sub = "sub", + jti = "jti", + nbf = "nbf", + exp = "exp", + aud = "aud", + vct = "vct", + revoked = "revoked", + _sd_alg = "_sd_alg", + _sd = "_sd", + disclosures = "disclosures" +} export enum JWTVerifiableCredentialProperties { iss = "iss", @@ -152,9 +166,12 @@ export type PresentationExchangeDefinitionRequest = { } } + + export type PresentationDefinitionData = { [CredentialType.AnonCreds]: PresentationAnoncredsRequest; [CredentialType.JWT]: PresentationExchangeDefinitionRequest; + [CredentialType.SDJWT]: any; [CredentialType.Unknown]: any; [CredentialType.W3C]: any; }; @@ -164,10 +181,10 @@ export type PresentationDefinitionData = { export class PresentationDefinitionRequestType { constructor(public data: PresentationDefinitionData[Type]) { } - static fromData( - data: PresentationDefinitionData[CredentialType] - ): PresentationDefinitionRequestType { - return new PresentationDefinitionRequestType(data) + static fromData( + data: PresentationDefinitionData + ): PresentationDefinitionRequestType { + return new PresentationDefinitionRequestType(data) } } @@ -200,6 +217,7 @@ export type AnoncredsPresentationSubmission = Anoncreds.PresentationType; export type PresentationSubmissionData = { [CredentialType.AnonCreds]: AnoncredsPresentationSubmission; [CredentialType.JWT]: JWTPresentationSubmission; + [CredentialType.SDJWT]: any; [CredentialType.Unknown]: any; [CredentialType.W3C]: any; } @@ -286,9 +304,24 @@ export type W3CVerifiablePresentationProof = { challenge: string, domain: string } - +export type Hasher = (data: string, alg: string) => Promise; +export type Signer = (data: string | Uint8Array) => Promise; +export type Verifier = (data: string, sig: string) => Promise; + +export type JWTHeader = { + typ: 'JWT' + alg: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [x: string]: any +} export type JWTPayload = JWTCredentialPayload | JWTPresentationPayload; +export type JWTObject = { + header: JWTHeader + payload: JWTPayload + signature: string + data: string +} export type PresentationJWTOptions = { jwtAlg?: string[], @@ -301,15 +334,12 @@ export type PresentationRequestOptions = { [CredentialType.W3C]: any; } -export type PresentationOptionsSS = - PresentationRequestOptions[Type] - export class PresentationOptions { constructor( - private data: PresentationRequestOptions[CredentialType] = {}, + private data: any = {}, private type: CredentialType = CredentialType.JWT ) { @@ -329,7 +359,7 @@ export class PresentationOptions { export class AnoncredsPresentationOptions { - constructor(data: any) { } + constructor(_data: any) { } } export class JWTPresentationOptions { @@ -353,7 +383,13 @@ export class JWTPresentationOptions { this.challenge = options.challenge; this.domain = options.domain ?? 'N/A'; this.jwt = options.jwt ?? { - jwtAlg: ['ES256K'], + jwtAlg: [JWT_ALG.ES256K], }; } +} + +export enum JWT_ALG { + ES256K = "ES256K", + EdDSA = "EdDSA", + unknown = 'unknown' } \ No newline at end of file diff --git a/src/domain/models/derivation/DerivationAxis.ts b/src/domain/models/derivation/DerivationAxis.ts new file mode 100644 index 000000000..045c79816 --- /dev/null +++ b/src/domain/models/derivation/DerivationAxis.ts @@ -0,0 +1,56 @@ +import { ApolloError } from "../../../domain"; + +export class DerivationAxis { + constructor(private readonly i: number) { } + + /** + * Represents if the axis is hardened + */ + get hardened(): boolean { + return ((this.i >> 31) & 1) == 1; + } + + /** + * Number corresponding to the axis (different for index), always between 0 and 2^31^ + */ + get number(): number { + return this.i & ~(1 << 31); + } + + /** + * Renders axis as number with optional ' for hardened path, e.g. 1 or 7' + */ + toString(): string { + return this.hardened ? `${this.number}'` : `${this.i}`; + } + + private static safeCast(num: any): number { + const safeNum = parseInt(num) + if (Number.isNaN(safeNum)) { + throw new ApolloError.InvalidDerivationPath( + "Invalid axis, not a number" + ); + } + return safeNum + } + + static normal(num: number): DerivationAxis { + const safeNum = this.safeCast(num) + if (safeNum < 0) { + throw new ApolloError.InvalidDerivationPath( + "Number corresponding to the axis should be a positive number" + ); + } + return new DerivationAxis(safeNum); + } + + static hardened(num: number): DerivationAxis { + const safeNum = this.safeCast(num) + if (safeNum < 0) { + throw new ApolloError.InvalidDerivationPath( + "Number corresponding to the axis should be a positive number" + ); + } + return new DerivationAxis(safeNum | (1 << 31)); + } +} diff --git a/src/domain/models/derivation/index.ts b/src/domain/models/derivation/index.ts new file mode 100644 index 000000000..f2f91c85f --- /dev/null +++ b/src/domain/models/derivation/index.ts @@ -0,0 +1,29 @@ +import { DerivationAxis } from "./DerivationAxis"; + +export class AxesArray extends Array { + override toString(): string { + return `m/${this.map((axis) => axis.toString()).join("/")}` + } +} + + +export interface BaseSchema { + [name: string]: number +} + + +export abstract class DerivationPathBase { + constructor(protected variables: T) { } + abstract toString(): string; + abstract axes: AxesArray; + abstract schema: string; + abstract index: number; +} + + +export type DerivationSchema = DerivationPathBase + +export type DerivationClass< + T extends BaseSchema = BaseSchema +> = new (variables: number[]) => DerivationPathBase; + diff --git a/src/domain/models/derivation/schemas/DeprecatedDerivation.ts b/src/domain/models/derivation/schemas/DeprecatedDerivation.ts new file mode 100644 index 000000000..22cc24a90 --- /dev/null +++ b/src/domain/models/derivation/schemas/DeprecatedDerivation.ts @@ -0,0 +1,58 @@ +import { BaseSchema, DerivationPathBase, AxesArray } from ".."; +import { ApolloError } from "../../Errors"; +import { DerivationAxis } from "../DerivationAxis"; + + +interface DeprecatedDerivationSchema extends BaseSchema { + keyType: number + didIndex: number + keyIndex: number +} +export const DeprecatedDerivationPathSchema = "deprecated"; +export class DeprecatedDerivationPath extends DerivationPathBase { + + schema = DeprecatedDerivationPathSchema + + constructor(paths: number[]) { + if (paths.length !== 3) { + throw new ApolloError.InvalidDerivationPath("Incorrect Derivation Schema") + } + const [keyType, didIndex, keyIndex] = paths; + if (typeof keyType === 'undefined' || + typeof didIndex === 'undefined' || + typeof keyIndex === 'undefined' + ) { + throw new ApolloError.InvalidDerivationPath("Incorrect Derivation Schema") + } + super({ keyType, didIndex, keyIndex }) + } + + get index(): number { + return this.keyIndex.number + } + + get keyType() { + return DerivationAxis.hardened(this.variables.keyType) + } + + get didIndex() { + return DerivationAxis.hardened(this.variables.didIndex) + } + + get keyIndex() { + return DerivationAxis.hardened(this.variables.keyIndex) + } + + + toString(): string { + return this.axes.toString() + } + + get axes() { + return AxesArray.from([ + this.keyType, + this.didIndex, + this.keyIndex + ]) + } +} \ No newline at end of file diff --git a/src/domain/models/derivation/schemas/PrismDerivation.ts b/src/domain/models/derivation/schemas/PrismDerivation.ts new file mode 100644 index 000000000..d4d1e21cc --- /dev/null +++ b/src/domain/models/derivation/schemas/PrismDerivation.ts @@ -0,0 +1,88 @@ +import { AxesArray, BaseSchema, DerivationPathBase } from ".." +import { ApolloError } from "../../Errors" +import { KeyUsage } from "../../keyManagement" +import { DerivationAxis } from "../DerivationAxis" + +interface PrismDerivationSchema extends BaseSchema { + walletPurpose: number + didMethod: number + didIndex: number + keyPurpose: number + keyIndex: number +} + +export const PRISM_IDENTIFIER = 0x1D; +export const PRISM_WALLET_PURPOSE = PRISM_IDENTIFIER; +export const PRISM_DID_METHOD = PRISM_IDENTIFIER; +export const AUTHENTICATION_KEY = KeyUsage.AUTHENTICATION_KEY; +export const MASTER_KEY = KeyUsage.MASTER_KEY; +export const ISSUING_KEY = KeyUsage.ISSUING_KEY; +export const PrismDerivationPathSchema = "prism"; + +export class PrismDerivationPath extends DerivationPathBase { + schema = PrismDerivationPathSchema + + constructor(paths: number[]) { + if (paths.length !== 5) { + throw new ApolloError.InvalidDerivationPath("Incorrect Derivation Schema") + } + const [walletPurpose, didMethod, didIndex, keyPurpose, keyIndex] = paths; + if (typeof walletPurpose === 'undefined' || + typeof didMethod === 'undefined' || + typeof didIndex === 'undefined' || + typeof keyPurpose === 'undefined' || + typeof keyIndex === 'undefined' + ) { + throw new ApolloError.InvalidDerivationPath("Incorrect Derivation Schema") + } + super({ walletPurpose, didMethod, didIndex, keyPurpose, keyIndex }) + } + + get index(): number { + return this.keyIndex.number + } + + get walletPurpose() { + return DerivationAxis.hardened(this.variables.walletPurpose) + } + + get didMethod() { + return DerivationAxis.hardened(this.variables.didMethod) + } + + get didIndex() { + return DerivationAxis.hardened(this.variables.didIndex) + } + + get keyPurpose() { + return DerivationAxis.hardened(this.variables.keyPurpose) + } + + get keyIndex() { + return DerivationAxis.hardened(this.variables.keyIndex) + } + + static init(keyIndex = 0, didIndex = 0, keyPurpose: number = AUTHENTICATION_KEY): PrismDerivationPath { + return new PrismDerivationPath([ + PRISM_WALLET_PURPOSE, + PRISM_DID_METHOD, + didIndex, + keyPurpose, + keyIndex + ]) + } + + toString(): string { + return this.axes.toString() + } + + get axes() { + return AxesArray.from([ + this.walletPurpose, + this.didMethod, + this.didIndex, + this.keyPurpose, + this.keyIndex + ]) + } +} diff --git a/src/domain/models/errors/Apollo.ts b/src/domain/models/errors/Apollo.ts index e19474f6e..a1e66b5d3 100644 --- a/src/domain/models/errors/Apollo.ts +++ b/src/domain/models/errors/Apollo.ts @@ -1,3 +1,4 @@ +import { SupportedHashingAlg } from "../../utils/hash"; import { StorableKey } from "../keyManagement"; export class InvalidMnemonicWord extends Error { @@ -18,6 +19,13 @@ export class InvalidPrivateKey extends Error { } } +export class InvalidHashingAlgorithm extends Error { + constructor(message?: string) { + const supported = Object.keys(Object.fromEntries(Object.entries(SupportedHashingAlg))) + super(message || `Invalid Hashing Algorithm, supported ${supported.join(", ")}`); + } +} + export class InvalidKeyCurve extends Error { constructor(invalidKeyCurve: string, validKeyCurves: string[]) { diff --git a/src/domain/models/keyManagement/DerivableKey.ts b/src/domain/models/keyManagement/DerivableKey.ts index 5415ae22b..a83e9c932 100644 --- a/src/domain/models/keyManagement/DerivableKey.ts +++ b/src/domain/models/keyManagement/DerivableKey.ts @@ -1,6 +1,5 @@ -import { DerivationPath } from "../../../apollo/utils/derivation/DerivationPath"; import { PrivateKey } from "./PrivateKey"; export abstract class DerivableKey { - abstract derive(derivationPath: DerivationPath): PrivateKey; + abstract derive(derivationPath: string): PrivateKey; } diff --git a/src/domain/models/keyManagement/Key.ts b/src/domain/models/keyManagement/Key.ts index 2970772dd..ebaa8d030 100644 --- a/src/domain/models/keyManagement/Key.ts +++ b/src/domain/models/keyManagement/Key.ts @@ -8,7 +8,123 @@ import { KeyCurve } from "../KeyCurve"; import { Curve } from "./Curve"; import { KeyTypes } from "./KeyTypes"; import { ExportableKey } from "./exportable"; +import { JWT_ALG } from "../VerifiableCredential"; +export enum KeyUsage { + UNKNOWN_KEY = 0, + MASTER_KEY = 1, + ISSUING_KEY = 2, + KEY_AGREEMENT_KEY = 3, + AUTHENTICATION_KEY = 4, + REVOCATION_KEY = 5, + CAPABILITY_INVOCATION_KEY = 6, + CAPABILITY_DELEGATION_KEY = 7 +} + +export function getProtosUsage( + usage: Usage +): KeyUsage { + switch (usage) { + case Usage.UNKNOWN_KEY: + return KeyUsage.UNKNOWN_KEY; + case Usage.MASTER_KEY: + return KeyUsage.MASTER_KEY; + case Usage.ISSUING_KEY: + return KeyUsage.ISSUING_KEY; + case Usage.KEY_AGREEMENT_KEY: + return KeyUsage.KEY_AGREEMENT_KEY; + case Usage.AUTHENTICATION_KEY: + return KeyUsage.AUTHENTICATION_KEY; + case Usage.REVOCATION_KEY: + return KeyUsage.REVOCATION_KEY; + case Usage.CAPABILITY_INVOCATION_KEY: + return KeyUsage + .CAPABILITY_INVOCATION_KEY; + case Usage.CAPABILITY_DELEGATION_KEY: + return KeyUsage + .CAPABILITY_DELEGATION_KEY; + default: + return KeyUsage.UNKNOWN_KEY; + } +} + + +export function getUsageId(index: Usage): string { + switch (index) { + case Usage.MASTER_KEY: + return `master${index}`; + case Usage.ISSUING_KEY: + return `issuing${index}`; + case Usage.KEY_AGREEMENT_KEY: + return `agreement${index}`; + case Usage.AUTHENTICATION_KEY: + return `authentication${index}`; + case Usage.REVOCATION_KEY: + return `revocation${index}`; + case Usage.CAPABILITY_DELEGATION_KEY: + return `delegation${index}`; + case Usage.CAPABILITY_INVOCATION_KEY: + return `invocation${index}`; + default: + return `unknown${index}`; + } +} + +export function getUsage( + protosUsage: KeyUsage +): Usage { + let usage: Usage; + switch (protosUsage) { + case 0: + usage = Usage.UNKNOWN_KEY; + break; + case 1: + usage = Usage.MASTER_KEY; + break; + case 2: + usage = Usage.ISSUING_KEY; + break; + case 3: + usage = Usage.KEY_AGREEMENT_KEY; + break; + case 4: + usage = Usage.AUTHENTICATION_KEY; + break; + case 5: + usage = Usage.REVOCATION_KEY; + break; + case 6: + usage = Usage.CAPABILITY_INVOCATION_KEY; + break; + case 7: + usage = Usage.CAPABILITY_DELEGATION_KEY; + break; + default: + usage = Usage.UNKNOWN_KEY; + break; + } + return usage; +} + +export enum Usage { + MASTER_KEY = "masterKey", + ISSUING_KEY = "issuingKey", + AUTHENTICATION_KEY = "authenticationKey", + REVOCATION_KEY = "revocationKey", + CAPABILITY_DELEGATION_KEY = "capabilityDelegationKey", + CAPABILITY_INVOCATION_KEY = "capabilityInvocationKey", + KEY_AGREEMENT_KEY = "keyAgreementKey", + UNKNOWN_KEY = "unknownKey", +} +export function curveToAlg(curve: string) { + if (curve === Curve.SECP256K1) { + return JWT_ALG.ES256K; + } + if (curve === Curve.ED25519 || curve === Curve.X25519) { + return JWT_ALG.EdDSA; + } + return JWT_ALG.unknown +} export function getKeyCurveByNameAndIndex( name: string, index?: number @@ -39,6 +155,12 @@ export abstract class Key { return this.getProperty(KeyProperties.curve)!; } + get alg(): JWT_ALG { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const curve = this.getProperty(KeyProperties.curve)!; + return curveToAlg(curve) + } + isDerivable(): this is DerivableKey { return "derive" in this; } diff --git a/src/domain/models/keyManagement/KeyTypes.ts b/src/domain/models/keyManagement/KeyTypes.ts index 23c2b0159..068625641 100644 --- a/src/domain/models/keyManagement/KeyTypes.ts +++ b/src/domain/models/keyManagement/KeyTypes.ts @@ -1,4 +1,5 @@ export enum KeyTypes { "EC" = "EC", "Curve25519" = "Curve25519", + "unknown" = "unknown" } diff --git a/src/domain/models/keyManagement/StorableKey.ts b/src/domain/models/keyManagement/StorableKey.ts index 32391034c..ae0c30f75 100644 --- a/src/domain/models/keyManagement/StorableKey.ts +++ b/src/domain/models/keyManagement/StorableKey.ts @@ -6,8 +6,6 @@ export interface StorableKey { } export namespace StorableKey { - // export type RecoveryId = `${RecoveryId.algorithm}+${RecoveryId.privacy}`; - namespace RecoveryId { export type algorithm = "secp256k1" | "x25519" | "ed25519"; export type suffix = privacy; diff --git a/src/domain/utils/DER.ts b/src/domain/utils/DER.ts new file mode 100644 index 000000000..d3b438dfd --- /dev/null +++ b/src/domain/utils/DER.ts @@ -0,0 +1,108 @@ +/** + * Fix around normalising DER signatures into their raw representation + * @param derSignature Buffer + * @returns Buffer + */ +export function normaliseDER(derSignature: Buffer): Buffer { + // Ensure the DER signature starts with the correct sequence header + if (derSignature[0] !== 0x30) { + return derSignature; + } + // Get the length of the sequence + let seqLength = derSignature[1]; + let offset = 2; + if (seqLength & 0x80) { + const lengthBytes = seqLength & 0x7f; + seqLength = 0; + for (let i = 0; i < lengthBytes; i++) { + seqLength = (seqLength << 8) | derSignature[offset++]; + } + } + + if (derSignature[offset++] !== 0x02) { + throw new Error('Invalid DER signature: expected integer for r'); + } + + const rLength = derSignature[offset++]; + let r = derSignature.slice(offset, offset + rLength); + offset += rLength; + + // Extract s value + if (derSignature[offset++] !== 0x02) { + throw new Error('Invalid DER signature: expected integer for s'); + } + const sLength = derSignature[offset++]; + let s = derSignature.slice(offset, offset + sLength); + + // Normalize r and s to 32 bytes + if (r.length > 32) { + r = r.slice(-32); // truncate if r is longer than 32 bytes + } else if (r.length < 32) { + const paddedR = Buffer.alloc(32); + r.copy(paddedR, 32 - r.length); + r = paddedR; // left pad with zeros if r is shorter than 32 bytes + } + + if (s.length > 32) { + s = s.slice(-32); // truncate if s is longer than 32 bytes + } else if (s.length < 32) { + const paddedS = Buffer.alloc(32); + s.copy(paddedS, 32 - s.length); + s = paddedS; // left pad with zeros if s is shorter than 32 bytes + } + + // Concatenate r and s to form the raw signature + return Buffer.concat([r, s]); +} + +/** + * Converts a raw signature to DER format + * @param rawSignature Buffer + * @returns Buffer + */ +export function rawToDER(rawSignature: Buffer): Buffer { + if (rawSignature.length !== 64) { + return rawSignature; + } + + let r = rawSignature.slice(0, 32); + let s = rawSignature.slice(32, 64); + + r = removeLeadingZeros(r); + s = removeLeadingZeros(s); + + // Ensure the integers are positive by prepending a zero byte if necessary + if (r[0] & 0x80) { + r = Buffer.concat([Buffer.from([0x00]), r]); + } + if (s[0] & 0x80) { + s = Buffer.concat([Buffer.from([0x00]), s]); + } + + const rLen = r.length; + const sLen = s.length; + + const derSignature = Buffer.concat([ + Buffer.from([0x30, rLen + sLen + 4]), // Sequence tag and length + Buffer.from([0x02, rLen]), // Integer tag and length for r + r, + Buffer.from([0x02, sLen]), // Integer tag and length for s + s + ]); + + return derSignature; +} + +/** + * Remove leading zeros from a buffer + * @param buffer Buffer + * @returns Buffer + */ +function removeLeadingZeros(buffer: Buffer): Buffer { + const arr = Array.from(buffer) + let i = 0; + while (i < arr.length - 1 && arr[i] === 0) { + i++; + } + return Buffer.from(arr.slice(i)); +} diff --git a/src/domain/utils/hash.ts b/src/domain/utils/hash.ts new file mode 100644 index 000000000..b82f38794 --- /dev/null +++ b/src/domain/utils/hash.ts @@ -0,0 +1,43 @@ +import { sha512, sha256 } from "hash.js"; +import { ApolloError } from "../../domain"; + +export enum SupportedHashingAlg { + SHA256 = 'SHA256', + SHA512 = 'SHA512' +} + +function isSupported(alg: string): boolean { + if (alg === SupportedHashingAlg.SHA256) { + return true; + } + if (alg === SupportedHashingAlg.SHA512) { + return true; + } + return true; +} + +export async function hash(data: string | Uint8Array, alg: string) { + if (!isSupported(alg)) { + throw new ApolloError.InvalidHashingAlgorithm() + } + if (alg === SupportedHashingAlg.SHA256) { + return Uint8Array.from(sha256().update(data).digest()); + } + if (alg === SupportedHashingAlg.SHA512) { + return Uint8Array.from(sha512().update(data).digest()); + } + throw new ApolloError.InvalidHashingAlgorithm() +} + +export function hashSync(data: string, alg: string) { + if (!isSupported(alg)) { + throw new ApolloError.InvalidHashingAlgorithm() + } + if (alg === SupportedHashingAlg.SHA256) { + return Uint8Array.from(sha256().update(data).digest()); + } + if (alg === SupportedHashingAlg.SHA512) { + return Uint8Array.from(sha512().update(data).digest()); + } + throw new ApolloError.InvalidHashingAlgorithm() +} diff --git a/src/domain/utils/randomBytes.ts b/src/domain/utils/randomBytes.ts new file mode 100644 index 000000000..5bb8ed563 --- /dev/null +++ b/src/domain/utils/randomBytes.ts @@ -0,0 +1,8 @@ +import { crypto } from '@noble/ciphers/webcrypto/crypto'; + +export function randomBytes(bytes: Uint8Array): Uint8Array { + if (crypto && typeof crypto.getRandomValues === 'function') { + return crypto.getRandomValues(bytes); + } + throw new Error('crypto.getRandomValues must be defined'); +} \ No newline at end of file diff --git a/src/edge-agent/Agent.Credentials.ts b/src/edge-agent/Agent.Credentials.ts index c24c976f2..f6ecf64e8 100644 --- a/src/edge-agent/Agent.Credentials.ts +++ b/src/edge-agent/Agent.Credentials.ts @@ -3,7 +3,6 @@ import { base64 } from "multiformats/bases/base64"; import { AgentError, Apollo, - AttachmentBase64, AttachmentDescriptor, Castor, Credential, @@ -24,6 +23,7 @@ import { PresentationClaims, AttachmentFormats, PolluxError, + curveToAlg, } from "../domain"; import { AnonCredsCredential } from "../pollux/models/AnonCredsVerifiableCredential"; @@ -41,6 +41,7 @@ import { PrismKeyPathIndexTask } from "./Agent.PrismKeyPathIndexTask"; import { uuid } from "@stablelib/uuid"; import { ProtocolType } from "./protocols/ProtocolTypes"; import { validatePresentationClaims } from "../pollux/utils/claims"; +import { SDJWTCredential } from "../pollux/models/SDJWTVerifiableCredential"; export class AgentCredentials implements AgentCredentialsClass { /** @@ -61,7 +62,7 @@ export class AgentCredentials implements AgentCredentialsClass { protected seed: Seed, protected mercury: Mercury, protected agentDIDHigherFunctions: AgentDIDHigherFunctions - ) {} + ) { } private createPresentationDefinitionRequest( @@ -132,7 +133,7 @@ export class AgentCredentials implements AgentCredentialsClass { claims, new PresentationOptions({ jwt: { - jwtAlg: ['ES256K'] + jwtAlg: [curveToAlg(Curve.SECP256K1)] }, challenge: "Sign this text " + uuid(), domain: 'N/A' @@ -163,29 +164,33 @@ export class AgentCredentials implements AgentCredentialsClass { async processIssuedCredentialMessage( issueCredential: IssueCredential ): Promise { - const credentialType = this.pollux.extractCredentialFormatFromMessage( - issueCredential.makeMessage() - ); - const attachment = issueCredential.attachments.at(0)?.data; + const message = issueCredential.makeMessage() + const credentialType = message.credentialFormat; + const attachment = message.attachments.at(0); if (!attachment) { throw new Error("No attachment"); } - const credData = base64.baseDecode((attachment as AttachmentBase64).base64); + if (!issueCredential.thid) { + throw new Error("No thid"); + } const parseOpts: CredentialIssueOptions = { type: credentialType, }; + + const payload = typeof attachment.payload === 'string' ? attachment.payload : JSON.stringify(attachment.payload); + const credData = Uint8Array.from(Buffer.from(payload)); + if (credentialType === CredentialType.AnonCreds) { const linkSecret = await this.pluto.getLinkSecret(); parseOpts.linkSecret = linkSecret?.secret; const credentialMetadata = await this.pluto.getCredentialMetadata( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - issueCredential.thid! + issueCredential.thid ); if (!credentialMetadata || !credentialMetadata.isType(CredentialType.AnonCreds)) { @@ -195,13 +200,15 @@ export class AgentCredentials implements AgentCredentialsClass { parseOpts.credentialMetadata = credentialMetadata.toJSON(); } - const credential = await this.pollux.parseCredential(credData, parseOpts); + const credential: Credential = await this.pollux.parseCredential(credData, parseOpts); await this.pluto.storeCredential(credential); return credential; } + + /** * Asyncronously prepare a request credential message from a valid offerCredential for now supporting w3c verifiable credentials offers. * @@ -212,49 +219,69 @@ export class AgentCredentials implements AgentCredentialsClass { async prepareRequestCredentialWithIssuer( offer: OfferCredential ): Promise { - const message = offer.makeMessage(); - const credentialType = - this.pollux.extractCredentialFormatFromMessage(message); + const attachment = offer.attachments.at(0); + if (!attachment) { + throw new Error("Invalid attachment") + } + const credentialType = offer.makeMessage().credentialFormat; + const payload = attachment.payload let credRequestBuffer: string; + const requestCredentialBody = createRequestCredentialBody( + [], + offer.body.goalCode, + offer.body.comment + ); + + const from = offer.to; + const to = offer.from; + if (!from) { + throw new Error("Missing from"); + } + if (!to) { + throw new Error("Missing to"); + } + const thid = offer.thid; + const credentialFormat = + credentialType === CredentialType.AnonCreds ? AttachmentFormats.ANONCREDS_REQUEST : + credentialType === CredentialType.JWT ? CredentialType.JWT : + credentialType === CredentialType.SDJWT ? CredentialType.SDJWT : + CredentialType.Unknown; + if (credentialType === CredentialType.AnonCreds) { + const metaname = offer.thid; + if (!metaname) { + throw new Error("Missing offer.thid"); + } + const linkSecret = await this.pluto.getLinkSecret(); if (!linkSecret) { throw new Error("No linkSecret available."); } const [credentialRequest, credentialRequestMetadata] = - await this.pollux.processAnonCredsCredential(message, { linkSecret }); + await this.pollux.processCredentialOffer(payload, { linkSecret }); credRequestBuffer = JSON.stringify(credentialRequest); - // TODO can we fallback here? would need another identifier - const metaname = offer.thid; - - if (!metaname) { - throw new Error("Missing offer.thid"); - } - const metadata = new CredentialMetadata(CredentialType.AnonCreds, metaname, credentialRequestMetadata); await this.pluto.storeCredentialMetadata(metadata); } else if (credentialType === CredentialType.JWT) { - // ?? duplicated Agent.DIDHigherFunctions.createNewPrismDID const getIndexTask = new PrismKeyPathIndexTask(this.pluto); - const keyIndex = await getIndexTask.run(); const privateKey = await this.apollo.createPrivateKey({ [KeyProperties.curve]: Curve.SECP256K1, - [KeyProperties.index]: keyIndex, + [KeyProperties.index]: await getIndexTask.run(), [KeyProperties.type]: KeyTypes.EC, - [KeyProperties.seed]: this.seed.value, + [KeyProperties.seed]: Buffer.from(this.seed.value).toString("hex"), }); const did = await this.castor.createPrismDID(privateKey.publicKey()); await this.pluto.storeDID(did, privateKey); - credRequestBuffer = await this.pollux.processJWTCredential(message, { + credRequestBuffer = await this.pollux.processCredentialOffer(payload, { did: did, keyPair: { curve: Curve.SECP256K1, @@ -262,29 +289,46 @@ export class AgentCredentials implements AgentCredentialsClass { publicKey: privateKey.publicKey(), }, }); + } else if (credentialType === CredentialType.SDJWT) { + + const getIndexTask = new PrismKeyPathIndexTask(this.pluto); + const masterSk = await this.apollo.createPrivateKey({ + [KeyProperties.curve]: Curve.SECP256K1, + [KeyProperties.index]: await getIndexTask.run(), + [KeyProperties.type]: KeyTypes.EC, + [KeyProperties.seed]: Buffer.from(this.seed.value).toString("hex"), + }); + + const issSK = await this.apollo.createPrivateKey({ + [KeyProperties.curve]: Curve.ED25519, + [KeyProperties.index]: await getIndexTask.run(), + [KeyProperties.type]: KeyTypes.EC, + [KeyProperties.seed]: Buffer.from(this.seed.value).toString("hex"), + }); + + const did = await this.castor.createPrismDID( + masterSk.publicKey(), + [], + [ + issSK.publicKey() + ] + ); + + await this.pluto.storeDID(did, [masterSk, issSK]); + + credRequestBuffer = await this.pollux.processCredentialOffer(payload, { + did: did, + sdJWT: true, + keyPair: { + curve: Curve.SECP256K1, + privateKey: masterSk, + publicKey: masterSk.publicKey(), + }, + }); } else { throw new AgentError.InvalidCredentialFormats(); } - const requestCredentialBody = createRequestCredentialBody( - offer.body.formats, - offer.body.goalCode, - offer.body.comment - ); - - // TODO: remove assertions - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const from = offer.to!; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const to = offer.from!; - const thid = offer.thid; - - const credentialFormat = - credentialType === CredentialType.AnonCreds - ? AttachmentFormats.ANONCREDS_REQUEST - : credentialType === CredentialType.JWT - ? CredentialType.JWT - : CredentialType.Unknown; const attachments = [ new AttachmentDescriptor( @@ -310,7 +354,7 @@ export class AgentCredentials implements AgentCredentialsClass { attachments.forEach((attachment) => { requestCredential.body.formats.push({ attach_id: attachment.id, - format: credentialFormat, + format: `${credentialFormat}`, }); }); @@ -341,8 +385,8 @@ export class AgentCredentials implements AgentCredentialsClass { if (!presentation.thid) { throw new AgentError.UnsupportedAttachmentType("Cannot find any message with that threadID"); } - const presentationSubmission = Message.Attachment.extractJSON(attachment); - const presentationDefinitionRequest = await this.getPresentationDefinitionByThid(presentation.thid!); + const presentationSubmission = JSON.parse(attachment.payload) + const presentationDefinitionRequest = await this.getPresentationDefinitionByThid(presentation.thid); const options = { presentationDefinitionRequest }; @@ -421,7 +465,7 @@ export class AgentCredentials implements AgentCredentialsClass { * @returns {PresentationRequest} * @throws */ - private parseProofRequest(attachment: AttachmentDescriptor): PresentationRequest { + private parseProofRequest(attachment: AttachmentDescriptor) { const data = Message.Attachment.extractJSON(attachment); if (attachment.format === AttachmentFormats.ANONCREDS_PROOF_REQUEST) { return new PresentationRequest(AttachmentFormats.AnonCreds, data); @@ -429,6 +473,9 @@ export class AgentCredentials implements AgentCredentialsClass { if (attachment.format === CredentialType.JWT) { return new PresentationRequest(AttachmentFormats.JWT, data); } + if (attachment.format === CredentialType.SDJWT) { + return new PresentationRequest(AttachmentFormats.SDJWT, data); + } if (attachment.format === AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS) { return new PresentationRequest(AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS, data); } @@ -436,10 +483,10 @@ export class AgentCredentials implements AgentCredentialsClass { } private async handlePresentationDefinitionRequest( - request: PresentationRequest, + request: PresentationRequest, credential: Credential, ): Promise { - if (request.isType(AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS)) { + if (request.isType(AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS)) { if (credential instanceof JWTCredential) { const privateKeys = await this.pluto.getDIDPrivateKeysByDID(DID.fromString(credential.subject)); const privateKey = privateKeys.at(0); @@ -457,14 +504,16 @@ export class AgentCredentials implements AgentCredentialsClass { return JSON.stringify(presentationSubmission); } } - if (request.isType(AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS)) { + if (request.isType(AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS)) { if (credential instanceof AnonCredsCredential) { const storedLinkSecret = await this.pluto.getLinkSecret(); if (!storedLinkSecret) { throw new Error("Link secret not found."); } + + const req = request.toJSON() const presentationSubmission = await this.pollux.createPresentationSubmission( - request.toJSON(), + req as any, credential, storedLinkSecret ); @@ -476,16 +525,32 @@ export class AgentCredentials implements AgentCredentialsClass { } private async handlePresentationRequest( - request: PresentationRequest, + request: PresentationRequest, credential: Credential ): Promise { + if (credential instanceof SDJWTCredential && request.isType(AttachmentFormats.SDJWT)) { + if (!credential.isProvable()) { + throw new Error("Credential is not Provable"); + } + const subjectDID = DID.from(credential.subject); + const prismPrivateKeys = await this.pluto.getDIDPrivateKeysByDID(subjectDID); + const prismPrivateKey = prismPrivateKeys.find((key) => key.curve === Curve.ED25519) + if (prismPrivateKey === undefined) { + throw new AgentError.CannotFindDIDPrivateKey(); + } + const signedJWT = await this.pollux.createPresentationProof(request, credential, { + did: subjectDID, + privateKey: prismPrivateKey + }); + return signedJWT; + } if (credential instanceof AnonCredsCredential && request.isType(AttachmentFormats.AnonCreds)) { const linkSecret = await this.pluto.getLinkSecret(); if (!linkSecret) { throw new AgentError.CannotFindLinkSecret(); } const presentation = await this.pollux.createPresentationProof(request, credential, { linkSecret }); - return JSON.stringify(presentation); + return presentation; } if (credential instanceof JWTCredential && request.isType(AttachmentFormats.JWT)) { if (!credential.isProvable()) { @@ -493,7 +558,7 @@ export class AgentCredentials implements AgentCredentialsClass { } const subjectDID = DID.from(credential.subject); const prismPrivateKeys = await this.pluto.getDIDPrivateKeysByDID(subjectDID); - const prismPrivateKey = prismPrivateKeys.at(0); + const prismPrivateKey = prismPrivateKeys.find((key) => key.curve === Curve.SECP256K1) if (prismPrivateKey === undefined) { throw new AgentError.CannotFindDIDPrivateKey(); } @@ -503,6 +568,8 @@ export class AgentCredentials implements AgentCredentialsClass { }); return signedJWT; } + throw new AgentError.UnhandledPresentationRequest(); } + } diff --git a/src/edge-agent/Agent.DIDHigherFunctions.ts b/src/edge-agent/Agent.DIDHigherFunctions.ts index 7d2aeeea0..81c54e972 100644 --- a/src/edge-agent/Agent.DIDHigherFunctions.ts +++ b/src/edge-agent/Agent.DIDHigherFunctions.ts @@ -15,11 +15,10 @@ import { Pluto } from "../domain/buildingBlocks/Pluto"; import { AgentError } from "../domain/models/Errors"; import { AgentDIDHigherFunctions as AgentDIDHigherFunctionsClass, - ConnectionsManager, MediatorHandler, } from "./types"; import { PrismKeyPathIndexTask } from "./Agent.PrismKeyPathIndexTask"; -import { DerivationPath } from "../apollo/utils/derivation/DerivationPath"; +import { PrismDerivationPath, PRISM_WALLET_PURPOSE, PRISM_DID_METHOD, AUTHENTICATION_KEY, ISSUING_KEY, PrismDerivationPathSchema } from "../domain/models/derivation/schemas/PrismDerivation"; /** * An extension for the Edge agent that groups some DID related operations mainly used to expose the create did functionality @@ -46,7 +45,7 @@ export class AgentDIDHigherFunctions implements AgentDIDHigherFunctionsClass { protected pluto: Pluto, protected mediationHandler: MediatorHandler, protected seed: Seed - ) {} + ) { } /** @@ -141,21 +140,46 @@ export class AgentDIDHigherFunctions implements AgentDIDHigherFunctionsClass { services: Service[], keyPathIndex?: number ): Promise { - const index = keyPathIndex ?? await this.getNextKeyPathIndex(); - const derivationPath = DerivationPath.fromIndex(index); - const privateKey = this.apollo.createPrivateKey({ + const authenticationDerivation = new PrismDerivationPath([ + PRISM_WALLET_PURPOSE, + PRISM_DID_METHOD, + 0, + AUTHENTICATION_KEY, + await this.getNextKeyPathIndex(keyPathIndex) + ]); + const issuingDerivation = new PrismDerivationPath([ + PRISM_WALLET_PURPOSE, + PRISM_DID_METHOD, + 0, + ISSUING_KEY, + await this.getNextKeyPathIndex(keyPathIndex) + ]); + + const sk = this.apollo.createPrivateKey({ [KeyProperties.type]: KeyTypes.EC, [KeyProperties.curve]: Curve.SECP256K1, [KeyProperties.seed]: Buffer.from(this.seed.value).toString("hex"), - [KeyProperties.derivationPath]: derivationPath, + [KeyProperties.derivationPath]: authenticationDerivation.toString(), + [KeyProperties.derivationSchema]: PrismDerivationPathSchema + }); + const edSk = this.apollo.createPrivateKey({ + [KeyProperties.type]: KeyTypes.EC, + [KeyProperties.curve]: Curve.ED25519, + [KeyProperties.seed]: Buffer.from(this.seed.value).toString("hex"), + [KeyProperties.derivationPath]: issuingDerivation.toString(), + [KeyProperties.derivationSchema]: PrismDerivationPathSchema }); - const publicKey = privateKey.publicKey(); - const did = await this.castor.createPrismDID(publicKey, services); - // TODO tmp fix as index not set on key currently (PR open) - privateKey.keySpecification.set(KeyProperties.index, index.toString()); + const publicKey = sk.publicKey(); + const did = await this.castor.createPrismDID( + publicKey, + services, + [ + edSk.publicKey() + ] + ); + await this.pluto.storeDID(did, [sk, edSk], alias); - await this.pluto.storeDID(did, privateKey, alias); return did; } @@ -164,11 +188,14 @@ export class AgentDIDHigherFunctions implements AgentDIDHigherFunctionsClass { * * @returns {number} */ - private async getNextKeyPathIndex(): Promise { + private async getNextKeyPathIndex( + optionalKeyIndex?: number + ): Promise { const getIndexTask = new PrismKeyPathIndexTask(this.pluto); - const index = await getIndexTask.run(); - const next = index + 1; - + const index = await getIndexTask.run( + optionalKeyIndex + ); + const next = index; return next; } } diff --git a/src/edge-agent/Agent.PrismKeyPathIndexTask.ts b/src/edge-agent/Agent.PrismKeyPathIndexTask.ts index 121309ad1..e5346f42e 100644 --- a/src/edge-agent/Agent.PrismKeyPathIndexTask.ts +++ b/src/edge-agent/Agent.PrismKeyPathIndexTask.ts @@ -9,13 +9,18 @@ import type * as Domain from "../domain"; * @returns number */ export class PrismKeyPathIndexTask { - constructor(private readonly pluto: Domain.Pluto) {} + constructor(private readonly pluto: Domain.Pluto) { } - async run(): Promise { + async run( + index?: number + ): Promise { const prismDIDs = await this.pluto.getAllPrismDIDs(); + if (prismDIDs.length <= 0) { + return 0 + } const indexes = prismDIDs.map(x => x.privateKey.index ?? 0); - const keyPathIndex = Math.max(0, ...indexes); - - return keyPathIndex; + const maxKey = Math.max(0, ...indexes); + const keyPathIndex = maxKey; + return typeof index !== 'undefined' && index > keyPathIndex ? index : keyPathIndex + 1 } } diff --git a/src/edge-agent/Agent.ts b/src/edge-agent/Agent.ts index 407346af3..0b199c155 100644 --- a/src/edge-agent/Agent.ts +++ b/src/edge-agent/Agent.ts @@ -93,7 +93,7 @@ export default class Agent options?: AgentOptions ) { - this.pollux = new Pollux(castor); + this.pollux = new Pollux(apollo, castor); this.agentDIDHigherFunctions = new AgentDIDHigherFunctions( apollo, castor, @@ -170,7 +170,7 @@ export default class Agent const store = new PublicMediatorStore(pluto); const handler = new BasicMediatorHandler(mediatorDID, mercury, store); - const pollux = new Pollux(castor); + const pollux = new Pollux(apollo, castor); const seed = params.seed ?? apollo.createRandomSeed().seed; const agentCredentials = new AgentCredentials( diff --git a/src/edge-agent/connectionsManager/ConnectionsManager.ts b/src/edge-agent/connectionsManager/ConnectionsManager.ts index ba8914a3d..8747ce5b2 100644 --- a/src/edge-agent/connectionsManager/ConnectionsManager.ts +++ b/src/edge-agent/connectionsManager/ConnectionsManager.ts @@ -148,7 +148,7 @@ export class ConnectionsManager implements ConnectionsManagerClass { const revokeMessages = messages.filter(x => x.piuri === ProtocolType.PrismRevocation); const allMessages = await this.pluto.getAllMessages(); - for (let message of revokeMessages) { + for (const message of revokeMessages) { const revokeMessage = RevocationNotification.fromMessage(message); const threadId = revokeMessage.body.issueCredentialProtocolThreadId; @@ -158,7 +158,7 @@ export class ConnectionsManager implements ConnectionsManagerClass { ); if (matchingMessages.length > 0) { - for (let message of matchingMessages) { + for (const message of matchingMessages) { const issueMessage = IssueCredential.fromMessage(message); const credential = await this.agentCredentials.processIssuedCredentialMessage( issueMessage diff --git a/src/edge-agent/mediator/BasicMediatorHandler.ts b/src/edge-agent/mediator/BasicMediatorHandler.ts index ec20ad6f9..cf13465e7 100644 --- a/src/edge-agent/mediator/BasicMediatorHandler.ts +++ b/src/edge-agent/mediator/BasicMediatorHandler.ts @@ -10,7 +10,7 @@ import { MediationRequest } from "../protocols/mediation/MediationRequest"; import { PickupReceived } from "../protocols/pickup/PickupReceived"; import { PickupRequest } from "../protocols/pickup/PickupRequest"; import { PickupRunner } from "../protocols/pickup/PickupRunner"; -import { EventPickupCallback, MediatorHandler, MediatorStore } from "../types"; +import { MediatorHandler, MediatorStore } from "../types"; import { ProtocolType } from '../protocols/ProtocolTypes'; /** diff --git a/src/edge-agent/protocols/proofPresentation/RequestPresentation.ts b/src/edge-agent/protocols/proofPresentation/RequestPresentation.ts index 160d2ab93..f75b9e289 100644 --- a/src/edge-agent/protocols/proofPresentation/RequestPresentation.ts +++ b/src/edge-agent/protocols/proofPresentation/RequestPresentation.ts @@ -28,7 +28,7 @@ export class RequestPresentation { } get decodedAttachments() { - return this.attachments.map(Message.Attachment.extractJSON) + return this.attachments.map((attachment) => attachment.payload) } makeMessage(): Message { diff --git a/src/edge-agent/protocols/revocation/RevocationNotfiication.ts b/src/edge-agent/protocols/revocation/RevocationNotfiication.ts index 5fd8c752f..9ba77f939 100644 --- a/src/edge-agent/protocols/revocation/RevocationNotfiication.ts +++ b/src/edge-agent/protocols/revocation/RevocationNotfiication.ts @@ -29,8 +29,8 @@ export class RevocationNotification { } return new RevocationNotification( JSON.parse(message.body), - message.from!, - message.to! + message.from, + message.to ) } } \ No newline at end of file diff --git a/src/edge-agent/protocols/types.ts b/src/edge-agent/protocols/types.ts index 3fb60df95..3947c5abf 100644 --- a/src/edge-agent/protocols/types.ts +++ b/src/edge-agent/protocols/types.ts @@ -1,4 +1,3 @@ -import { PredicateType } from "../../domain"; import { CredentialFormat } from "./issueCredential/CredentialFormat"; import { CredentialPreview } from "./issueCredential/CredentialPreview"; diff --git a/src/edge-agent/types/index.ts b/src/edge-agent/types/index.ts index 42d61f193..5e9fb1081 100644 --- a/src/edge-agent/types/index.ts +++ b/src/edge-agent/types/index.ts @@ -6,12 +6,10 @@ import { Service as DIDDocumentService, Signature, Credential, - Pollux, CredentialType, - PrivateKey, PresentationClaims, - PredicateType, - AttributeType, + KeyPair, + PublicKey, } from "../../domain"; import { DIDPair } from "../../domain/models/DIDPair"; import { Castor } from "../../domain/buildingBlocks/Castor"; @@ -95,7 +93,8 @@ export interface AgentDIDHigherFunctions { createNewPrismDID( alias: string, services: DIDDocumentService[], - keyPathIndex?: number + keyPathIndex?: number, + issuingKeys?: (PublicKey | KeyPair)[] ): Promise; } diff --git a/src/mercury/didcomm/Wrapper.ts b/src/mercury/didcomm/Wrapper.ts index f0596fe51..3a21cf4a4 100644 --- a/src/mercury/didcomm/Wrapper.ts +++ b/src/mercury/didcomm/Wrapper.ts @@ -174,16 +174,16 @@ export class DIDCommWrapper implements DIDCommProtocol { if (typeof attachment.id !== "string" || attachment.id.length === 0) throw new MercuryError.MessageAttachmentWithoutIDError(); - return { - id: attachment.id, - data: this.parseAttachmentDataToDomain(attachment.data), - byteCount: attachment.byte_count, - description: attachment.description, - filename: attachment.filename?.split("/"), - format: attachment.format, - lastModTime: attachment.lastmod_time?.toString(), - mediaType: attachment.media_type, - }; + return new Domain.AttachmentDescriptor( + this.parseAttachmentDataToDomain(attachment.data), + attachment.id, + attachment.media_type, + attachment.filename?.split("/"), + attachment.format, + attachment.lastmod_time?.toString(), + attachment.byte_count, + attachment.description, + ); } private parseAttachmentDataToDomain( diff --git a/src/pluto/Pluto.ts b/src/pluto/Pluto.ts index 7351dab13..2594407cc 100644 --- a/src/pluto/Pluto.ts +++ b/src/pluto/Pluto.ts @@ -198,7 +198,6 @@ export class Pluto implements Domain.Pluto { async storeDID(did: Domain.DID, keys?: Arrayable, alias?: string): Promise { await this.Repositories.DIDs.save(did, alias); - await Promise.all( asArray(keys).map(async key => { await this.Repositories.Keys.save(key); @@ -210,6 +209,8 @@ export class Pluto implements Domain.Pluto { }); }) ); + + } /** Prism DIDs **/ @@ -227,29 +228,29 @@ export class Pluto implements Domain.Pluto { async getAllPrismDIDs(): Promise { const dids = await this.Repositories.DIDs.find({ method: "prism" }); - const results = await Promise.all(dids.map(x => this.getPrismDID(x.uuid))); - const filtered = results.filter((x): x is Domain.PrismDID => x != null); - - return filtered; - } - - private async getPrismDID(didId: string): Promise { - try { - const links = await this.Repositories.DIDKeyLinks.getModels({ selector: { didId } }); - const link = this.onlyOne(links); - const did = await this.Repositories.DIDs.byUUID(link.didId); - const key = await this.Repositories.Keys.byUUID(link.keyId); - - if (!did || !key) { - throw new Error("PrismDID not found"); + const prismDIDS: Domain.PrismDID[] = []; + for (const did of dids) { + const dbDids = await this.getPrismDIDS(did.uuid); + for (const prismDID of dbDids) { + prismDIDS.push(prismDID) } - - const prismDID = new Domain.PrismDID(did, key, link.alias); - return prismDID; - } - catch (e) { - return null; } + return prismDIDS; + } + + private async getPrismDIDS(didId: string): Promise { + const links = await this.Repositories.DIDKeyLinks.getModels({ selector: { didId } }); + return Promise.all( + links.map(async (link) => { + const did = await this.Repositories.DIDs.byUUID(link.didId); + const key = await this.Repositories.Keys.byUUID(link.keyId); + if (!did || !key) { + throw new Error("PrismDID not found"); + } + const prismDID = new Domain.PrismDID(did, key, link.alias); + return prismDID; + }) + ) } diff --git a/src/pluto/models/Key.ts b/src/pluto/models/Key.ts index a749f4201..dbd873133 100644 --- a/src/pluto/models/Key.ts +++ b/src/pluto/models/Key.ts @@ -23,10 +23,11 @@ export interface Key extends Model { } export const KeySchema = schemaFactory(schema => { - schema.setRequired("recoveryId", "rawHex"); schema.addProperty("string", "recoveryId"); schema.addProperty("string", "rawHex"); schema.addProperty("string", "alias"); schema.addProperty("number", "index"); schema.setEncrypted("rawHex"); + schema.setRequired("recoveryId", "rawHex"); + schema.setVersion(0); }); diff --git a/src/pluto/repositories/CredentialRepository.ts b/src/pluto/repositories/CredentialRepository.ts index 023edf9e7..cfd3f4a44 100644 --- a/src/pluto/repositories/CredentialRepository.ts +++ b/src/pluto/repositories/CredentialRepository.ts @@ -4,6 +4,7 @@ import type { Pluto } from "../Pluto"; import { MapperRepository } from "./builders/MapperRepository"; import { AnonCredsCredential, AnonCredsRecoveryId } from "../../pollux/models/AnonCredsVerifiableCredential"; import { JWTCredential, JWTVerifiableCredentialRecoveryId } from "../../pollux/models/JWTVerifiableCredential"; +import { SDJWTCredential, SDJWTVerifiableCredentialRecoveryId } from "../../pollux/models/SDJWTVerifiableCredential"; export class CredentialRepository extends MapperRepository { constructor(store: Pluto.Store) { @@ -12,6 +13,17 @@ export class CredentialRepository extends MapperRepository disclosure._encoded); + const credential = SDJWTCredential.fromJWS( + jws, + jwtModel.revoked ?? false, + disclosures + ); + return this.withId(credential, model.uuid); + } case JWTVerifiableCredentialRecoveryId: { const jwtModel = JSON.parse(model.dataJson); const credential = JWTCredential.fromJWS( diff --git a/src/pluto/repositories/KeyRepository.ts b/src/pluto/repositories/KeyRepository.ts index b16752226..9bfa3b1da 100644 --- a/src/pluto/repositories/KeyRepository.ts +++ b/src/pluto/repositories/KeyRepository.ts @@ -22,7 +22,7 @@ export class KeyRepository extends MapperRepository> { - if (!(privateKey instanceof Secp256k1PrivateKey)) { - throw new CastorError.InvalidKeyError("Required Secp256k1PrivateKey key") - } if (!this.isPresentationDefinitionRequestType(presentationDefinitionRequest, CredentialType.JWT)) { throw new Error("PresentationDefinition didn't match credential type") } @@ -169,7 +178,6 @@ export default class Pollux implements IPollux { throw new PolluxError.InvalidCredentialError("Cannot create proofs with this type of credential.") } - const payload: JWTPresentationPayload = { iss: subject, aud: domain, @@ -178,7 +186,7 @@ export default class Pollux implements IPollux { vp: credential.presentation(), } - const jws = await this.jwt.sign({ + const jws = await this.JWT.sign({ issuerDID: DID.fromString(subject), privateKey, payload @@ -218,8 +226,6 @@ export default class Pollux implements IPollux { ); } - - async createPresentationSubmission< Type extends CredentialType = CredentialType.JWT >( @@ -433,7 +439,7 @@ export default class Pollux implements IPollux { const presentationSubmissionMapper = new DescriptorPath(presentationSubmission); const descriptorMaps = presentationSubmission.presentation_submission.descriptor_map; - for (let descriptorItem of descriptorMaps) { + for (const descriptorItem of descriptorMaps) { if (descriptorItem.format !== DescriptorItemFormat.JWT_VP) { throw new InvalidVerifyFormatError( `Invalid Submission, ${descriptorItem.path} expected to have format ${DescriptorItemFormat.JWT_VP}` @@ -465,7 +471,7 @@ export default class Pollux implements IPollux { if (!isPrismJWTCredentialType) { throw new InvalidVerifyCredentialError(jws, "Invalid JWT Credential only prism/jwt is supported for jwt"); } - const credentialValid = await this.jwt.verify({ + const credentialValid = await this.JWT.verify({ issuerDID: issuer, jws }); @@ -490,7 +496,7 @@ export default class Pollux implements IPollux { throw new InvalidVerifyCredentialError(vc, "Invalid Verifiable Presentation payload, the credential has been issued to another holder"); } - const verifiableCredentialValid = await this.jwt.verify({ + const verifiableCredentialValid = await this.JWT.verify({ holderDID: verifiableCredential.subject ? DID.fromString(verifiableCredential.subject) : undefined, issuerDID: verifiableCredential.issuer, jws: verifiableCredential.id @@ -504,7 +510,7 @@ export default class Pollux implements IPollux { const constraints = inputDescriptor.constraints; const fields = constraints.fields; if (constraints.limit_disclosure === InputLimitDisclosure.REQUIRED) { - for (let field of fields) { + for (const field of fields) { const paths = [ ...field.path ]; @@ -592,8 +598,8 @@ export default class Pollux implements IPollux { ) } - verifyPresentationSubmission(presentationSubmission: PresentationSubmission, options: IPollux.verifyPresentationSubmission.options.Anoncreds): Promise; - verifyPresentationSubmission(presentationSubmission: PresentationSubmission, options: IPollux.verifyPresentationSubmission.options.JWT): Promise; + verifyPresentationSubmission(presentationSubmission: PresentationSubmission, options: IPollux.verifyPresentationSubmission.options.Anoncreds): Promise; + verifyPresentationSubmission(presentationSubmission: PresentationSubmission, options: IPollux.verifyPresentationSubmission.options.JWT): Promise; async verifyPresentationSubmission( presentationSubmission: any, options?: IPollux.verifyPresentationSubmission.options @@ -631,71 +637,127 @@ export default class Pollux implements IPollux { this._anoncreds = await AnoncredsLoader.getInstance(); } - // TODO: should anoncreds hidden through abstraction - get anoncreds() { - if (this._anoncreds === undefined) { - throw new Error("Pollux - Anoncreds not loaded"); - } - - return this._anoncreds; - } - - // TODO: does this function belong in Pollux, can we move to Message? - public extractCredentialFormatFromMessage(message: Message) { - const [attachment] = message.attachments; - if (!attachment) { - throw new Error("Required Attachment"); + private isOfferPayload(offer: any, type: Type): offer is CredentialOfferPayloads[CredentialType.JWT] { + if (type === CredentialType.JWT) { + if (!offer || !offer.options) { + return false + } + const options = offer.options + if (!options.challenge || typeof options.challenge !== "string") { + return false + } + if (!options.domain || typeof options.domain !== "string") { + return false + } + return true } - - if ( - attachment.format === AttachmentFormats.ANONCREDS_PROOF_REQUEST || - attachment.format === AttachmentFormats.ANONCREDS_OFFER || - attachment.format === AttachmentFormats.ANONCREDS_ISSUE || - attachment.format === AttachmentFormats.ANONCREDS_REQUEST - ) { - return CredentialType.AnonCreds; + if (type === CredentialType.SDJWT) { + if (!offer.options) { + return false + } + const options = offer.options + if (!options.challenge || typeof options.challenge !== "string") { + return false + } + if (!options.domain || typeof options.domain !== "string") { + return false + } + return true } - - if (attachment.format === CredentialType.JWT) { - return CredentialType.JWT; + if (type === CredentialType.AnonCreds) { + if (!offer.cred_def_id || typeof offer.cred_def_id !== 'string') { + return false; + } + if (!offer.nonce || typeof offer.nonce !== 'string') { + return false; + } + if (!offer.schema_id || typeof offer.schema_id !== 'string') { + return false; + } + if (!offer.key_correctness_proof || + !offer.key_correctness_proof.c || typeof offer.key_correctness_proof.c !== 'string' || + !offer.key_correctness_proof.xz_cap || typeof offer.key_correctness_proof.xz_cap !== 'string' || + !offer.key_correctness_proof.xr_cap || !Array.isArray(offer.key_correctness_proof.xr_cap)) { + return false + } + return true } - - return CredentialType.Unknown; + return true } - async processJWTCredential( - offer: Message, - options: CredentialRequestOptions = {} - ) { - const { did, keyPair } = options; - if (!did) { - throw new Error("Required did "); - } + async processCredentialOffer< + Types extends CredentialOfferTypes + >( + offer: CredentialOfferPayloads[Types], + options: CredentialRequestOptions + ): Promise { + if (this.isOfferPayload(offer, CredentialType.SDJWT) && options.sdJWT) { + const { did, keyPair } = options; + if (!did) { + throw new Error("Required did "); + } + if (!keyPair) { + throw new Error("Required keyPair "); + } + const challenge = offer.options.challenge; + const domain = offer.options.domain; - if (!keyPair) { - throw new Error("Required keyPair "); + const signedJWT = await this.JWT.sign({ + issuerDID: did, + privateKey: keyPair.privateKey, + payload: { + aud: domain, + nonce: challenge, + vp: { + "@context": ["https://www.w3.org/2018/presentations/v1"], + type: ["VerifiablePresentation"], + }, + } + }); + return signedJWT as ProcessedCredentialOfferPayloads[Types]; + } + if (this.isOfferPayload(offer, CredentialType.JWT)) { + const { did, keyPair } = options; + if (!did) { + throw new Error("Required did "); + } + if (!keyPair) { + throw new Error("Required keyPair "); + } + const challenge = offer.options.challenge; + const domain = offer.options.domain; + const signedJWT = await this.JWT.sign({ + issuerDID: did, + privateKey: keyPair.privateKey, + payload: { + aud: domain, + nonce: challenge, + vp: { + "@context": ["https://www.w3.org/2018/presentations/v1"], + type: ["VerifiablePresentation"], + }, + } + }); + return signedJWT as ProcessedCredentialOfferPayloads[Types]; } - const domainChallenge = this.extractDomainChallenge(offer.attachments); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const challenge = domainChallenge.challenge!; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const domain = domainChallenge.domain!; - - const signedJWT = await this.jwt.sign({ - issuerDID: did, - privateKey: keyPair.privateKey, - payload: { - aud: domain, - nonce: challenge, - vp: { - "@context": ["https://www.w3.org/2018/presentations/v1"], - type: ["VerifiablePresentation"], - }, - } - }); + if (this.isOfferPayload(offer, CredentialType.AnonCreds)) { + const linkSecret = options.linkSecret; + if (!linkSecret) { + throw new Error("Link Secret is not available."); + } + const { cred_def_id } = offer; + const credentialDefinition = + await this.fetchCredentialDefinition(cred_def_id); + return this.anoncreds.createCredentialRequest( + offer, + credentialDefinition, + linkSecret.secret, + linkSecret.name + ) as ProcessedCredentialOfferPayloads[Types]; + } - return signedJWT; + throw new Error("Not implemented") } /** @@ -736,87 +798,6 @@ export default class Pollux implements IPollux { return response.body; } - private extractAttachment(body: any, attachments: AttachmentDescriptor[]) { - const [attachment] = attachments; - if (!attachment) { - throw new Error("Attachment not found"); - } - - return JSON.parse( - Buffer.from( - base64.baseDecode((attachment.data as AttachmentBase64).base64) - ).toString() - ); - } - - private isAnonCredsBody(body: any): body is Anoncreds.CredentialOffer { - const { - cred_def_id, - schema_id, - key_correctness_proof, - nonce, - method_name, - } = body; - - if (!cred_def_id || typeof cred_def_id !== "string") return false; - if (!schema_id || typeof schema_id !== "string") return false; - if (!nonce || typeof nonce !== "string") return false; - if (!key_correctness_proof) return false; - - const { c, xr_cap, xz_cap } = key_correctness_proof; - - if (!c || !xr_cap || !xz_cap) return false; - if ( - typeof c !== "string" || - !Array.isArray(xr_cap) || - typeof xz_cap !== "string" - ) - return false; - - if ( - xr_cap.length <= 0 || - xr_cap.find( - ([first, second]) => - typeof first !== "string" || typeof second !== "string" - ) - ) - return false; - - if (method_name && typeof method_name !== "string") return false; - - return true; - } - - async processAnonCredsCredential( - offer: Message, - options: CredentialRequestOptions - ) { - const linkSecret = options.linkSecret; - if (!linkSecret) { - throw new Error("Link Secret is not available."); - } - - const body = JSON.parse(offer.body); - - const credentialOfferBody = this.extractAttachment(body, offer.attachments); - - const isAnonCredsBody = this.isAnonCredsBody(credentialOfferBody); - if (!isAnonCredsBody) { - throw new Error("Invalid AnonCreds offer body"); - } - - const { cred_def_id } = credentialOfferBody; - const credentialDefinition = - await this.fetchCredentialDefinition(cred_def_id); - - return this.anoncreds.createCredentialRequest( - credentialOfferBody, - credentialDefinition, - linkSecret.secret, - linkSecret.name - ); - } - async parseCredential( credentialBuffer: Uint8Array, options?: { @@ -833,6 +814,12 @@ export default class Pollux implements IPollux { return JWTCredential.fromJWS(credentialString); } + if (credentialType === CredentialType.SDJWT) { + const sdJWTPayload = JSON.parse(credentialString); + const jwsString = `${sdJWTPayload.protected}.${sdJWTPayload.payload}.${sdJWTPayload.signature},${sdJWTPayload.disclosures}`; + return SDJWTCredential.fromJWS(jwsString, false, sdJWTPayload.disclosures); + } + if (credentialType === CredentialType.AnonCreds) { if (options?.linkSecret === undefined) { throw new Error("LinkSecret is required"); @@ -865,23 +852,8 @@ export default class Pollux implements IPollux { throw new Error("Not implemented"); } - private extractDomainChallenge(attachments: AttachmentDescriptor[]) { - return attachments.reduce( - (_, attachment: any) => ({ - challenge: attachment?.data?.data?.options?.challenge, - domain: attachment?.data?.data?.options?.domain, - }), - { challenge: undefined, domain: undefined } as { - challenge?: string; - domain?: string; - } - ); - } - - createPresentationProof(presentationRequest: PresentationRequest, credential: AnonCredsCredential, options: IPollux.createPresentationProof.options.Anoncreds): Promise; - createPresentationProof(presentationRequest: PresentationRequest, credential: JWTCredential, options: IPollux.createPresentationProof.options.JWT): Promise; async createPresentationProof( - presentationRequest: PresentationRequest, + presentationRequest: PresentationRequest, credential: Credential, options: IPollux.createPresentationProof.options ) { @@ -903,7 +875,7 @@ export default class Pollux implements IPollux { options.linkSecret.secret ); - return result; + return JSON.stringify(result); } if ( @@ -912,14 +884,17 @@ export default class Pollux implements IPollux { && "did" in options && "privateKey" in options ) { - const presReqJson = presentationRequest.toJSON(); - const signedJWT = await this.jwt.sign({ + //For some reason I can't find out rollup is unable to understand the type is correct + const jwtPresentationRequest = presentationRequest + const presReqJson: JWTJson = jwtPresentationRequest.toJSON() as any; + const presReqOptions = presReqJson.options; + const signedJWT = await this.JWT.sign({ issuerDID: options.did, privateKey: options.privateKey, payload: { iss: options.did.toString(), - aud: presReqJson.options.domain, - nonce: presReqJson.options.challenge, + aud: presReqOptions.domain, + nonce: presReqOptions.challenge, vp: credential.presentation() } }); @@ -927,6 +902,42 @@ export default class Pollux implements IPollux { return signedJWT; } + if ( + credential instanceof SDJWTCredential + && presentationRequest.isType(AttachmentFormats.SDJWT) + && "did" in options + && "privateKey" in options + ) { + const jwtPresentationRequest = presentationRequest + const presReqJson: SDJWTJson = jwtPresentationRequest.toJSON() as any + const presentationJWS = await this.SDJWT.createPresentationFor( + { + jws: credential.id, + frame: Object.keys(presReqJson.claims).reduce((all, claimName) => ({ + ...all, + [claimName]: presReqJson.claims[claimName] + }), {}), + privateKey: options.privateKey + } + ) + const [header, payload, signature] = presentationJWS.replace("~", "").split("."); + + if (typeof header !== "undefined" && + typeof payload !== "undefined" && + typeof signature !== "undefined") { + + const [onlySignature, ...disclosures] = signature.split(","); + + return JSON.stringify({ + protected: header, + payload: payload, + signature: onlySignature, + disclosures: disclosures + }); + } + + } + throw new PolluxError.InvalidPresentationProofArgs(); } } diff --git a/src/pollux/models/JWTVerifiableCredential.ts b/src/pollux/models/JWTVerifiableCredential.ts index 4a0b3f5a1..b1ace4491 100644 --- a/src/pollux/models/JWTVerifiableCredential.ts +++ b/src/pollux/models/JWTVerifiableCredential.ts @@ -41,9 +41,11 @@ export class JWTCredential let originalString: string | undefined; if (typeof payload === 'string') { - const jsonString = decodeJWS(payload) + const jwtObject = decodeJWS(payload) originalString = payload; - payload = JSON.parse(jsonString); + payload = jwtObject.payload + } else { + originalString = payload.jti; } if (this.isCredentialPayload(payload)) { diff --git a/src/pollux/models/PresentationRequest.ts b/src/pollux/models/PresentationRequest.ts index 8b9253571..90d07fa59 100644 --- a/src/pollux/models/PresentationRequest.ts +++ b/src/pollux/models/PresentationRequest.ts @@ -1,7 +1,7 @@ import { AttachmentFormats, CredentialType, PresentationDefinitionRequest } from "../../domain"; import type * as Anoncreds from "anoncreds-browser"; -interface JWTJson { +export interface JWTJson { options: { challenge: string; domain: string; @@ -11,35 +11,49 @@ interface JWTJson { }; } +export type SDJWTJson = { + claims: { + [name: string]: any + }, + options: { + challenge: string, + domain: string + } +} + /** * Wrapper for Presentation Requests for different Credential Types * * @class PresentationRequest * @typedef {PresentationRequest} */ -export class PresentationRequest { +export class PresentationRequest< + Type extends AttachmentFormats = AttachmentFormats.JWT, + Body = Type extends AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS ? PresentationDefinitionRequest : + Type extends AttachmentFormats.AnonCreds ? Anoncreds.PresentationRequestType : + Type extends AttachmentFormats.JWT ? JWTJson : + Type extends AttachmentFormats.SDJWT ? SDJWTJson : unknown +> { /** * @constructor * @param type - CredentialType the json is related to * @param json - the raw value */ - constructor(type: AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS, json: PresentationDefinitionRequest); - constructor(type: AttachmentFormats.AnonCreds, json: Anoncreds.PresentationRequestType); - constructor(type: AttachmentFormats.JWT, json: JWTJson); constructor( - private readonly type: AttachmentFormats, - private readonly json: T + private readonly type: Type, + private readonly json: Body ) { } /** * Type guard that the instance is for the given CredentialType * - * @param type + * @param target * @returns {boolean} */ - isType(type: AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS): this is PresentationRequest>; - isType(type: AttachmentFormats.AnonCreds): this is PresentationRequest; - isType(type: AttachmentFormats.JWT): this is PresentationRequest; + isType(type: AttachmentFormats.PRESENTATION_EXCHANGE_DEFINITIONS): this is PresentationRequest>; + isType(type: AttachmentFormats.AnonCreds): this is PresentationRequest; + isType(type: AttachmentFormats.JWT): this is PresentationRequest; + isType(type: AttachmentFormats.SDJWT): this is PresentationRequest; isType(target: AttachmentFormats) { return this.type === target; } @@ -49,7 +63,7 @@ export class PresentationRequest { * * @returns JSON */ - toJSON(): T { + toJSON() { return this.json; } -} +} \ No newline at end of file diff --git a/src/pollux/models/SDJWTVerifiableCredential.ts b/src/pollux/models/SDJWTVerifiableCredential.ts new file mode 100644 index 000000000..81be1fe6d --- /dev/null +++ b/src/pollux/models/SDJWTVerifiableCredential.ts @@ -0,0 +1,204 @@ +import { uuid } from "@stablelib/uuid"; +import { SDJwt, Jwt } from "@sd-jwt/core"; +import { Disclosure } from '@sd-jwt/utils'; +import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode'; + +import { + Pluto, + StorableCredential, + Credential, + CredentialType, + SDJWTVerifiableCredentialProperties as SDJWT_VP_PROPS, + ProvableCredential, + W3CVerifiablePresentation, + W3CVerifiableCredentialContext, + W3CVerifiableCredentialType, + PolluxError +} from "../../domain"; +import { defaultHashConfig } from "../utils/jwt/config"; + +export const SDJWTVerifiableCredentialRecoveryId = "sd+jwt+credential"; + +export class SDJWTCredential extends Credential implements ProvableCredential, StorableCredential, Pluto.Storable { + credentialType: CredentialType = CredentialType.SDJWT + public recoveryId = SDJWTVerifiableCredentialRecoveryId; + + get id() { + return this.properties.get(SDJWT_VP_PROPS.jti); + } + + get issuer() { + return this.claims.at(0)?.iss ?? this.properties.get(SDJWT_VP_PROPS.iss); + } + + get subject() { + return this.claims.at(0)?.sub ?? this.properties.get(SDJWT_VP_PROPS.sub); + } + + public uuid: string = uuid(); + public properties: Map = new Map(); + public claims: Record[] = []; + public core: SDJwt; + + constructor( + object: SDJwt, + claims: Record[], + revoked?: boolean + ) { + super() + + const { jwt } = object; + + this.claims = claims; + this.core = object; + + if (!jwt || !jwt.payload) { + throw new PolluxError.InvalidJWTString("Cannot decode SDJWT properly") + } + + const payload = jwt.payload; + if (typeof payload[SDJWT_VP_PROPS.revoked] === "boolean") { + this.properties.set( + SDJWT_VP_PROPS.revoked, + payload[SDJWT_VP_PROPS.revoked] + ); + } else if (typeof revoked === 'boolean') { + this.properties.set( + SDJWT_VP_PROPS.revoked, + revoked + ); + } else { + this.properties.set( + SDJWT_VP_PROPS.revoked, + false + ); + } + + if (payload.iss) { + this.properties.set( + SDJWT_VP_PROPS.iss, + payload.iss + ); + } + + if (payload.sub) { + this.properties.set( + SDJWT_VP_PROPS.sub, + payload.sub + ); + } + + this.properties.set( + SDJWT_VP_PROPS.jti, + jwt.encodeJwt() + ); + + if (payload.nbf) { + this.properties.set( + SDJWT_VP_PROPS.nbf, + payload.nbf + ); + } + + if (payload.exp) { + this.properties.set( + SDJWT_VP_PROPS.exp, + payload.exp + ); + } + + if (payload.aud) { + this.properties.set( + SDJWT_VP_PROPS.aud, + payload.aud + ); + } + + if (payload.vct) { + this.properties.set( + SDJWT_VP_PROPS.vct, + payload.vct + ); + } + + if (payload._sd) { + this.properties.set( + SDJWT_VP_PROPS._sd, + payload._sd + ); + } + + if (payload._sd_alg) { + this.properties.set( + SDJWT_VP_PROPS._sd_alg, + payload._sd_alg + ); + } + + this.properties.set( + SDJWT_VP_PROPS.disclosures, + object.disclosures + ); + } + + verifiableCredential(): unknown { + throw new Error("Method not implemented."); + } + + presentation(): W3CVerifiablePresentation { + return { + "@context": [ + W3CVerifiableCredentialContext.credential + ], + type: [ + W3CVerifiableCredentialType.presentation + ], + verifiableCredential: [ + this.id + ], + }; + } + + get revoked(): boolean | undefined { + return this.properties.get(SDJWT_VP_PROPS.revoked); + } + + toStorable(): { id: string; recoveryId: string; credentialData: string; issuer?: string | undefined; subject?: string | undefined; credentialCreated?: string | undefined; credentialUpdated?: string | undefined; credentialSchema?: string | undefined; validUntil?: string | undefined; revoked?: boolean | undefined; availableClaims?: string[] | undefined; } { + const id = this.id; + const data = { id, ...Object.fromEntries(this.properties) }; + const claims = this.claims.map((claim) => typeof claim !== 'string' ? JSON.stringify(claim) : claim) + return { + id, + recoveryId: this.recoveryId, + credentialData: JSON.stringify(data), + issuer: this.issuer, + subject: this.properties.get(SDJWT_VP_PROPS.sub), + validUntil: this.getProperty(SDJWT_VP_PROPS.exp), + availableClaims: claims, + revoked: this.revoked + }; + } + + static fromJWS = Record>( + jws: string, + revoked?: boolean, + disclosures: string[] = [] + ): SDJWTCredential { + const { hasherSync, hasherAlg } = defaultHashConfig; + const jwt = new Jwt(Jwt.decodeJWT(jws)) + const decoded = decodeSdJwtSync(jws, hasherSync); + const computed = disclosures.map((disclosure) => Disclosure.fromEncodeSync(disclosure, { + hasher: hasherSync, + alg: hasherAlg + })) + const claims = getClaimsSync(decoded.jwt.payload, computed, hasherSync) + const loaded = new SDJwt( + { + jwt: jwt, + disclosures: computed + } + ) + return new SDJWTCredential(loaded, [claims], revoked); + } + +} \ No newline at end of file diff --git a/src/pollux/utils/DescriptorPath.ts b/src/pollux/utils/DescriptorPath.ts index 954456bb4..e7987f006 100644 --- a/src/pollux/utils/DescriptorPath.ts +++ b/src/pollux/utils/DescriptorPath.ts @@ -1,8 +1,4 @@ - - - - export class DescriptorPath { constructor(private obj: any) { } diff --git a/src/pollux/utils/JWT.ts b/src/pollux/utils/JWT.ts index 7cf3ae7c5..8a3a08a8c 100644 --- a/src/pollux/utils/JWT.ts +++ b/src/pollux/utils/JWT.ts @@ -1,151 +1,67 @@ import * as didJWT from "did-jwt"; -import * as didResolver from "did-resolver"; - -import { Castor } from "../../domain/buildingBlocks/Castor"; -import { - AlsoKnownAs, - Controller, - Services, - VerificationMethods, - DID, - PrivateKey, - Curve, - PolluxError, -} from "../../domain"; import { JWTCredential } from "../../pollux/models/JWTVerifiableCredential"; +import { JWTCore } from "./jwt/JWTCore"; +import { JWTInstanceType, JWTSignOptions, JWTVerifyOptions } from "./jwt/types"; +import { PublicKey } from "../../domain"; +import { decodeJWS } from "./decodeJWS"; +import { base64url } from "multiformats/bases/base64"; -export class JWT { - private castor: Castor; +export class JWT extends JWTCore { + public type = JWTInstanceType.JWT; - constructor(castor: Castor) { - this.castor = castor; + async decode(jws: string) { + return decodeJWS(jws) } async verify( - options: { - issuerDID: DID, - holderDID?: DID, - jws: string - } + options: JWTVerifyOptions ): Promise { try { const { issuerDID, jws, holderDID } = options; const resolved = await this.resolve(issuerDID.toString()); - const verificationMethod = resolved.didDocument?.verificationMethod; - if (!verificationMethod) { + const verificationMethods = resolved.didDocument?.verificationMethod; + if (!verificationMethods) { throw new Error("Invalid did document"); } - - const validVerificationMethod = didJWT.verifyJWS(jws, verificationMethod); const jwtObject = JWTCredential.fromJWS(jws); - if (jwtObject.issuer !== issuerDID.toString()) { throw new Error("Invalid issuer"); } - if (jwtObject.isCredential && holderDID && holderDID.toString() !== jwtObject.subject) { throw new Error("Invalid subject (holder)"); } - - return true; + const { signature, data } = await this.decode(jws); + for (const verificationMethod of verificationMethods) { + const pk: PublicKey | undefined = this.getPKInstance(verificationMethod) + if (!pk) { + throw new Error("Invalid key verification method type found") + } + if (!pk.canVerify()) { + throw new Error("Invalid key verification method type found") + } + const decodedSignature = base64url.baseDecode(signature) + return pk.verify( + Buffer.from(data), Buffer.from(decodedSignature) + ) + } + return false; } catch (err) { return false; } } - public async resolve(did: string): Promise { - const resolved = await this.castor.resolveDID(did); - const alsoKnownAs = resolved.coreProperties.find( - (prop): prop is AlsoKnownAs => prop instanceof AlsoKnownAs - ); - const controller = resolved.coreProperties.find( - (prop): prop is Controller => prop instanceof Controller - ); - const verificationMethods = resolved.coreProperties.find( - (prop): prop is VerificationMethods => prop instanceof VerificationMethods - ); - const service = resolved.coreProperties.find( - (prop): prop is Services => prop instanceof Services - ); - return { - didResolutionMetadata: { contentType: "application/did+ld+json" }, - didDocumentMetadata: {}, - didDocument: { - id: resolved.id.toString(), - alsoKnownAs: alsoKnownAs && alsoKnownAs.values, - controller: - controller && controller.values - ? controller.values.map((v) => v.toString()) - : [], - verificationMethod: - verificationMethods && verificationMethods.values - ? verificationMethods.values.map((vm) => { - if (vm.publicKeyMultibase) { - return { - id: vm.id, - type: "EcdsaSecp256k1VerificationKey2019", - controller: vm.controller, - publicKeyMultibase: vm.publicKeyMultibase, - }; - } - if (vm.publicKeyJwk) { - return { - id: vm.id, - type: "JsonWebKey2020", - controller: vm.controller, - publicKeyJwk: vm.publicKeyJwk, - }; - } - throw new Error("Invalid KeyType"); - }) - : [], - service: - service?.values?.reduce((acc, service) => { - const type = service.type.at(0); - if (type === undefined) return acc; - return acc.concat({ - id: service.id, - type: type, - serviceEndpoint: service.serviceEndpoint, - }); - }, []) ?? [], - }, - }; - } - - private getPrivateKeyAlgo(privateKey: PrivateKey): { alg: string, signer: didJWT.Signer } { - if (privateKey.curve === Curve.SECP256K1) { - return { - alg: 'ES256K', - signer: didJWT.ES256KSigner(privateKey.raw) - }; - } - if (privateKey.curve === Curve.ED25519) { - return { - alg: 'EdDSA', - signer: didJWT.EdDSASigner(privateKey.raw) - }; - } - /* istanbul ignore next */ - throw new PolluxError.InvalidCredentialError(`Unsupported key type ${privateKey.curve}`) - } - async sign( - options: { - issuerDID: DID, - privateKey: PrivateKey, - payload: Partial - } + options: JWTSignOptions ): Promise { const { issuerDID, privateKey, payload } = options; if (!privateKey.isSignable()) { throw new Error("Key is not signable"); } - const { alg, signer } = this.getPrivateKeyAlgo(privateKey); + const { signAlg, signer } = this.getSKConfig(privateKey); const jwt = await didJWT.createJWT( payload, { issuer: issuerDID.toString(), signer }, - { alg } + { alg: signAlg } ); return jwt; } diff --git a/src/pollux/utils/SDJWT.ts b/src/pollux/utils/SDJWT.ts new file mode 100644 index 000000000..cef8fa30f --- /dev/null +++ b/src/pollux/utils/SDJWT.ts @@ -0,0 +1,70 @@ +import { SDJwtVcInstance, } from '@sd-jwt/sd-jwt-vc'; +import type { DisclosureFrame, Extensible, PresentationFrame } from '@sd-jwt/types'; +import { JWTCore } from "./jwt/JWTCore"; +import { JWTInstanceType, JWTSignOptions, JWTVerifyOptions } from "./jwt/types"; +import { JWTObject, PublicKey, PrivateKey } from '../../domain'; +import { decodeJWS } from './decodeJWS'; +import { SDJWTCredential } from '../models/SDJWTVerifiableCredential'; + + +export class SDJWT extends JWTCore { + public type = JWTInstanceType.SDJWT; + + async decode(jws: string): Promise { + return decodeJWS(jws) + } + + public createDisclosureFrameFor(config: DisclosureFrame): DisclosureFrame { + return config; + } + + async verify(options: JWTVerifyOptions): Promise { + const { issuerDID, jws } = options; + const resolved = await this.resolve(issuerDID.toString()); + const verificationMethods = resolved.didDocument?.verificationMethod; + if (!verificationMethods) { + throw new Error("Invalid did document"); + } + const jwtObject = await SDJWTCredential.fromJWS(jws); + if (jwtObject.issuer !== issuerDID.toString()) { + throw new Error("Invalid issuer"); + } + for (const verificationMethod of verificationMethods) { + const pk: PublicKey | undefined = this.getPKInstance(verificationMethod) + if (pk && pk.canVerify()) { + const sdjwt = new SDJwtVcInstance(this.getPKConfig(pk)); + try { + await sdjwt.verify( + options.jws, + options.requiredClaimKeys, + !!options.requiredKeyBindings ?? false + ); + return true; + } catch (err) { + console.log(err) + } + } + } + + return false; + } + + async sign(options: JWTSignOptions): Promise { + const sdjwt = new SDJwtVcInstance(this.getSKConfig(options.privateKey)); + return sdjwt.issue(options.payload, options.disclosureFrame) + } + + async createPresentationFor( + options: { + jws: string, + privateKey: PrivateKey, + frame?: PresentationFrame | undefined + } + ) { + const sdjwt = new SDJwtVcInstance( + this.getSKConfig(options.privateKey) + ); + return sdjwt.present(options.jws, options.frame) + } + +} \ No newline at end of file diff --git a/src/pollux/utils/claims.ts b/src/pollux/utils/claims.ts index 7533b2979..1fcf67ac9 100644 --- a/src/pollux/utils/claims.ts +++ b/src/pollux/utils/claims.ts @@ -1,4 +1,4 @@ -import { CredentialType, PresentationClaims, PolluxError, InputFieldFilter, PresentationSubmission, PresentationDefinitionRequest } from "../../domain"; +import { CredentialType, PresentationClaims, InputFieldFilter, PresentationSubmission, PresentationDefinitionRequest } from "../../domain"; import { AnoncredsLoader } from "../AnoncredsLoader"; export function validatePresentationClaims( diff --git a/src/pollux/utils/decodeJWS.ts b/src/pollux/utils/decodeJWS.ts index 52f2d3523..6ca96ffbc 100644 --- a/src/pollux/utils/decodeJWS.ts +++ b/src/pollux/utils/decodeJWS.ts @@ -1,14 +1,21 @@ import { base64url } from "multiformats/bases/base64"; import { InvalidJWTString } from "../../domain/models/errors/Pollux"; +import { JWTObject } from "../../domain"; -export function decodeJWS(payload: string): string { - const parts = payload.split("."); +export function decodeJWS(jws: string): JWTObject { + const parts = jws.split("."); if (parts.length != 3 || parts.at(1) === undefined) { throw new InvalidJWTString(); } - const jwtCredentialString = parts.at(1)!; - const base64Data = base64url.baseDecode(jwtCredentialString); - const jsonString = Buffer.from(base64Data).toString(); - return jsonString -} + const headersEnc = parts[0]; + const headers = base64url.baseDecode(headersEnc); + const payloadEnc = parts[1]; + const payload = base64url.baseDecode(payloadEnc); + return { + header: JSON.parse(Buffer.from(headers).toString()), + payload: JSON.parse(Buffer.from(payload).toString()), + signature: parts[2], + data: `${parts[0]}.${parts[1]}`, + } +} \ No newline at end of file diff --git a/src/pollux/utils/jwt/JWTCore.ts b/src/pollux/utils/jwt/JWTCore.ts new file mode 100644 index 000000000..c05fd6b5e --- /dev/null +++ b/src/pollux/utils/jwt/JWTCore.ts @@ -0,0 +1,155 @@ +import * as didResolver from "did-resolver"; +import type { Extensible } from '@sd-jwt/types'; +import { base64url } from "multiformats/bases/base64"; +import { base58btc } from 'multiformats/bases/base58'; + +import { Castor, AlsoKnownAs, Controller, VerificationMethods, Services, PublicKey, PrivateKey, Signer, Hasher, Verifier, Curve, Apollo, KeyProperties, KeyTypes } from "../../../domain"; +import { JWTDecodeResponse, JWTInstanceType, JWTSignOptions, JWTVerifyOptions } from "./types"; + +import { defaultHashConfig, defaultSaltGen } from "./config"; +import { VerificationKeyType } from "../../../castor/types"; + + +/** + * JWTCore + * Wraps signing and verifying functionality with all our supported algorithms + * Works for both secp256k1(ECDSA) and ed25519(EdDSA) + */ +export abstract class JWTCore { + abstract verify( + options: JWTVerifyOptions + ): Promise; + + abstract sign( + options: JWTSignOptions + ): Promise; + + abstract decode(jws: string): JWTDecodeResponse + abstract type: JWTInstanceType; + + public castor: Castor; + public apollo: Apollo; + + constructor(apollo: Apollo, castor: Castor) { + this.apollo = apollo; + this.castor = castor; + } + + public async resolve(did: string): Promise { + const resolved = await this.castor.resolveDID(did); + const alsoKnownAs = resolved.coreProperties.find( + (prop): prop is AlsoKnownAs => prop instanceof AlsoKnownAs + ); + const controller = resolved.coreProperties.find( + (prop): prop is Controller => prop instanceof Controller + ); + const verificationMethods = resolved.coreProperties.find( + (prop): prop is VerificationMethods => prop instanceof VerificationMethods + ); + const service = resolved.coreProperties.find( + (prop): prop is Services => prop instanceof Services + ); + return { + didResolutionMetadata: { contentType: "application/did+ld+json" }, + didDocumentMetadata: {}, + didDocument: { + id: resolved.id.toString(), + alsoKnownAs: alsoKnownAs && alsoKnownAs.values, + controller: + controller && controller.values + ? controller.values.map((v) => v.toString()) + : [], + verificationMethod: + verificationMethods && verificationMethods.values + ? verificationMethods.values.map((vm) => { + if (vm.publicKeyMultibase) { + return { + id: vm.id, + type: vm.type === Curve.SECP256K1 ? "EcdsaSecp256k1VerificationKey2019" : vm.type === Curve.ED25519 ? 'Ed25519VerificationKey2020' : 'unknown', + controller: vm.controller, + publicKeyMultibase: vm.publicKeyMultibase, + }; + } + if (vm.publicKeyJwk) { + return { + id: vm.id, + type: "JsonWebKey2020", + controller: vm.controller, + publicKeyJwk: vm.publicKeyJwk, + }; + } + throw new Error("Invalid KeyType"); + }) + : [], + service: + service?.values?.reduce((acc, service) => { + const type = service.type.at(0); + if (type === undefined) return acc; + return acc.concat({ + id: service.id, + type: type, + serviceEndpoint: service.serviceEndpoint, + }); + }, []) ?? [], + }, + }; + } + + // Function to convert DER signature to raw signature + + + protected getSKConfig(privateKey: PrivateKey): { signAlg: string, signer: Signer, hasher: Hasher, hasherAlg: string } { + return { + signAlg: privateKey.alg, + signer: async (data) => { + if (!privateKey.isSignable()) { + throw new Error("Cannot sign with this key"); + } + const signature = privateKey.sign(Buffer.from(data)); + const signatureEncoded = base64url.baseEncode(signature) + return signatureEncoded + }, + ...defaultHashConfig, + ...defaultSaltGen + }; + } + + protected getPKInstance(verificationMethod: didResolver.VerificationMethod) { + if (verificationMethod.publicKeyMultibase) { + const decoded = base58btc.decode(verificationMethod.publicKeyMultibase); + let pk: PublicKey | undefined = undefined + if (verificationMethod.type === VerificationKeyType.EcdsaSecp256k1VerificationKey2019) { + pk = this.apollo.createPublicKey({ + [KeyProperties.curve]: Curve.SECP256K1, + [KeyProperties.type]: KeyTypes.EC, + [KeyProperties.rawKey]: decoded + }) + } else if (verificationMethod.type === VerificationKeyType.Ed25519VerificationKey2018 || + verificationMethod.type === VerificationKeyType.Ed25519VerificationKey2020) { + pk = this.apollo.createPublicKey({ + [KeyProperties.curve]: Curve.ED25519, + [KeyProperties.type]: KeyTypes.EC, + [KeyProperties.rawKey]: decoded + }) + } + return pk + } + throw new Error("Not supported") + } + + protected getPKConfig(publicKey: PublicKey): { signAlg: string, verifier: Verifier, hasher: Hasher, hasherAlg: string } { + return { + signAlg: publicKey.alg, + verifier: async (data, signatureEncoded) => { + if (!publicKey.canVerify()) { + throw new Error("Cannot verify with this key"); + } + const signature = Buffer.from(base64url.baseDecode(signatureEncoded)) + return publicKey.verify(Buffer.from(data), signature) + }, + ...defaultHashConfig, + ...defaultSaltGen + } + } + +} \ No newline at end of file diff --git a/src/pollux/utils/jwt/config.ts b/src/pollux/utils/jwt/config.ts new file mode 100644 index 000000000..3489e56f2 --- /dev/null +++ b/src/pollux/utils/jwt/config.ts @@ -0,0 +1,26 @@ + + +import { hashSync, hash } from "../../../domain/utils/hash"; +import { randomBytes } from "../../../domain/utils/randomBytes"; + + +export const defaultHashConfig = { + hasher: hash, + hasherSync: hashSync, + hasherAlg: 'SHA256' +} + +export const defaultSaltGen = { + // Justified ignore as its not used but required by the JWT libraries to instantiate + // istanbul ignore next + saltGenerator(length: number): string { + if (length <= 0) { + return ''; + } + const array = randomBytes(new Uint8Array(length / 2)); + const salt = Array.from(array, (byte) => + byte.toString(16).padStart(2, '0'), + ).join(''); + return salt; + } +} \ No newline at end of file diff --git a/src/pollux/utils/jwt/types.ts b/src/pollux/utils/jwt/types.ts new file mode 100644 index 000000000..38deeedfd --- /dev/null +++ b/src/pollux/utils/jwt/types.ts @@ -0,0 +1,54 @@ +import { JWTPayload } from "did-jwt"; +import { SdJwtVcPayload } from "@sd-jwt/sd-jwt-vc"; +import { DisclosureFrame } from '@sd-jwt/types'; +import { SDJwtInstance } from '@sd-jwt/core'; +import { + DID, + JWTObject, + PrivateKey, +} from "../../../domain"; +import { Extensible } from "did-resolver"; + +export enum JWTInstanceType { + JWT = "JWT", + SDJWT = "SDJWT" +} + +export type JWTVerifyOptions< + T extends JWTInstanceType +> = T extends JWTInstanceType.JWT ? + { + issuerDID: DID, + holderDID?: DID, + jws: string + } : + { + issuerDID: DID, + jws: string, + requiredClaimKeys?: string[], + requiredKeyBindings?: boolean + } + +export type JWTSignOptions< + T extends JWTInstanceType, + E extends Extensible +> = T extends JWTInstanceType.JWT ? + { + issuerDID: DID, + privateKey: PrivateKey, + payload: Partial + } : + { + issuerDID: DID, + privateKey: PrivateKey, + payload: SdJwtVcPayload, + disclosureFrame: DisclosureFrame + } + + + +export type JWTDecodeResponse< + T extends JWTInstanceType +> = T extends JWTInstanceType.JWT ? + Promise : + ReturnType['verify']> \ No newline at end of file diff --git a/tests/agent/Agent.functional.test.ts b/tests/agent/Agent.functional.test.ts index ab92e1255..8982289ae 100644 --- a/tests/agent/Agent.functional.test.ts +++ b/tests/agent/Agent.functional.test.ts @@ -9,6 +9,7 @@ import Agent from "../../src/edge-agent/Agent"; import { Pluto } from "../../src"; import { mockPluto } from "../fixtures/inmemory/factory"; import * as Fixtures from "../fixtures"; +import { ApolloError } from "../../src/domain"; chai.use(SinonChai); chai.use(chaiAsPromised); @@ -19,43 +20,28 @@ describe("Agent", () => { let pluto: Pluto; let sandbox: sinon.SinonSandbox; - afterEach(async () => { - jest.useRealTimers(); - await agent.stop(); - sandbox.restore(); - }); + describe("Functional Tests", () => { - beforeEach(async () => { - jest.useFakeTimers(); + afterEach(async () => { + jest.useRealTimers(); + sandbox.restore(); + }); - sandbox = sinon.createSandbox(); - pluto = mockPluto(); - agent = Agent.initialize({ mediatorDID: Fixtures.DIDs.peerDID1, pluto }); - // ??? cant start agent - errors for start mediation - // await agent.start(); - await pluto.start(); - }); + beforeEach(async () => { + jest.useFakeTimers(); + sandbox = sinon.createSandbox(); + pluto = mockPluto(); + agent = Agent.initialize({ mediatorDID: Fixtures.DIDs.peerDID1, pluto }); + await pluto.start(); + }); - describe("Functional Tests", () => { describe("createPrismDID", () => { - test("default parameters - should return unique DIDs", async () => { + it("default parameters - should return unique DIDs", async () => { const first = await agent.createNewPrismDID("a"); const second = await agent.createNewPrismDID("a"); - expect(first).to.not.deep.eq(second); }); - - test("same services and keyPathIndex - should return the same DID", async () => { - const services = []; - const keyPathIndex = 1; - // alias (first parameter) doesn't affect ouput - const first = await agent.createNewPrismDID("first", services, keyPathIndex); - const second = await agent.createNewPrismDID("second", services, keyPathIndex); - - expect(first).to.deep.eq(second); - expect(first.toString()).to.deep.eq(second.toString()); - }); }); }); }); diff --git a/tests/agent/Agent.test.ts b/tests/agent/Agent.test.ts index ca0b6439c..5019c6601 100644 --- a/tests/agent/Agent.test.ts +++ b/tests/agent/Agent.test.ts @@ -15,6 +15,7 @@ import * as Fixtures from "../fixtures"; import { Api, + AttachmentDescriptor, Credential, CredentialMetadata, CredentialType, @@ -37,7 +38,7 @@ import { ProtocolType } from "../../src/edge-agent/protocols/ProtocolTypes"; import { CredentialPreview } from "../../src/edge-agent/protocols/issueCredential/CredentialPreview"; import { RequestCredential } from "../../src/edge-agent/protocols/issueCredential/RequestCredential"; import { IssueCredential } from "../../src/edge-agent/protocols/issueCredential/IssueCredential"; -import { base64url } from "multiformats/bases/base64"; +import { base64, base64url } from "multiformats/bases/base64"; import Pollux from "../../src/pollux/Pollux"; import { AnoncredsLoader } from "../../src/pollux/AnoncredsLoader"; import { RequestPresentation } from "../../src/edge-agent/protocols/proofPresentation/RequestPresentation"; @@ -102,7 +103,7 @@ describe("Agent Tests", () => { pluto = new Pluto(store, apollo); const mercury = new Mercury(castor, didProtocol, httpManager); - const polluxInstance = new Pollux(castor); + const polluxInstance = new Pollux(apollo, castor); const handler = new BasicMediatorHandler(DID.fromString("did:peer:123456"), mercury, pluto); const seed: Seed = { value: new Uint8Array([69, 191, 35, 232, 213, 102, 3, 93, 180, 106, 224, 144, 79, 171, 79, 223, 154, 217, 235, 232, 96, 30, 248, 92, 100, 38, 38, 42, 101, 53, 2, 247, 56, 111, 148, 220, 237, 122, 15, 120, 55, 82, 89, 150, 35, 45, 123, 135, 159, 140, 52, 127, 239, 148, 150, 109, 86, 145, 77, 109, 47, 60, 20, 16]) @@ -298,10 +299,8 @@ describe("Agent Tests", () => { test("restore", async () => { const stubRestore = sandbox.stub(pluto, "restore"); - const backupJWE = "eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJlcGsiOnsieCI6ImFmYVBmZW9BVEVfdzdLOFBzcGN2S2ctTVJvTUllYk80Y1JVZS1mbENTRFEiLCJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AifX0.QIoYOLdEB1vkp8zsy3GU1WP0RmPp-8QGvtzAmIVtyIV7yVg9KtSD3nMSlIEbTRBSh7bAfJhPqDSS3wpmZ8buktfBR0xSrOtr.EsrbZ_n6iLDexZIgY5HF1A.r1rlDSoqvfxYxGSxCccjCGUAculGoOM99o5yryEraaj0Bhj6i4TfvVaIhOudmAH1OgpPxMCsH0aTEmlxVwJRJN2Ke6DEVA3hnNgPbpOJC0e4D3FNALzhgLQ5Dql9eiIZpgRov83nuw-4xUZGHK2MEkPyA8s9UEb62dNn-Ech_blIYKfljyl3cH9LiuVvd4ojLleeABBVcJSLHCQu2rTA1m4vLFJOzJYPQ5KPxO7Joh6I-2SA73WjAciL7ivUcXJcQzzAkq6Xug58vOZe7C7-_PAgL31nVXxbxkJYbSrCgsZkHpLwbT4lRS64txdRy02MkiB9i-2xNkGa5yKL5T1eyugdOTr1JaUoJeJ7aIeqTR6bxQzk8BnoQgPu2I-0-xEPlWtBxNg50UT2VP2WBqO5z1cpAuK3BomcEfMO6xNEaF7CcDInrFAEsiClf3tBpRiP5A3b4-IKSjACoxB27S3O4uoAr3ObKHjuHgN2bW_bM0dwoYoTbanzNM3EFiqD9rP9dCbq0xySMgWUtv8gWKOABtOI1LexOZM9ZbHDy0ekuaYhaTRe6hYR0XqUdo3Pwe5f_HoUWdAFSMxQZ_dJgnu0EO22-Vw2q30YN5nkzMY2php8PJ0eyH9HSw6q4jMDkzVpwE77QWojfAbsO21s7Tj4RQJRvmGGlNP2FIlgSoLjXgCj27xmXTNbYX2BVJTb03EowV6QBmEo-1xJtBdDz6bTztn0SYiv0J-U5fgGN0vYPYe-5srvNOiY7bqFoFm8r-J-kng89-z9gtcfCiIDwHDICxdLp_wPUMYQydhrTpKxit-BL3r3OSl9QR3Y16snUXmTgCxhEP75V5grTuHAC6oRKDUHc8fmUzA7JTJAmBGkmg_Thhl75WHx4FNHVi7NJDZ9ertkWIVbWmg3dwJcKXWp-xRx0gDj9v0SZ5HXeTez9u84w3TjFaQU9uargwr2pR0KqvcBJKwkblqI2W1LvdE2rJ4EqPVtvn8HO1yEei0fiHXT3A5E5pdUi5bRvvXT2Bz9j36QbmDo4decXP6oei1MTqm-T_nR466LqPd61z7AtXyOuqrWNEpOvi3pNms_ADRQUEBav1QwUQaVIb4cFI6QcZVZiBuucxA164Bv7vayIcV4UZw3ec_WMEiSEwTI2cO7t1tZdlkfLygbVySR7PpayGrvpAvxEkBmp0QM92G5m9z-0upQGlkuLF9ATaB5KLbGyetUjbPah8z_Ybrz5x__bI0r81PRs_GEtd_2sqTy8J0Uh9C0lT12Ul9rBji42t6dyNlGiibLytEP3zaY9al9xQVs7AksIzLxIP_xcu8mu4V8S0BV5D8YW8y866_9idjCQRcTXijwfz3mzOhmXVVPN3h66nYvjsiDoBm1R0sDtLFJK0wE4DiiU5F-Je7B5--IHB620Dc8YeyRcWJZn48ZVKPiyrU6n2xLnuYgUn4ht3UplvOdGZhxWwlkaEXrI1eAEMC-osMzzEiQCjieQMtmJTnaw85q-OQDhCEvsupIgTO5jIiXcREXPs9Zr7pRFqFP37Ue4OpJ4VMYtpC-MSHT21Aqi0A0IqU-kbcFR-VQUX89fVfHmyPHGKrWAfUQ121TlbB9e8k7hq7KUFV7g6G068YFHR7Qf2vVnYO-Qc_-djH-oZHU5D4fTInJWRJtqezzYSsDNbpYYC-jj3-bSVVk8L48nW1V1zwtCFmIdUy5g-I8huY8t2EwU4TkTYzGSjxHAQxX7ZE8D6NCu9bvUD3uYTTSOYzktepJc2EaeRWaRHYo1S7e-E7hfvKvXBHaSzHZGCBVOUkB6AWH7StEEVDlK7ZiREgRM6fKh1nSlVnUd1TaJUpXidNueNTW2r0g5Mfm1CKnnsdxpMjS8_TbTExPfAfiK4Tun3Q-uRiw8f45TgFgq2DBqjXODfhXbQdPLoLPKshE4YJDad7w6FvItUF5VZyfXqW0p6G_bQ7lMrgAhAHESehBYCYrIBvnaDyI6wpsYCS70chAj_P55xIiPs2NPXWFtl8sznrcns6zKDYIycnZem0r_DbpMa3N-bwe26OlGnUehAKRd3MvD3chX5yy5QHwFllXN7orGcLLOZE9FzBNK1LSVWWGId1xH8kbpLHbmWjLjCFGcssXgU4R0WU31CROaNxfDFNJ5bMFUxLkFsH5eWZHrr-xTELU7SVjvO8X1uZC_pV7e5pR5SwLNkdZ_BkqKZ-Ad7GYaG98i3omaXRHqYDKx38g1pcVSXngbjJSk0JD-MtCaj12M9s9N_7uQik64lCT9Ta5QrmJzwAgFwnaMWB7WSvKxwdhEYBoDEM8j3gkbW4yJW46c21K0Q6Cadokh0otrNmycQv_pQVMpsRWiGJl5qDF6dNq59GdscdaWvKBJmR7A17twj6m7Mu09X5HcCz_0F900O0BfVYGQ2_IJMMFP1S74jWCg9rxKkLOcWNx27Txe5Ro9VGFww2YCXGSITtG9Pf5TG22U5uWUVuOFqWc2W_OeMSwtNTXylrs7THhX1L_a0oQVvd_NCaV127iHjOoFODkrbdUUpl4eGHa4M3S0d4HU4ezbj5QOiMhsbSrDh9RWx2KnTkgYsr69ZCOcGaIznc9m8j8qLKAwUnwbQ2-ipbvxbwLr3TK6JDpK-hvru3fPqBJ_TTknlA1kYnENvgIg9FksfBVscnT3kQhJ1YPOsSEYV_loYDQjZ9L1II1Z_8SNso5BgJgCUafmndFOpXHXGIRNt8VGxdxP1kNRoEIp4W3flLAuejJo0AdPcCM1G9656zX1ZXKcv66YgLdvB9F6PnijgGVaKklFOfex4GllYfDER2Qyci00msDIx4SnGW6QmkLjZ9Bd51slRQYrO9UHPfTOuvVXN5Wi9fiXfKg6oqpf6AVOxQxDKzjJUrmO8A-67SU2-ofpKcf_DbreLCb82UgcxhS4qyR8H63vgfXL2keGyCp-DVkZGy02vdVhk2zqtbitwT6b0RD_fBeKQQ9eWEOQ3m5eANejfa12WNDFUC5lu7WHcCPoiNN-u_kFstU6jB_Z34gZkcgasmmkrxZ-kfZwa5Zd8R6W79l9DQOKf4Q5yPFI0qcdT1TAr1RS7QLTVZxVRM8shGcQ6W7ZPV1PnTA8U2tX2S-MRxknim6t8eZdrbEtOzmTjS4xxckEfrvxBqgHNXVb-jxGrFkC_WyOMIdM3HeTnYvvVDneUGBmroa0uo51oMGARUqiDxw7C8IrjsjuHmulO8KadCIXJHJf1s0yCrUGwD7vtdIdK7a_USOpC_N_O9W40Eg1C9AnlypC4mnt1sGu0pQG0WheTsxZi4XWiYooy6eDsq9V4ueeCYkgzRnjYVX7t4WVXBbLK5mHKGsZwF2uguimwHhBgIaod3o62e930jhoiupijwSDyQ28ZjEYBmteeyvzNs98OMbklTYc3qAoTor-D4IgW9SRB41xPXsrt8givAPjyCtlDELceO76DOBZrMKo2GFL6XVkZe81RW5MGIu9iHuJHN67XCNvFvq61zo4rGcqaWA5TuylLwsUliv5ptT0WZOQj8bC4RS4ZCAjLp9Rv4VjpJ2P9iLlBMKynNwZlUx_0JCJSlaW1X_zrqfv3raRLTzoF5xKMAuNZgzHap1ajjo_iuHfU8VgVfVT-FUf4UuHv-WOuSv1htYMrtrCJvaU3osUIt3Y935ieqpW6cxlv3qTWkWXA0SyGXOWMh4VfNTuOmN7lOZEPkvs9uM3rqOvk5int0ol0cyj3RacBldZnoVyLWGJtrn1YNRvTStWx-nCDrRGQLN5KFaX73jAGyu7VWVbdwxX2cxe8uylLCn-qweI-AEG1QuIMHmoYk28BjTWIo5INRyUU64edlTBkmR3i6uLoNiPEgZGXri1fbdl4A8zr3cMJ79f3SVSis4R2nfCH86U-fF9irRGcrYY_MrAxvP5tIi8HbaA1yeKFEmycXWsHdIY0fxwEGMHhD8XyDMq13riGfD2KMKKaANygR4buILU-Nl4knT2LxvqJUj7eyZQXNK3gA-Na3qGQfJgF27ZlQfyKq6LeQWSKHyMmkeJhJcXzp9wJFxZ-ib876IGZdrLMxQY1lvummSiIc_cNrARKskkepqCmusr_yav2ahH44juNVSeshcnLSXbPIRAM3Yl97X1noazR60BiESsVLN5H8nw3uEqsL_OfHFUsOWAdrcTFvfD-2xXUjfWyStu3DTyKHEBPVowwqFaROjMEZt81kgsbe8yW-baL3M3Hr1-O8-ofJ8FZH7ADnQoBHHpRfskESpcPneA3PHj_H3fQJpA9zqFuJXdTmCzJVkf34qQbUJs2YUzY7SvnH2Xfcpc4Ch1zu-YgOhu8duGouSifAi1ds52RKcK7Pldiv1xSoscetx7CXz7zxtr0WWS7RQQnCfJblLbgp-bqnv4C0tufMcXnsSUWJW3jup1j0Go5neXzrAypJ6ABD1H184yF9mEeSCIT724NgVhTMfV32COtrsb4i1AoyaDtprTgnPnI1gfz9cM5jT1oZE42UzRzycrCgABEQqpQwpjYh5m8_2jml-i6iK247sEqExajylyG_fWZxdeU8_ieAJccLgOfNZgsZv3Vnx4KjHX0DUm5MCjj6jTxkkpj0I9i1i0YSOa_8KDAy_Qlqq5QWrNF2XrZwFpGtSIZJ4e2abCHdsN2Vc8lFxkmzPv2UM-IzgxM9pSWfjrO0N66ajfLRLOXj8poSjo1v2prlIpp0C0Zw-yc8pagCuKn7nIC0i53qkEeLNoZ5brXqJVeWqlk5m5tGFykC1njpaYcyTh01Khy33QuGdNDCwxSL7M1qIrwWgbnsB3AmybezSJraOG-14pnBRsggVb9EhaJgP3-8W19P4lX6ulTTKZppyB5TfLeLaVKKTgaZDAPMh-F3VnaqlI8kxDHfU4AzzADNFZJ8v41giN3o5P7E8gfWMC-ygq33F2a5IfjESCH80LC7C6X57DtLG8E5raXL4p44x24u26VZS1xvxGaw6QwoAg2N45RFTsCUemApjTi_7jG9Zuf6IsrwQF0dd82EuNx0oWifcMMTJy1-kfafytb4BILnjxTzwUZ17nOatmNuOYEw49j1d6BvB3R1LZuuAqTxp9UeFBPP7t0At5xHYYJMHAV4k8gdxhiv8p8m76MUtQUIgzVP9PZQJA-BA1WMUXIbz0kagMNIm8QyhAMChgJyrNJ28i--8A0KLYsz8nbLycITXZeT5SERFFrqDm2xuL0eIYQDbWFQl7pZxfgOHA6IDSflJqXnxEMlwjDbv1mgCXidrELsRH_YJEEx-i6yoQ95OWCWEC8Ja5LxwrQvlRcg333g-zTL8sZYBk3N3Ypv0tVhPB9Z0BDaA8qFK2T_5Nsbx1eiLgIn6gvo-DLxNneEs98iYIE4TT18DH6B3WHKzTNvdLk0m4pt5ZYfOeGEewzhb3o8-L7pCfa2N3pSX4V_zavs7dOioEgaEcZBljxataw_gqPiokwI-2KxN7ki9w9cp9-5y2IvC_7ibcPGYqp6U0TzAGisn_QEO0Bj1Hlkdn41NM0K_khM98QKjBhIe_lLsgwhPuJCBU9RvRMBrj6AXboxbvliMxNOEMk1iQQOufrpibObMuQOyX2tWNGyAzRCzOkJaahqsg2KA4rMVmhCw5FxzosHXaFEqIQhgzXNEZ7bOsXMHvfmu-HxLBO7NL3MgBAweiJpuIA4meGAIX6X5AXAhL0XsgC4_CCkDi7GEUi5WQ8b98QJ-FTAZNxmDczFteHnLkZsQ0XCHeGrDII-a18kokWj51Z1lygoswZYSSjnaD-4-Y-Q8fADtJg641n5pmSr7ZFRz8bg5xHd4XPpBYd-S6Ibtd8uTPr3rSmsgnZzRSCN3rcBnIbpTVZy76NNLHF4-WEZYAPsVYmcK6nbXSImgfhYJKM3TWacJXaM-ST3DIi76UsW5CCtWamq6DjZZqn6kUQFeX_Bmmdokl-RBHAibUW0G7lBEpDofXlypmzbDv4uVx7vf1LmskyYkROrc2qVyOlFIx0ho8GRtqHTvPtafIVe6sr1XXuZyVLf5GVCW6bZFOQGnodlnnP6xPnuRmUvAt4UQ-HMuJyCRLCGL7wZ2WEwDcPyW5mnkCNBSvZ9_Y6SVGKBdpNq3w5lUsithQpHvHbnZbZwci4j2bsHRuvTc6ZOMe54dsaKdJ6rGFQvjbB5hcyYx8s0v_xHhBrxSWsanHL9L0shM4qvQlikVnN6fsEACniA-6sZdRhZ28CcvMnUScaZePkKYJgPczMfdt61ba0ycoyDMCIwkn_X-Bt7A8RBubnWRipfWEjqAXNZyLQ2dkifJRbqDhObvuyEc_lCBRB63ujE26riMQZZvUl_OQO4-LHG7QX3oanngPGAlTCBsGKgwhKGc94zGLnIYbYRfdZMydshX85DmvSi4Nan5AUN6R-Q0Agjx8keZBk_jp1ewixsKDa7eYEPGlt_I18BpfchpfcmG62eE7CHORu7uMqdinDnywY3vccC0PP29RatAFAaC3klfN3deXe6XeVayNOysZuZ__7Tvh8TqsaFO1OBhhKUIXHAFqFiF3QsUM38l2qlEAhjG35oZgVtfSmG10gjAOAlczeeOCIq0XNYmXYSX4dthhwrqpkCze-kHSnQ3DINq1v6ghbLVxXIaS4MNEOT26s0ix1lR79WxMlf8u99wT9cm1qpCdAjDbanaLnpZHAxdR5FyXBIYBHaOYqmNO1iZO4njinECyMak90LVA5Rt0R8qxV8T30LhwMDttcvX4SGjnqzFcZagrOybCwuo-oejjiAfvilsTD8CW83cug327swvShAARw21cuz_mqSuKWMkug2bs_B1sA2u5ViFFfvdQ8x2kSSuZE8GPTr844HTtIPg2ZqTezgFpJnFFMmMZmQXe-3RQxcOZkXi4mPYvG6dPtycgqF6I128VGPKGhugH4HfJXKoutU-VJW1SW_sMNJN-ijD4BAMDljqV-gqEorr9fyuOo5ABzlWcs_ITxEsRzYlY3HgRYSHTYKKyBLeUgnZjXAgIXnya8nuFOqIcm-HopatFcOuwumyixhGGx8X7NPXzN9JZr9z3sHem41cbaTqmr_KFPugJAp6_5EezJBQn0Tmr7_NF5ZzcjKK_TqdeiWfxmwWdlVZnWH0hfEkXVO9bHr8l61lOM6xrSeWw5nTMk08noafZ6gXId406tNovFydxI8KRL2zr_VUnIVJhJRq3AWwnS01AV2T_WlZTxYQr8v5m9pRdH1cpDTTeRfjr9Cuk9cIggQC8ya-D7gDY6ciuIk5CExgkA_gM0xeYUBDcXxgaw9a5IdgS0mZ7CzL8aw2qB_ItsnOrEOlDPZ43kR1tDFtwaaBk_KBSqFZh-C741JQXEPAeE4enk3QIsgRW9fyI-xSCZXWAO6Rjso7SXlGiMK2RbKeqPEGCoi8ecNzkWF797UuSMHKU4fb5ZLkF6L9wheNHoeRomalaVQToF1qeiQtpF0IArAQuYDRaRt5tutRbxIOx3EbbhTB54Lhoes13Oo4Y4NK8g7lAREBoncLi18Yp2Q0n45alzZLZwM-gEaBGEzJ8THaqkIgDxl2N2QfIzBYt41OyKVbLv1YnZJCIC07jlol823b7ghlGrRsNP8-I7WrkS-4zuiiVHWAmPrmXXuaJqRrz_AvLUtLm_hEGi6YCQIWl4obIJ-WW1_z4kO5MYkKK39O_SmrKhNrlOoTWFpIkrWPLsl2fdhZmx619zP_yaV6Xpy3wVp4lUnbk7SrkMwM8dTde9XtIQPK-dKgR8igcU95tQKXuoaTyG6u63WvOWewzzKkFd7U_em38Dn_9nzllX6fhNCmrVoF8eKG5KQruj_KsviEcs25QDaKF-MbgE4c8kn80hGHfpvs8vqqzNjZBiL8yR9HAqWaxd91k74IUNQyC8-7fINXSYe-WTkfuS48CdkYGCN--hVeAwLLZsGO5opvHPrrGfUEsBM6sUW5KV3Xrzt5QkIu3rEEI9Uq-ZtjqVjgIRDXlCinJvSxwd1sjIcIi2eyEoyG8u0K_-7sHWVGQTypYDEAXNOSBcMC5p7Py88gyyq4Z2ryBb6kvGirbLnkLo1lU-3vZ9jM5djDvbbT_2JujvfLGl64ugjvjhRv20FF6FDlqV3TwhNdhngK9c0O5BcTu3OERmNqhJksQWW25hW7tnQ_rGXF-h3jrnXIGY-TTvwaHO3Eg9mF199gmuELinityaY4gkznBjKYo93F52RoMxcJok29cEeakgriFzxeWeQQLu9FmaQOvq_iLxzFnXhoQ0aZ2DlklcD4a7Gl7g9J9tXrD4gnRnIzgO8lOz17-vEts-dQhwyWsYXWgLErddk1NwTXVWpCQv0cONGzv9QIlgbKC2M8pPwzuARSOvMsM24cJdjtv_0ccKwitGfAkc6qgbAL-ofopbNrsfNt3NLF6woLwGUTdzW4RPTF94tSapy6Sg3Ei9ohKYYa6uc3jwFrVZzRjsDqxktfDoGfOgdjdJw9LnfffG1I640XAHS5jL56XWFYygsnckrXWMxRYyh39JTIOtGYgR3ivFo_kTG6xn5p5lqUC6NEE6N2WJtZDoeZ9bQsnUV6D7Wxi4WTKfLxmAnsBWjggffC5PX9qLcWCqA99JWxQ746Nx-vBBU_5W5sEZEz_o2lVuRlrk1xwZ52tZSTqvyxh_q1Cl4ljrXriq-rM_AVKLP3PgA3uCPOHF5xHkFHPEl92aFt5KaAEHuGTktex1UYYeSigHu_wT2EFTNUGKyPYbU8PWx0IO8IG95frEL8rxOPO7D2ErsUkf0-kGID1RM9t8cOJFxRM_hYsNuXpBwR835jIPia_j0YcWDhamxt-Y4Mn_8EBeKxEGkQiLPLHvW_XkPMWQW4_NwfUWa1g-XnRWfZrkI-TnwscwSwP6FozIqcuiNaKAQHpJtp0odY4iMU0FoO9kW1r3kVTemMetPpDaM_GmRnvE8e1SlcsrOz9RBWAwnvFds8O-TxOp-jo6n6qhMg7JHoURUpMwb1lAHjrEaJBm9mrDysvpSsLuDuR5Nwqb6oY8eRA4izG_7HZwZBEDdbZlGkkrZPoKulbTqBFipcv6sustrwX3WltiSFkxO9EvR73MlvTMVWzXfs-J7dLvpRawoOaM7oB3DTHV8WWtu5XHCnUNIP__Br4e_-7kfgsn97irZJAbAfBgMuIgpKSqt5Iz_2zvJET2AI92RezOHxh4qgagdKvEC1Q8k0lYknknC9F_lnwmfYLHfkD7LfL5OxFTXUAQ4jSByMX9QEyINtJ4vrlL3j-tdG3_HjFuUqbktiCGhLxGFERBgy2_rSiJg-n3-9xfo3uIa_iyZp0tLU2dSRYKd-EIXHc3l2T223p-lGez8HUFcJjWMKJtga_ko5AGsNmkKR6fRl2CQCaIFimoPt8VgaZEpQjvlwKnB64LhL7Hi56AwxKzRZ9Jo_isyvAMRryUQJhJNXzdOH9GF1sTm9_pWm6ieyBLXP_WLGCExtORfnL9o5tHJy1EnAWZ7p8QmdU0sDP6kKNmNC6aquMugf9AeQqOhMmVitOIRlfNRPm61scBntn6r-nWe9Z_3NdF-U3riU5RtSm0MuM0ID5Y7gNStTCRHKlNqAv6WE0vJVkGI39-FSrd2HhMuqaK0Rx5gxTYXtrdXFfjTiFjzMHQkNpTd5EfQjzAoSR4NkX23BERNSu9zPmDnuZubUVDqxFlJC-lSHcQamZiOy7H8c5ytBcTVPLbVIPXnxQVbwPfigwHts0aEgp0LKtH22kEZWpDLu_Hxc6OpbXGVJE29HlF7R_6mSt0JUQoOiSY-UQxD0mwWHYy5H90elNtWZTPFxyfcdnZ3aCI20D0dEj3SLSUeVGbtD4W_z6etS3DNBrjgAH91PV4Chgo1V1mipMcjpUQCh6bilvWm6OxrKRrJ06mFrApa0hsCGptYbNVpT8jrcL1xuhWT0c9HvHrhuIe_G4CjCWOAj9hHIWvmB7IYTsveIdytiCfT58inkxZpiBAiVH7oWzMG4NjUmcPJRlJ336ugY84FI5KiN92ZYNxb78u8anJxa_FU6WPITAOfhFGIiMLCetsxHlGaVaBjKktXZRgwnA8sdOJwLPfOnq_Vp6U2RoUfsACS5c8hQJ0nXVYc-CxQcwOgm65l9AO_myk-KGn-99n1rYJmhkBR1C942aFj_sp4E4iXeC0y8iwHc4F4RQxbru442MYEA7Q8ioyqQPFjyLfHJo1t4gwpsPYnEkIud_ceROinWjy_Uzib5jw19T1xfKrWAZlYB64Nml39aj_C4WHfxZfdEU7Gqh0DCznDolAQOLYbAO6OzvMUzBqpr-y5M-tBQUqbfQrIyGTxMUDLXPBUPCCSpPk7eX9EqzT0xpMuzPcuQg22l8Bk-p6ydTakUjiHQRrb-_SmSfr-e88vN7zFvCQ2LsdJPbSaZx0Cw0vyLnmeCwla9gDb3lyYCmiGHj4Tluu2KHEYP_fiUqbDPOlwAQOAxFHLKAHYd2bVHUKrEAwNu7-qGs1--7gsxlcwa6oYfYHZFv91T6-KkWLtYMCB8sKYbYOraOe2aXGCJG3h-Ds0geA-sEO60HvnHwQvKHzG52xuLZZ2LVYWnniNUfpHjOIABduG_tZbg7RCxlj_Y0DOL8May5jTmwCIIMP29yOkSrFMO4l5WoNsLbQ4lWyqwVb6U7hNuGhVxT5yYuMt260Xwq97Y37syDOYr8ges5UhWN4CLpS2iDJ2o_NHWY71d3_0pklQbEVIQwJixkPEdOGfS-9mxPdS6wNmze7xPC-gcZ5qQSXQRIc6GqHA9aRYzU9vdCP86u2m5rqSF1J6qWHHrU6eFjn6RYU_OXfP1LBtoulyKJ1ZNtpk1RswxUL6-8KNVPVcXT_Ua2v_cOfjbDac9cM0pw4r50fkWJiObkAOpMksvyPiBwMjMIcusP4i4qhpjRI5pzObeijGBjOfmDuyPhckMC83JFL7Icqwpwb-DNRklPvSdDGoIw2nLyaDckggR6lD4ByfpYLDIxhROc1fOXFZNi61vLzqPCn2NF8ACfhH1PE_mhDyyvVlTQ0J0VdPpYtv5Ezcr_re-j0WvTP_8i0QhqBdnL_0xet4XMW2mIB6QC8uKnHK9efoOAT6IJx38bXcH_cf6fzoxD3LpxgPYT6fGKViZYGLJUa97dBMode9ycwQmOixhFzG3DWvb-mdXjbh61UxAqA9qW6HIxNLVBcQ4GRV7wQ1KkoAuzgida_ZF_hm_6D2zf_M20pIFMx-TctA24_rRF8ensGe49_jz7v02pzSm6O_14po95RtP_3AW95uOXonV1EeN3G--TLp7qTWGLDhm7pEDG14mgar2O-oDVCwZE0AkZwqVP97VDX3Y0rNqqwqwqEiKkB1xftvBDZo7Xd1ED4aJbS7TJnKrvUXWl7Q4T0cbFdDHaSfZi2dP47U3rR01JYKhzyb6vbPBvwEeLu8gJ3ItZMs5QrdVO7GELALGbWNL5RTFxamLlFy5eO4fH-OS6MZwh_FcA1yPrDvzrHGK-Ib7aJE97U5YEA4MuezCfDVoYDWau5SThHgRCyPD28O3PYf4ynIUtBrd3H70uN-9Pp-FlfXqjk_tmAtCAUubElcj9Gq4kMu7nExcqpNlm1pmycGvJqohrNRja0IKtiyUtr_Q1awGCw2FDvpAaCmi47tsdHttUfemP20e7LzE1l86-toB0fSdEFe7z1Jz4nC04muznsdo2qPQLokLHhd2NuWHrDPoRLnhHIppkOG-9w7046Qled6fTxykKHl8ki30-_lpoQSzHROJuKwPO1wul3i2AgdBNNVYxrbyB1dfR43vq4-uHtQ4TdSluZSeZuUmLbqgDYAwCtHRI7xNrN9mLfNxNpqrNATOOxf2xB9k5msDQclq6rZKL2aubzoIDTDKPce6f7M7UfTsnidIhMh2uA61AThdia2pU5K0iTm_rRMNJlFGeQcEf7d9hR04xq3lCk-DEkjlt_4Oqd0p3PddINPxtywZU0eSeqw5cbe5j7EUS4NB7xtF2yJSuiLQGKmIRZU6FkXF5Ul5bPBi3IyHnXQ_fP6ovPIupT6N3K8mgUSpgw0UorEahkaDNkgg5ColpRz-EbuobhV5wuLtQJ40h3otWSbYuFVWo5If8LEfBtKGNqNz55MlqDWsUm49lV4p4sFOVrgD01XFs3m4YzjKXKKbk7m4ah2pSevlAm-EVxtk7GiYzjaJ9X2rHtZGu6lw0X_53_Wcz2t-OSwZWDCaVI_ov0B4sul-MKH-yL8xHxhvqIjIWOQZeYnBlK1fJg7qkNFHClVrACX12jSX7xeBh_wGkFtfpkzkIgqg9bMH-QVwKiyftVWaZrSliBPn2EUIFzrQYNYrlZ2bTB6zXvIP607Cd7yhHT2egwEuGA46tmnhQ54Kx7cj_4fPdsEwtJGG13raBTXt-xl2n03L3elK3vQMREDjdP1lB_OGDxYSdsMaeR331_tswF923ZIcfSJDtw4vPArABngMPaiSYAUftfWsiM1hdnkjITvhE6Zo4EoJt4fPxDWNcMTtemeMobWvFLEOb9uLzDrc_pRkERZTi5aOh_HLeJi7YdBD3SFJYAw3bBV6_6q7FV-Qs4385OMi4qsClvF864jL0vNpJLR1GzIdULEPwwTkj4bKy0N-0PLFE_fzo0WrmeNCa8rxYgPxUOtso63AG50qGrQOWOi5Us_1CoyYbrF6nFZYZSzNlfDXL7h78Hb5Zz31wGIQUJcK8IJwoXsHVgAsYlzGn50c9omiqmuUgVCqKCbGrnWNr5Zy-1Y9sH1VGWfAmHhAEqAVeSTCgc9cwZD1IoBMBLspNVwNAZOKYdw8Enx2pBh4w9gCA93tk5V-1-qHGxWKoJiP3uVMYOWZ5W59iKK9yHAxAaYVzqgVbOBlDFd_2h-XIs_1rR9ciD2MPu8BUCl4-312xf4QNs2zBhnT_Asv_RKZbExlKXll9xvJaMqJsg92aviLrWsQzzDNEjOX3VNSyUkgeLRIwh8JMV9owADWfTtJz70J6CQAKdcSYBlFtn5JCGBXbyiKpjfnzTRdemJFaPgOEXLtcnwtBQ66f1JeqEvQWU7auSxRDiifyxGPfV98CYsagiTe6kyQXuBVQNpCDUVPldow8VK5vRYiNqILXXS7IqE2FDNSkTuV9xug_f4Kh06RGuymfGvqvRGyF_N13RsFkdT2nwKuilW85dX_n55hwLCi5wZvZKw_cCSVaG5Ip0uZNn2FJ95kJ-MQIR5guN7UEg0BrVfLY8kgbgvcCSlEqTd1Ds5E0TGjRWFX-BExk8foqfLODyMVHexUf92Kktv46A60dxTmC7zwMwmRUlG1_oz7rvGzffz8oTvY6EKMFPOtWw6qEcgVbVjY9_U1mQGLHMqThXJT1wpdn8eSdM2jbdMNKVoIKvDOh_6SbnRLfZSIj6JYGMmNID78ZHkFLx0HyAXj8zNaUtwWrsic5urRkURiiDA_tGFxD2ZoojqVQPLDa0J1YNwPQ99dvkv9wfMiv7CuPjmJOjb-2qubMu0GEM8odi5GCItlCFWJfBLgvYI7UWQ8GKuva8XwUt5BF4rfrCrBHR-c2Vt0_h9_rU6vUYKWe63jGNabOWK0N_kgb7eTKdc-WZdIYGQDczxcdrUPhgt41kLwys6A3Aq39XaWsK1pCZ8rURSiSDaU4tA-415e5BHW5m0cvfR3prv45UOoZA6JaTqVsxBXs8ZsLpFMdJj_EqWzUaJsChuPGWOcnml3Id0Nj1MU58n4REAsOUTBLuAelJghp8TmSlsD5wT0-s8I7OE1PGsMt5QpE7nDq_NAUjgfRjaYEHD_8_WZwARBnhnJn8eLitJFmu9u53ToYQjhG7NGfvpIMMsfV7cf4IJ6KFlEKDt833lPyH1T6AK0kCOhDf7x-83o2hFVFftIDlDgi6JpzotW8eiIgIXGpMIsttsT-0MOTYS4qHrwLhcfO67ZgfOl6BQH2fI7OrX8C4DVvzKfhSoJPem1scAvqMLWeyydiZv7Hm3H1Ivt69oWDL_eDHXaUjo-MVSMEDYgnavtMQA2jFyZFT5l2sWUs6OLNPrmLN-fITI4H6QI-apTHnDY3QRn21tSIkZotTpwL3F7yHvJbYAo-AXtF2S2V8thoqbMmHSmJD1rWTzDXEOP2DFz0NFd_0mgW2rdXur1tYmQSA83JMGMJqx5qcXPcTpANxSfUHu2DgLv8SHx-lOovYmJVWeXWH9l6ckkD8Ln_pCng7muI2kZNkwjAfNsibDaf6D7XDC7uBobEkvieRSgDaV8YMMl7-omCZ6Or5PV7eYks0ykG8e4wI1R1Yh0FxGEGFy0G5m55mE_N7sSKhzSJ4SyXOufkiGbs9b61bwtXx_h0aQwOKMI1nRNgrRt3UBgH8kRBRTe1J1HP3Wu1FUJ3qKIPS08YNkiQeqPts3wjJFXooCtU11GEImNkMRaWiVNO1mcVC1QsM9SPl2U6Nt0sSKkOvKAlKuYc30eioH2IJyUXIKqH1zTnU8XUmIWKh3eV0OUXOLKZKjkkUnjjU-nRtmo_JarsxuG0w0gcStdWyV3ZfTcQWMK-TuM0wPb_pxMVNU3rMzZJkYOm-4-4sH_KfurLu2ctXpovwRKjoc2fGu4-kAldJMMYWGgl3KsOagH0Pd0cD_vRUtzWG8PTyNQ4jD22aXwsleTU7z3WNEG90y8vDXJBfQBGmhsOPhceZHU7Uvu_N9qa2QX2gjeR8jtUcAdosrDOgX2XwNnJ-z_cPMA8RgHpn41d72WoYco8E6VbJJPIeStP2-DEfPZhuB1NueTqU_IMbYMJWgMrWvsqkNoAicLWQhgmLMmn5Zpl2wvn7jtCTGDooalsPnnvl0QyH03VyrgyyKGLzuTMc6VgRCW-IPktpFJer40A6j7pP5QNCmFAs-dkoNLif-FWZZgHqyE6fZQ-_oqUIG7Wr0yk0N24yiatn6XGSIUBxZVRvlaXLbh83Fmxbnu4pGIgppPjvA3W8fDFksrn1BVLRslZDBamKe0FxqR92CZZcYNOnTlwGPM8wS88mJhFagqhaXlx6WqzZJP3H3Ie5yowVnGwJwcz-1_dmiX_Ra8yGcw_V54u7VEG0ibKj5JA9EO8tnU5I4pvVKDv6YYYGs8ivYMKJlbxNOWSW7mnjvk1MC-iUlxyJKwjZQL_wYioxLMtlNo07QCTPH5hElRKcRS2yPg45Vjujg2fJYj-fGU-y6XR3W0E0SPujFV-nwV69mBEK08ASTlTPBs2_l1BLsO74N0-QIeokJk5kKCVlEw9EDkBzt5IEVpVVj6SCLwTzL7PVsgSFm-KzmVLpj7nyhfa8sOKG40OY55rpfmjiQHeGs_qorL9TVA3KgIX84sFAzIGlM0N9gQbXFeoPmrn70KxBioLoEyh96bfG27BoNZw23RSoxI3KpP3qUtFzquC-15MU0ida5lcYVw4pLFGVcGlK6OZy6-5SW81lMgOfBEbVzy1qklGfmfdImhcvVffssLqjWToB7Wmt_4v-JBD00Ng8-7GwUm6YkmiDdRC-cH4oCrEU14zyS90XTYLuDchiNfDG-YJLTcCiz64W8nDkLQiq933J-Oz5-CEli6CI2zfFOas3ios9gS9xcwHW5roth-ahts3prj5P1WxJlCsooY25bItn3DTXBYsTv8HDbgycBor2tuT0ziTdJ6nFDR9TqWGdd47fy8kELUGEoSkQu1xNuwXYZUvNvdJBP8twaD_1JIvstt6UtYGI4we15gPZ97IHgz2fGDgdmEYTHLKQrMpIGu0DHuHgNZMlPBxP4xO-YROIShUeZSuPeNpb6tk8-3W6sWV9XWnXS2QwCD99PQdV49LMR1sjKo9wb0cxcl98nB9SiJkUVBtzBjWa4CXoMqizOVGbbTUiPSMGrAtRUywODHSsX5BXNfs05UonSAC54YEgXfrlzs_WoFYCiHvM94bZF_0c4Yqe_XoHu-mZMLEEbQ9nkhxeV7rZKhDCHxPUMhIJIJMsOxCX67AnY4h20BWRmItKaKFHxqv2CzMqvLtzs-k9dilP9y-9roGmKC2gdZZni6ZuFn5pCsYixyOTQsKM8tYMpRveUz9bt57rRjb1xCe_sdVClIDWAa6vxklrI_GQ_xTLIKTKgwry7mu3XWU_qxxg6uKjpJEje6w3y3xN52_WKm3y1_sXjbNz3OTsb2IvGnJw5eV8BxQweMlMSOjeTYPowHOMwo4rwfTaoXdXra2RknvnbRUAh-TMycsG_HGtN4MUXzdLDt71SLo467azaMkF3S4HWWCIm9PJMOouszM-C32UnwC44EMkoUEWCN4yBcrll39jvRHMeB-AWE3aFJ9VmQwVHm-ZYTseLd77NZTe9qr-vwtnNOGA9ADxJTOk093vZrJhd9w-LepU8p5VGVPplUwWqUb7QcdsibqHMmcJnhosC_TLG4tattkRH42v_s37EFssZCRK8vBqlGcqYKLUFcPEzrQOlJqRJkf1IVQzNZWvp-DdYWFDZ-VkxyWJAYT1dlyd59pqcmt4lMoym0FR6D7w9lcHNtePCuZwMDQaWjVtjloBmQ_dnuD0rTDzpCuWVDuXYprfBtB74v8XBp2STsyHq6gZLZr5mmxzIJVzFGHEl-G3xyjKZ3yioe_L5aPUXQ76dz0IM2lZ_cx34ZiT2ysoWF3jI-R85TV-retuXpwS1K-ZCtndNwnsxFHTKwQyN-9Hp3GUoweO5vwbzO6Q.Vn7_b5GyazRU9j-OivopE0TYYXTj-l6zQDd0kZRJziU"; - + const backupJWE = 'eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJlcGsiOnsieCI6IjB1WVROdEdVOHBmajIzUHlsRkVmRTNaVzFCaFJ3R3NCVE13ZU1NSDFtUjAiLCJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AifX0.WydXcEk9-uHwtA0YZX_XdtU8XjuM1Nmt5h57aqJG4S1ICBOyLDTo9Z9G0Syfqm6O1oaKim4nYlHaOH8IxmLaz6RAlLQck1Tm.oiGvnVUY5ZhbCLlXHHZ9OA.otbhQOrLTPvHPbpy2Y0onMdPSmGkMLWRzAAqOVspEWwp4_Nh3_orbF45eOR2y_43kbzFr347vrCDGG5YahCl5b8fxtedpjA3MV-UrncKnCGgiFA9d-4s18PtdYlvbORt9p4QG0uU6ICNj0kzlSctmd8JlSlWATiHippOxrvAEZF3OnhM9mYLhcj7aGefgObBkfaO6qVbDOOIKN4vhtPHWW2NzycqaX1GD0Vvyz1UaYzphVXiq2tTGfYTIrNJ2n7t7sj7maqLOvJrBzTFQKywR01Q6kELpvIHewOIZZXdMvVam0t28y7iJZc8_M5RchDQZt3mmwCSQcHs9JXxrA3lDtkNLlDz45geYEQGzbBGMFD3dn6oXdVXPc3DKVKWi80u1jgFpuJX8ZSQQJQmm6E6Us-NJO2K3hbVq07YakbefLvXuw5IBh24NSxojnC1slEaZJ-33kHaJx-_QUmBJI-1W69XYcjQuztcMMM50GRjdaplcTeRb5JOpJu8dimdf1UPxyDgiQxirTsSWXJviRMBHkhmr7hSV6n5Xjmj_GaHYk6tTxen-sWNkSkxkFjCLq-p_2i2Qau8UVjdblG2y4-ckpoD_mjl_DCFBirQauhaAi49cZ5J8WZisIT6jUDs0Q4-GMb_yXjYs7_ksr1beqIUvIR2fwbTZUBdFvYFO5RpyMu_LnzEtYiPhsn5uRYwVNkkp-jEXGcucl99bsVp3tZlSLqDiDjooa3WsK2obbcd47pj0ZtSFoW0OCYpbC32Tn47CPlPb8PatoREwuxZyMyGd3EQYMTMajQ5lrQapv1Q4qprAz8I8ogduue1W522vTES8Yl6Dkm7ASsgUo5bOXkvh4iAaBnaZ5fBVOKUubcl6Acu_-7b-bbdPjkgesdq2__gOeD5ESkMDdGYYpcq92Ha4g7Lv_kpIs7uLvJfUk0ziyEOMjmSwXaenCdmWHikKTfG7Rz8nKwkCg7ZX_jwUS84mPIJI-ZVDEmMtis5nEzbygldfi-w0KlbjnhpYm3o9lRVw8-dXBZTVCEAMLRDIOANOUJp5j6e5f4zBuuMeSW31sG29jLFDg8qBAUvEg6dVPdCJjIuGkZTH36RUz348OXgUiaYFwkAPYH19zjZGGU9DzepL7l7t6P0Duqn2n4Tc2DbPDucFFTUkRneMCGTB-Z9VvT0Gn7j4YTru-EcNQ6pZyvv-eUOJUDliSPaBshMfJHOucUoLoQkbtrKXRONgVmmHzn5rSCXYG7yFh-iGzcJKg4AUbtty-VuBb86CvQxZ8fTdm2IvLG7qAq3mgWU1yF5VE0yF7c20pePJRqmUb9C0HcNT-8OX1XjBEcCZauKT4RO3rD62BLMVRyUUGZQoRJW1p3U2SLDmrZiEPNcnVKCJgeQnRvxi_aYUc5hjt76VGgDYqPhmyh428nKAFIsqaJK9IpjDw1cYhXhLo6EIoyvBt0NcGY_yR0c2v29NJ3Q6S74ct_GndERV6xOsRyhr4odzeaVBI94jVO6DPKQl7Pvwqf4BaPQnIA7xVV14NSB5aZ4xZv20NJjG6yepx34nePrsZPSdQXBK9McXVodAIF0LpuwmOpoLa34z7cAPsxw-297uAjj7_xqDimC90g00GXzTn_p2E7cjZPrQisAkkaxuuDbiNBydxCg2cJ9niAqCQ7ggxDjuOBuaouQBrgBlNh4CcWCpalE_xPvKLfa1ooYTDJdAWXhZpt7p5HdYorIcC7GOZpn1FfMd0UrI8ymyqEUMJVUNBdLbFbkd5jOjFpUjL3jYSIZ1GfqJek2ACi-8ddcubXmFZtN0Pu1CIfJGLMifJn4MKZPa-MJB9wEaNoOc9fFV4kYw1der3WbVY8mDnhdoCUCYYYI7dsVd9ul9VzLHf9mctD60mHpnwj87E5XoR89i0D3aWUMDhdYRhXw-JyRE5frmi7EUrLWWiStsLcXJK5-TkOFB_U8CDbHsccc6UuAOQtmx90FDQXUNXjM_eDkBdS2BsInL8MYi24r4dKLrjwPMddvqr4eYL1rILi4vW8zx-RSCuPvF0K58GZ0KotLf-o8YyTQ5JmO1AszLahEptMqGWcx3lDGqqhIghuxD35fr8ub9nNwpF2To8AnQDNgWgWGhUNt2clkdvK2GXv3hpcd8iaE4MJ83NAt22xaKHBPF9BfUDtnSnVO9tT64GXh5W0dt3yNVsfA1YTlwghBrB2cIapTDNR_NJCfJbpLy4hLH7IYKbN8OiMtmj2ZBJwl2mEb1YJR3fY2s0z_qmXO_DUr6OZwxQo9xRbHLQrghbzKJ7i1MoRkIlpSrJmmbBJ92xBW5hIFM5WP20wkP8gwGEbilj3MZbU7z_7wXJqw3t2FGFVpXl7MPenv1XMQmFHZyiNTgvCSGLdqnSc9Ankc_kA_JN_LHNQhlueZMxkKba9tUbbezIL2AYuxju8Kte-pSNUjXUJSHLqmZNhhUaQ64fSSJpb81x4jzdOeQzDY8WHkHs9tIYveIjXh05wDm-ly_HyL1URrqReCIieYQFx9JRxe7XwOTrEnhlSRsAbbA-WTNS6jX3gFbgo6YCmGhuKzdI_HSUvbtg9ALuuD6v9z2ODi0Qr-zCgW0073g1lMraASfKTRoteNLwhM-9H1HMnvMklp29OoaphB0cdVvRSudFkBhMynlLOPIxbKv-I3eIPHMVevGbxes2xXpKmvx1PxVcasD6_uiqejnLndYxbRlH5DGtayhnprFIWShNi7X4SZer97SP3I5CszDsVF3ycJ0EHO9mG8gFDAIDKNB1HjvYF7vCLKX4FsHzKOgtisAFHuQ7da4MrbtIQhf22JTl7D8iMudYdhCOSHYz6zFNonPx24bkob-y-WtTEVg559_dAlbD_cC2NTUSHOiamgaQ9Gq0YoznUG7YOT0gnZoa59-2lGbt6xRkDIM1YkMvscVDBkiusuPTAkm99MCarg7JvIw7qSHI9dIDFIovDjaUtCvB1hQTT6lPpNwUJBESvRFa16DND5a9kXiQ2o0b-0qFM2hKB-jfDoH4Ucb4WI3prvbAqL_58DxmtkpNVAuerNwvQYj5E3VRmox09LD8AUqqJbWUH-5gP6sqAbtJUs_xOJIKLd17c1hDKUCoTYs69PWQjTZD4rt9uqPoa2qR7_m6mj83n-45kICYH7MMH8XbDGU_HPC4EOoGt5xWzDBzG_6UlWV_QNQ3MDSB5BmgYyDngDYEDPpKyOWULfvV_h2N7ZkVR8a6gCEpmPOeifBMkPCVUSZ-x50AE47TpIDIRMkVG2g8Ye-dG4uWDhXUo03enpumTseZb-xARfeoseDgbNK6R_QxJ_z9K0SXlsEENo_eihGJGVjLGvP5343sJxjta5ZK9OCc56WSu753yqC79ENiNf4axg9G0xuAAdW4ssUHE5X6sJPxd1iqfcwlEuRVazGR4ojgsF2k8kFC1bRLtYTzL94ivfdsZOtwLVJuyMEiaIDO-e0qGxtVF2A55YAIxZGo1ah9XZArnbPqnBJqeEPP4ZZino2d9zJCKAYugrsQ3aFkn2P2RrYuf_30nCm49075Ce3dqEBocaIi6i1r3px3sutNZftKaQ7gsJwgBtFvoQ5NnIQHg-9D7_bXVgLmtIsl6iak7oiiy8tJnrsz_iGyhT5jgw4FmZaz86V_B3A6LJHNci92Y5x969CWx9Y2lzR7wfcU9kInmVjmFQ46K2K1zKma49EEBzJojhouTal61khBsp6v6qchaxYOo8ohD9w6ddTcUy5yK8MWdPrn1g3fkgaeXMhDr7DMWc26ldujj961s8K901TtszGNOL5Z9tGVn9T7tubHusjsAfUjGAvs7BrsMIdWY5QJ1TlrQo32djQ7VC8K489iYS1ZIQ0NGOoKVHvIZJd30YE5IZN31fAtRMWImEftsA0c89R3odjY3xgTPJk-ATyGjQKRaztmlrvKpZ1TDAXh0PmtUzQh3Flw_O9wRlaI99SLWeGzv6yFxRSBUJj0TDAp52kQyN9985qSNS-u441O1gM77JSI_WbZ4vZ3OmB9Gu4oR9tKzFs4Cpe_dt8hhtTP8o3v6qq3-yHSfNuMMZkNFFAku-sLLpCyyzN_Fhqy4HnDgevRx2ZZUXjjwW335CmDZQPo--x4LGwEKKACBwzniIHXubikXB6V-P0CPzB2pzUPr1ukE92HLWAwd--WCIt_gqsdASiRc2WclLUw_R6g5j4dIkqyR1JzgUVDGDRSnsngx4HlNkptuIfBra5B8_V8uaDYlNZFzTivt1xfPd-5C6I0dUUl0Lq4y_P2coMyzQcx8bSu785a3qZzrfoVLSmdtMf6goOjwQ_3nk3-LJcOa0HfT3-a9xewmMm-OcUtnt85yrF8fvzWh_N3F5uw4EF1bRB5w-lNJfSXJFCOEe2QtRKRJWIOeBoHWUPTeb9WmaUp2kL0QZEVKoq61DYimbUe-_nPAvsVz9pN5saei_-8p3AED7aQiW0N_amS9DKuccwEJiiraMt8VuyHpc5CvpfoFRPluus_nyas38CnmJG1sFTpLfDRiGc1_wwujcBnWd_Du0dIz6AuZ2MJIs3IIJwjO7HPmZZ3A2gQw4m8JYl6DSg7moXn75UV_FLl64-95PDQKFWGsrcujHoyuTg8S6KMsdCRknnQZOqM1lbWvsEjgQURMIHsa0FcExMKCdsn3roJaF5-_M3uCOt8GHwr4nfsEDi8Zqsj9Y4WZpOeazIOwsIkWeaLTUwOLWjD066yRtLtl1Un8yixwWg-yWJrp67QDkUde18YdlzUeMqhuKAzq4bEdScdrRtV7RDlVd19uuJodsphLhMiZDvmzSs-YfEhRfmwOWF_IytWfDCfb-o8EGqvi2nORVoRtj4mCSRxNMPlJ7lJt8F9CtnPr2xqWeJo0mahDM78tFCzMREiEQ6J1Xi1w8gTmkKB7afxHFIjywW193b9PqAvS2edI8oyC2TpW-EYjubgaLcTfK6AE7zGWusmFeqmrNFDiBWCrtrhp2Mdn_-PfR7eGiMIBxpUFLa50Fg_oYda2U-ak3VRuX34ZPYYBlRbkKVzre_tF2yU-aRV3JdlGyvNB-EQX2TTw491ry9dbeiVOp1c1QALMYHdhq-ahPtcr0AEG5S5lDuxDWUcsrqldD_ufARcdLVza__vgI6Km-aGcLLTUcY6RSQHN3ZrJdug1CLVS41nvjljXbBow65aIoofJ-vu0QDxoUGsgN3JF1TLD5qglJ4jEnV0PAWso7m20fXjcU3iOALRSL_tG8onFWtu5MCY2V5xGlKTGYnmwZaI145K2iJh1XHPSgKprhWrRRS_zuc766KwU-WB31JFRwfTf4m4wQrSUFTZNrlFc9j_lNMRxe97G2fp8Tb3UhhDSf741Ut5lbYDgzN-UPt4vF6HxfCeyZc9ADvcysAUnm67jeQZxUlIZtTMQt1HBAd-zlWwCK1Y_111EzzVuPr63LdSsLdnm78nG6EeyE8OW3W48LmOb5CRWkrFMbzHPaRbLcXu2FVSbyPZje0Yu-uJlbMIiLhCfHsiYGa8lOxsqTmhUm2dYqd5V6JskYa4_BDe6DmX0wVVoOYL7uqYn-sxGt0qrEvWSXhtrrx_0hfs4b_k_cpMX3rNdHfV4lReAGUXDjmi90rnexavUx4ltF7EZlITnYoGmQO4xdMgt1n_527my5k_igkAalECmtRyAuUA9Ln5B3aMVBelp0anXvLKOBTS1HggXciv1nGkAhcKmKWoTGqgHVLN7d-PgjuRWNUGbNjtKqJ1s5ddlG-CvTDqlimUEryrfN9Puvwr2tmIMPktL0Tcz9_VNKHDTSIYmYxkxq-ZQ0LThggl8LK7PBwh-4Q__VCe_VERJlubhJjORc7Rv8j-NVzFEMX5ykhWJepHCiKEh7lniK21zZ63P8yr2w8WazNR9q2gdbcOkZ0wjEg5CTAp7-UjSpYBJCFsp7chQe1ZcSW0dgTWjk-lxlgik_0xaVJMuvhe9viDEg0zxMEhccAmu5ZKran9RaGnoA1hKY6RrPV5U6gxcfw5AWv6VQoIUHpNv3ON3534v6VtSNQUyfzuoLbjxLZtuEo1lnE6o3hLwAAOB8q9qAR4cuCwsTlzwv-vxhGHEc4K_PE37G5wp1xQUUjErOvKBtCZhAWkFq2BjziBpN9UyniRT9CFkN8mvtd-IURsfuql66FXYE7Pms7woLfiLYJdnlaMiL4JHMJk_Mg2j0US2PO53kR72idil88bz4WrCHPakZF7EBecbO4XpKSsCCZNMvBtu3ipFpIZgS2okwjVI1TiV8BTVC6Kvv24nbaE7XNZNOWg0zoiWmSzAnwasBiadp06tHsrr8hdE1DmIeK-xlJTsMxsEno_7gSuaDwhTAgiDVtHdQzJxmG0GBKFl2SduimHsa5P_y3Zy8RvXdsNy3GHjsPsMpF0Qy9wp753C2mIACG7pW_E4Ic69HAv5dg6IODbEhzHEY92BxXyOlmCPBO5f4ZeUsF0rM7RQPu588RB56bU2cx_2egz8M70f89lHKqLCLQbgTP8YOB03k237hrlE147M28JdA34ik5B1B-JvPEWAO9jU1ygbga78OsDC6Q0PQOv0qVMyWFHuxL-EsA7E5CUWD-gfCG7ff00uFEV3wjr-Lyh5xKKd5btCsgXYa0WQn944unPN0oXIfFjcDehK8DsM4CF6o70R_g2AdBYgF_FZXOfY2H5a5RT7mazwpRPxu2JP3Oo_yNqx3RHETia5NQmHVGO2RpOBQdk6LuaWapuRm0_8YeAZ_T7PMB22Wu-n4xiyl_lX6fNy2KlgtBHemHH_vTDx6rIwfcLCInXbp-M-DVlEOJVog8MqjaHgs237nPSpTJI1H2NBwEzpjFJE4GjXxQlhP6vxM9IgmT9Fo_GoS3KPsVPuxGmWIhvq0RKsUSObkj5WPhaQaHfRfagzBvq42CSnG1hlNWEpTFBX_ZuBzsTpc5FoKATmwdOeMGUwTcDLH8Cf1_K7SFKqL1FXbsT8VtdLx6p69XX5Id-6Xzbh8ujusmBRm8JsI7aFeYieSHcE5GpAOLNl7F0eCD-pdfHcxTYHsE4ccVtQDdiAFT-jsAkB-a1BefoV9X9dmrLVCD6I8JW4P8JQxOZCHx0djLJ_nd_mTN9Cqn57RCqGF9pubywhJJaHhkj2fl0rXN3Mgl13xdZ4AWK5DFAa8D9MMMSGLcO3vRSKi9Qzb0_kMb_AfIhoXXWRuKOTVM03iN8v2qCScIVdzgwhKxRjlV7hhQ34aN4F00vavJ9xzfcPWULu5OCMFp4IZQ7GjFTaCwtRmJIs1DdKD05827NwEggdtPmCiT5fomrlOZT-XQyBHVjOr5eTfyeXK6B3d6g1XPHmjVUXpKa7XVBOL_yuxarda9iflLa3qZb7dPsnv3arkKvYIzbwx43O-Lj-xnBrC3JDVpI-UTfVu7Mq-2RVaZ7yMj-sYx8_bUVGLT0GLLG913XzHbi9KntfGV7H7eSTiRnMK9FcVaQWEr5_1lPasy6PaxXvMiGIMLQFR7KtBHN__gCQLABIgWStg2RpLxbKYsXeXWQzdJcfxfHRrmLxqzV6RXiBKbzEt1jhDtQ37sflZg8hBFNIccojt5cH3M4g3jD6fSMTseHncw8luE3hn5WL9q_9EWN3SFhfXWlwVoeYDHwT6blrUipbboL-CtDZCeCa8ran2ck9tiYsLi4sgBG4l3PEdKMgngKhhzHfCbaKa-Zcv0GqDxTCgIaGgUidje4XV_4yKpmW3Dpq-VpAk0HKQZ1GAw3s5qAIBxU6ZwcKhqTw77CpPUGuqECugmxsmiNhWnqFe2ECUmM3zWeDiktGfMWbBLyFVAkiS1ilVP3T4SFkOR54yHfN_4nNXlX_SSaQh7CzkxQ3NKM4mMJfwAEnobOy_93lzCZfOQp4v56ciWDnkfFacXxvGqiZc-Vvk7QasiYoqrvi0HWVGHOuUkmaBJy5OO9QObdF3GcactCmf9lGL81g04wwGPKviqhvJ3NSY74DSUg1yoxfrif5hiVz6SZg4xLHEDI9KLw1ULnyHqByycYTfi4BUQ93uGxJC0KA3STUN5b_gnS6uOsAbKxRVTazjXlHdWtS3k--NVM5WilsKQM4S-o_nE9gJBKALcQ_fXyrUos1bzLln-yEB2-eJ1Rhl3c6kFAiXYvf22Gy5SE_er5mrHCDg9MTV63955WovUGIUouoRJEN-tLOY1B_fa4b0gKWkFP27OB3rdFIDcaS7hzsPvLtfKFghrwF2QmcSmH27YxI5fM0ww-z1edYrM-HbzSPOaTZU8gMapRzsthoNmCIsLQo4rlzMYcpiyIMelscLnJ0d29nWWGdnpUP1qHD1Qn72_JJ2x-G4gD7JFQX9s7PE1v6EWMm0vj6cwHTGp4IgeqgKrVieCHR7Lq9br0cMX7cQAjsF9Nhao4Lr4Wo4m9_mX23vyi1Blln70FmAi9N2Bc2EF4VoeKceOENFBQumoH2zPC6-TM0NAeYG_27680EfkCXc72-HOI6xL4mI9TbEpVZvdfgreHS5MgSkyg8NcaNP9fqnzvrmjh2fosas20J8UjqNj7qeZ-ENyTVtuZAPZWwN6XMImo7rJCokXHOzdc0aZNEfRWD5wzNq0sznN-ejmyJxd0FPgg6KW3i4WH0UokWoDGEvDH0xFnScWhZ8T6q4_CL7F4iwudwPp_R-q32zSx6hOC7vcOlWIpvTgl4IiBJjl3nxQq_avCqF2p_CACIOai71PSUSg3iHT8I5NXgG4ZjlKrLYbXuJSGuIwu_JV7x89PiDjrQx8D-E-HNAl6UUrliz-KYtXbNcRrvd-btCnGEj9J39LDTZ9Tjx-adhakOeqS763S6HR3IH6MhrYsw8S6bYap6ai936KEz_gFQN9teUjSegQ6pwTe54FSs4-hvDbFkBQAhibctqE-QaXED6uQfCYINza-Fa3XSfCS-cKNKc6gX350mfJfyS5x6QbtGmwWvjDbzSpLGAA0c-ey9G8Yzt6n0PgoIYON2jT3yQDb-Am5_3HxJhKQkPiYVMF23cqOHyYAM_XGVMkpxjb1SgBVMnG7Lz8nS7-Bm-eGE_fpPsIqiOBMIWuy9JU8Nc-a0VciIFPCkJ0HK2US8pZZzKlBUHdChqc6aNYj4wEjzuiCukbrSZ2V2WiS_meyAuw_5VioPxamIeaP91_EYFZLlm0GlMVg-8pRVnZID2K_hZhFx1AqLhHlarz2mtxZ038KB7Vdxu62bn1QUlYHnEBU04QhAdPL5NBI_zuVoEZRNl5pzENAUz9sKIa-onXyDrpJn48iZfKh67aN-3N3eKn3oQdJmCIfTcH0FCD0-M2T-g5gLZmhEXwdFRdBD8pi058Y3P8_VzGjtygSSBUNI0yHmH23xjiwmo0zAuu_dbyPrxi_WwhJM2sRvS6WWKPpbIRlZjIdpMomAxRjRpETVzSAprqmGx9loEcb0gjZ8YpCyJWQbThBxs5OePG_II59WuIyEIlyGSq7mJ5jkVhM_P1t2XUzb7eAvhMPlOs84FNS9LCB0CNFlFDGSVjqTB0AcWWIOCetszvKie8vu4eI5DJZ4ZBPlWpxFxGJqZltOGV661FUM-V_gxZKRXnnv-4CT0Egfu8XghYMnExEVvK46PCNEY8dFokoi6YOMTNZDx8TJLxchARgY9KGOcJDJIenunhdccFV0anR2ea__GRuLgYRFGbgvv8GrAz_R38Pak58JrHHlxHlLumvimCFgpO1N_rioqS8RHZ4xRA5_n_00JqeACq0nnZLE7IaLaYjAJSnbDv8O1C4GqowymXUMit0n5n2516d5KdF_5oNtVzuci_XyyQsQYPRtk1Ue7ytQWZbM_eayd9uc0WkydytPQvXU1xlHMmEgL_WoQqh_7YAby91No0-vuameBldX6PdWHMcbuoMDnYcwXjXMGL3L6uP7VhW5W-zfCs1NEOe6n-YXKYZIctfUWWeXGy8pq4_7IA2_T-uEfguLqWHHGPL-Bs-CJAj4A5sD6ZtGIiWYM1Bk1eFJvYEDSsjdt9PdYp3h5sFAHMCouYf9V0N5wBLx-jTpGDtsnvFfsMln0PWjjaj4yfrSAVLpDoRqZv_f6KIT01qKXG4cQwp7hwCapfvoG_jeLWBr9FfvnYIDuC2dr_hgvU4H891Hmb8tR6N1_4mGcShC95AsyIKBytpN-ezRCxvZ1EdvLc2-AW4em0ckb4sE_4IxcDjrqp52HdnddW-1cacYABzgU9_dT1oVdlP4Yyr3BfWKhMGjnOPGIGZn0X4X7C1Pjiwq623_XScw-AY0x8QJm_vE8N33Kv246rK1FHxBNPpIV49m_2au8rtCBbPwQBznhtMFIlhFUjJUR-nqnEKAEl67W866T9yNyuPDnaFYt6u36Cm31-XLbn5imshhlSTAC4-k_SL7jADbV99xbcfnRMZ7RlEkxxB6_4qo5e4vr0VnMDkOiR7lIbeYzI8JVjhxyYogjW9mMwnXRK6bc9OICbrbu6hv3NSlIkNqGp9WwEM_93rND948RVOKdI30GeG907mU-g1tRHFAVMTCIcndrsgijADJ4bkFkAqafbhHH_in35CEajJKtzyJRYyKHzrilOrKJuwfvLdxbbWJ8-eSIYZxpGsurBQ5961QPKP7BMpmyazAlyjpaI0V7h3Q5vEBnD1WnszSLfWr5JC9SaRh9Yzzx1W6k3FWNlZM_Kfmv3Md9jRnWWRTXgRQ125LGCfEIxi4oTtUFJP5GH99Qw-wmhJdCEDfZHJBotroS5qaOdkEEi12H_KXHQnu5wVTjtH3JBLdmfC0O_qaRpPNddAMkqvZpf_B_6JcQi2Dwn3PsiTp1x_W3RWqdeWZqeF7KXBCJWuP3VJeZTXATbFBjiLkKn087_qQ3HBqbCVsW6l4XDJXkWVozIWv0GsucR6yasp69MtZVs_XvJrtz_aTBiqDnUYuyGNcXstQ4CaaRiY7L490Dc0NaYcDEQUVAFnLzQmuPSV3m5Xo0H0wNwAF_4KhLlvugB7RyKMoRBMrvV6U9aYEz62nMS3bHzNdX9uh6jzVuCb18BNsbIzFgNVXstpsspt-u-us9X5vzUQ771WFPL8RWU2ugDg5JiX2Dg_GiHOXdGNOmcOU7H0Z7-0v6oH-iIAzioOkK-C6fqpp6fKy02oVJBUKUM5fTh85mbN1NaWtlwZv5fZp5H_9y3sQLeu5lClyI95h6gSe_5i2IqaG_XtqB_ZSKHqnXz0iJ6OHtHe7fJFf46ZT2nZkyCWANu-LUMCa7s68dn_aYNqM4pXv2PO-xv7l-Dmq_RJ_qi5fTCfIa35NxwPJePo0N5QY37cPKeLbxvAcOtjEsJzcakcNq3kkrEGohRWKTasAXC19f_XJTa-wf0Kqgs7-EIS7j0ghNHOh4UPJDLXrh780FTfqsrJaBKL5aJUhRvsSe-VcCeWh0g72mUphGBiAldm5VOWpjD5C87x69rLfIRrivlvgdrMR5_Kb_imvN9ybuHV3E3ydkE3yK_E4-Mss-Y0Vjf28g_nEaEn8jxIg5cYNE1FD8KnnVY7rcHJo0yHG7LARYlnZQEXMnounbnQLrCV2ioewHcqJWT25HxLgFt7oH8toCaw69-7PBDx5XCr65QorRiIbLpJ699P31LCvo_8SglI_M3BEUXY526VySEpV27eTFmrkdX79POObD6WtL3thUXwYjXqL1fqAfA-5LA26h8GdlLRN6rrt2dmule10ohHhy3Rh-cH4bjrwuGg5gAMXfhcq3qwPeH76L0iaOy5BD03EM4LF2037gikATdnikwDxjCz1ShdFT5lFzy17K8MhXN0AE1sONrGY_R47n6oPQHrE5qIMZ2yOFcBydm7XJ1JhwTqJjnvHcTOOsbsqCivkP5MDGsMG8483EOrklkMPy2zVNxE0_gIzX2yPa2l0wrTKWzXhm0fh2lDYqxxPDUOwwLCPKozPXTySPJTUrESAyzVpgrv0LqCYP_2Gun2SHsGRyuxOPJ5GSp7WLaaKL9sJ_ok4qn_pYj_SsgFwtoKvP2PbN0vhagleakLzpc9zACiA-xllj4ZaR6oOKyQkUpGPrXlAPCw20ZxfQDpX9g2ONBLqq7aayZroqv__HchageZYm9BYQ0IkztWBVspELMbO9gqZnlT4qK4FwWdGA7M1VptBigwgYt8ajU1gwfQ1vm__x9GqlT0kPEN9tPkLBzoLvb0AMz3id1KFI96l1K_ZW8UVAdZQm19P6ozi_edT3bbGBJ-VdbwTe8wBmGbLhJvaDAT5MOnzJGpNfBJTjq_8odYHFEqRD1qA-KFTCKRPJ58ukxmlENkqAUBDLP0_j-o8aPzOhhPVZPVCG4YNRSzAlZlywn7Gi4qTkKwsX5Ds5GbJttbENDHFw3wfj4ESmYk4QFVeO8X6zbqjz2n9t4kx0IrfUHByBDwYQqPVL6wCESnK_7jkqGLflLEhRj31jJq2mYE50ijnpa0g2rl0PVolB9Oz2C0ZvzPJhZW1jOSqHjnSbZRFZJrkl4MU9eMutKCxTJRYybAdnOP-BEPDbNIeTKlMvKhfi5DKhwXn-HHFDo3VjFJqclETcKxTRYzzbgH1kbN_62GtBt9Uc_VEVic5Cc5_5RHHJtWJYoNDMeByYY-ZXYmFd5SYvizek356etvMHCQ1VP0Xln_t0ZRlPHCkc3qXkWFXIExIelDpckFzOsdqMGY3So47ZM7M9q6dSczOjohyxpaHmyy-0Bzboq3UXP-BySLVEptpHU1vPeU8jEHc8aaQpCMYYoEr4FWu_s0cU8psTJZXuOntoEJ-UzBHK1DLO700FYmL5zkv8eG1cokXj4-8p2vTw0xSSUDQZ3Z9mQLRrugD3BJl1OcPiCYT4NAs-xc_oOSNbWBrCsoLYSUAZzJMIflgfPLTh4FPQFwl4EL81PcXr02iHkonXHhofIaqGZeBExInB2_t4-nqkL8mNf0-C2mCiNGF0JEoc7u0leSryk4TmOBb9Wc9okFxS5bb2I3CliIVkXV1R2DauOHBCpKGarIe4ot4keemI0QLKwHW4dsXuW6WjLfu4b7mTlr5QH8xw5Eap5kFw4YAWMMlW1VkmMpRDs5mQZu1A4niqPss9K0YUCOPa8ky3wxkBUx3SkeisUEQLK6F8p2vyuDRX2qWNpGuoPNHJWGwnibSdoIYtTeNQblLH2uw1cRVvMCUl-sNoWEqEB4wHK0wUaHdYJHkOONxI6sV4LUR8VVUjdr9m8zj3klMYYEazjbZPxrOrhI_t3cF9Kw1Flq7o1sguc1SMgU9CQZMnxG6CM0FTV9YsGocMdxO_KH2AAIqRqX3sUcehtDMI_3gbx8sBm6Z939M1_P_P1RGdLDC153vXeVidMWFg6XpEw_jXAzWSheFwauNxW0_sZKYrXFuGMbB1Qr3LWAUTJP2GvSqAMNbuM9xPLpp-DoO68DciY85B0siOIHDstnvTeShJghhIFOmslGY-EdgWq5--bKTUdPBYlwD0-cXN8dry0rxqidZQCezjvcFEyzOgsNfMzO-29QU1QhEr9Im6T9GfYNMa98ytiE_hrgEeZfQGwxh1ufsDYAPDHiX1XJ_THH2wvoA4_3Zi3u7ZcpEWIEsJp_Gn_k6xODNJyiF4JWDhU9yzCgNI8M9a9PTfXw0TqTzPhNHehEFR3fFhYlg99GAD2bgDC9p_5pvieUt378K9KhMzeSwmlfsX4A2eNYTyCGdfSywkoVPSL_lgUS1pV0FGh-dQDGnsgKfdNSvjoItRrcRnOUhN0_tikqK7BjXJ08V1TSP1ndg9OtbaZwqtxk3nXq1-pE-bJ01UkL0uzedsjwczhuBrD3F_hHMYY2HI71ySUZcZ-Z9-D_KvojZLYm0-Q6zYV-bqHz0b9eFmSra73JQXwUDCt94bnk4oxxs9FYEUdqPj9QW9JHvDV-wpd5zre62WL_gX5g5yHf_OGfxv4rWpvU3AWZ6qagaCWV0yKvPHB_HSJk3qn_ocSLWwW3AApk4heSJf7D2bF2fuwrRrurGbvQgYjMMtu_RyNf0zpJmNKVSHt5VKLl9BinO8S937Qio7PHXi_4bXD9yzeZv1ZaDvo1LYAW2rubSjiBmPh9gKnweMu6hOifqh3gS7K3ii5WgCEkXHsEK0RbOWGS2BbyP0xplsjubj7q_qHqZ2E-I3b297ObCUpPtyHrwhcl9r4NSOLSosAaHtSMRJjCW_9FSQW9jpXZdGo9ZYE11suSPpkj4I4CinMy9WwWisLzAWoRU4PwOoQO3kZnwN3qG46Z01xl2KFupHABGD40dgptKgzjXWI_z_KeHjmPboALC1uzu4k6gmgPGpqkxxTt7NgBaJsJmQBk7GZAQT8CPuwG5hS_pgr6FeX3wmnb3IFmUAG0zyHJXC_49D7ang7YbY5e4zbP6f20_w9F15kwr4rwf-zUxCsbRSuUMMiwPcwZtc8lGE9sb_LR0rO1uFjkz27mgWKQwCM7wommmaQdSomvq5_YDVfkf-MX-9LrjFL-vXz4z_v6Ns9JznTWWjmT55qQNhLbf94MXbunRyIqTwqPmTxqTu8GwhPK8aW67MysTUGyz2lCKTpH2FnRiaql78ULmvqSBO_uvJuI0b3LIz1EsoUcNePf5Gw7_vZNIpge9-lBtjycT0cwBq7U7GkZtAfUIOZ6vtbh1wZ-daleFV8bQiH7EeJivBPd4U4pG7FLE_awuKjoSoUFVnIghwhpzrwHnuBhMKV8IjntJ2nH0xkXgUKBjJjK9x6hfB2ynbmLf4ToBNUbV0wmbFY-hinj6chNQfrk2dQYDedWCEphM9hPT1kgxokM0axVX5KilTTNNr4bgX12X9lFhu6vUEcZfbvt6ZhLu2a1qjCGGaM8r0di6znvbyR0VwmIvwfWJeLXFezSovdEMon-XWoQZ_uDxGDPoAERPP_ExbM8yNCfbdBXtDhayVZ0dyGOS9iSyRnSP-b-qekcoJ4KurOL1Mxn6I3f4oytjSxyvRpYzu-pL-PB3rdSSvhCPEf7FqHSePGSAc6XB8-tjcmHXCHQv5TVd2OAYoyzday-h3zPxPfgykOmnNXnc4y-85oFE2fffgZmtUBkuTzQOtiJpo6BjR3wBPxhsJ8Jt-X8zapwLay3eVuSowG12gNYAZ1rLoWQiuyVXybjO-MaCfISSmP4lV-H0yrbElbemxrKbfkTDUsjMX-TUuZsr2uw-xkEfTDYukUB67v4McbzR6U96qRJ_NyvPUS4b_OYFGE2JiqMEJQf6PxplaQh6avxXU-F_bkBRnD_4d1MKOZnLee7bK3ycd82pvCsU5itcTjKiFx7hn0VOHer2nrwJiQfgDVSkezOm6ZL-4w6Zhekc0G8iSTqtxpv_kt2lyRB7GjZUij74YpOj_ubDFjDNKbJhNOuYgqJSM4dktJxAcoXhckFDfyVqZoI_zvL0_864yBgXR8ptrgUXBU1GIfYE2U_Bh70kXQBttAY3x8aYdp11GwxPYlp4ycPTbwwAW_2F4GLZhpySFUn0jg5ULepbnvpwa_4fnN7soejB9cPBIce0zuk7BV2buBcAbmtCar7_mQMM13ofqscN67aeHhiivpjgQU4lBqtnszp8HitEZUSC-mElke5Od1WZ7SZdsWKnTmE3-ZeEJ2uW8sQZfoNOL6zVzqDNa20zgNZWyaWTOhKQtGKPpYR0yLC1K7DUBto-pNF-twZxHQqxJPTYNYWAwrb4jtgrsL3L4BW97J_mv7vXNZoS1VOI0ir_6B9hLfJDkQVirwNFkCDrvpahW9LfWqTbnE5TeGtelJm8imkY3wc8JOcxTF25YKldxUld1CJFJb6hZMa4N2dyEo7VemHaBffHSs7Wm2HmS2ZCW9PwJK75l7vNr-fJLTSfbRdt3ig_sb0zOE2v14uD7-jkbnHeXxWQ6CbqTIQh03bNaEsYyfvD7gaLFHO8H6kl9pOW2PajkRzKrq4KtKTqpY3VuC9jj1mUCcnWEMum4MG84Hp1D1Rwd4lqnSsH14d20PYGyQbewL_1o82mCVLm3MrH9mnwNy2aCC-BKTaYbB508COm3GV9cTtvP_C0OPS3ypfnIylVb3eOI8zcSAYkG6qgMfvU5J2EwX1oolBJ17Jj02Ia0Pn438bXaNxkclz0axatLdyxk9CFyfnIfWvg9k8E7RMEwTCiirg_ns7V0EuuhWIuL4UByFJJPSzGmXRSz0CV9_yHSifE7nEVZzj1zf0dF9SXrcmU1qknzNqAsQsJbBM4M7TDQy2nohzpsGaJynF2SNeiKFoouztRnEbhbrD9qk4dnDeQtlKlR4XqSJgNpVpexAFNqaH48HhUPsXJPPo10zT6H0R7tCHfN8Pqi41QR_GeWB42eDNIH5AEZiZSZyWhnPKlRG3dXBtvio7HE-Und2bS4ICbC5q6GEkL7h8tMZnDyWfjspHYojrjB16Ia9JMrQuJOlgj7oWK-xyp4FcwEbms8Gw_lTHERCBzEk00WhiFDaMXLW_2HQHWTBncKhDZSFIlcQdnKOjhkgstcVedkWyfbBR75V2ovG0EKavY8PUCGPvbe6tVKCcdmy_UBcO24D0uU6r6B-Q59i_MbqAfFNGtjjhrycDnaT6LW-I25EeDDp3Aojtwo4770YfDCe95emFK9pR2CKrVsceO9y5npnab7X2MH6u3EhFpfoI8TGe3dx8mMui4p20Ay6AhaKXrXqttv2GA7ATiyEWWot6xpgjo5Hp_s_A.sKq3Y0ZrJtStcYfAk0DFlceSFPTNcd5IhMbjrucWNIA'; await agent.backup.restore(backupJWE); - const expected = JSON.parse(JSON.stringify(Fixtures.Backup.backupJson)); expect(stubRestore).to.have.been.calledWith(expected); }); @@ -347,11 +346,16 @@ describe("Agent Tests", () => { const credentialMap = new Map(); if (credType === CredentialType.JWT) { credentialMap.set( - "prism/jwt", + CredentialType.JWT, Fixtures.Credentials.JWT.credentialPayload ); } else if (credType === CredentialType.AnonCreds) { credentialMap.set(credType, Fixtures.Credentials.Anoncreds.credentialOffer); + } else if (credType === CredentialType.SDJWT) { + credentialMap.set( + CredentialType.SDJWT, + Fixtures.Credentials.SDJWT.credentialPayloadEncoded + ); } return OfferCredential.build( @@ -436,7 +440,34 @@ describe("Agent Tests", () => { ); expect(foundAttachment).to.not.be.undefined; - expect(foundAttachment?.format).to.equal("prism/jwt"); + expect(foundAttachment?.format).to.equal(CredentialType.JWT); + }); + + it(`CredentialType [${CredentialType.SDJWT}]`, async () => { + + // const offer = createOffer(CredentialType.JWT); + const offer = Fixtures.Credentials.SDJWT.credentialOfferMessage; + + const requestCredential = await agent.prepareRequestCredentialWithIssuer(offer); + + expect(requestCredential).to.be.instanceOf(RequestCredential); + expect(requestCredential.to?.toString()).to.equal( + offer.from?.toString() + ); + expect(requestCredential.from.toString()).to.equal( + offer.to?.toString() + ); + + expect(requestCredential.body.formats).to.be.an("array"); + expect(requestCredential.body.formats).to.have.length(1); + // expect(requestCredential.body.formats[0].format).to.equal(credType); + + const foundAttachment = requestCredential.attachments.find( + ({ id }) => id === requestCredential.body.formats[0].attach_id + ); + + expect(foundAttachment).to.not.be.undefined; + expect(foundAttachment?.format).to.equal(CredentialType.SDJWT); }); }); @@ -467,16 +498,17 @@ describe("Agent Tests", () => { describe("JWTCredential", () => { const parseCredentialResult = { mock: "JWTCredential" }; - const jwtPayload = Fixtures.Credentials.JWT.credentialPayload; - const json = JSON.stringify(jwtPayload); - const encoded = Buffer.from(json).toString("base64"); - const base64Data = base64url.baseEncode( - Buffer.from(`jwtPart0.${encoded}.jwtPart2`) - ); - + const base64Data = base64url.baseEncode(Buffer.from(Fixtures.Credentials.JWT.credentialPayloadEncoded)); + const jwtAttachment = AttachmentDescriptor.build( + Fixtures.Credentials.JWT.credentialPayloadEncoded, + "attach_1", + undefined, + undefined, + CredentialType.JWT + ) const issueCredential = new IssueCredential( - { formats: [{ attach_id: "attach_id", format: CredentialType.JWT }] }, - [{ id: "attach_1", format: "prism/jwt", data: { base64: base64Data } }], + { formats: [{ attach_id: "attach_1", format: CredentialType.JWT }] }, + [jwtAttachment], new DID("did", "prism", "from"), new DID("did", "prism", "to"), "test-revocation-thid" @@ -484,8 +516,8 @@ describe("Agent Tests", () => { it("Should revoke a JWT Credential", async () => { const revocationIssueMessage = new IssueCredential( - { formats: [{ attach_id: "attach_id", format: CredentialType.JWT }] }, - [{ id: "attach_1", format: "prism/jwt", data: { base64: base64Data } }], + { formats: [{ attach_id: "attach_1", format: CredentialType.JWT }] }, + [new AttachmentDescriptor({ base64: base64Data }, "attach_1", undefined, undefined, CredentialType.JWT)], new DID("did", "prism", "from"), new DID("did", "prism", "to"), "12345" @@ -516,13 +548,16 @@ describe("Agent Tests", () => { it("Pollux.parseCredential is called with correct decoded data and CredentialType", async () => { sandbox.stub(pluto, "storeCredential").resolves(); + const credData = base64url.baseDecode(base64Data); + const stubParseCredential = sandbox .stub(pollux, "parseCredential") .resolves(parseCredentialResult as any); + + await agent.processIssuedCredentialMessage(issueCredential); - const credData = base64url.baseDecode(base64Data); expect(stubParseCredential).to.have.been.calledOnceWith(credData, { type: CredentialType.JWT, }); @@ -557,11 +592,75 @@ describe("Agent Tests", () => { }); }); + describe("SD+JWTCredential", () => { + const parseCredentialResult = { mock: "SD+JWTCredential" }; + const base64Data = base64url.baseEncode(Buffer.from(Fixtures.Credentials.SDJWT.credentialPayloadEncoded)); + const sdJWTAttachment = AttachmentDescriptor.build( + Fixtures.Credentials.SDJWT.credentialPayloadEncoded, + "attach_1", + undefined, + undefined, + CredentialType.JWT + ) + const issueCredential = new IssueCredential( + { formats: [{ attach_id: "attach_1", format: CredentialType.SDJWT }] }, + [sdJWTAttachment], + new DID("did", "prism", "from"), + new DID("did", "prism", "to"), + "test-revocation-thid" + ); + + it("Pollux.parseCredential is called with correct decoded data and CredentialType", async () => { + sandbox.stub(pluto, "storeCredential").resolves(); + + const credData = base64url.baseDecode(base64Data); + + const stubParseCredential = sandbox + .stub(pollux, "parseCredential") + .resolves(parseCredentialResult as any); + + + + await agent.processIssuedCredentialMessage(issueCredential); + + expect(stubParseCredential).to.have.been.calledOnceWith(credData, { + type: CredentialType.SDJWT, + }); + }); + + it("Pluto.storeCredential is called with result of parseCredential", async () => { + sandbox + .stub(pollux, "parseCredential") + .resolves(parseCredentialResult as any); + + const stubStoreCredential = sandbox + .stub(pluto, "storeCredential") + .resolves(); + + await agent.processIssuedCredentialMessage(issueCredential); + + expect(stubStoreCredential).to.have.been.calledOnceWith( + parseCredentialResult + ); + }); + + it("result of Pollux.parseCredential is returned", async () => { + sandbox.stub(pluto, "storeCredential").resolves(); + sandbox + .stub(pollux, "parseCredential") + .resolves(parseCredentialResult as any); + + const result = + await agent.processIssuedCredentialMessage(issueCredential); + + expect(result).to.equal(parseCredentialResult); + }); + }); + describe("AnonCreds", () => { const parseCredentialResult = { mock: "AnonCredsCredential" }; - const encoded = Buffer.from("testing").toString("base64"); const base64Data = base64url.baseEncode( - Buffer.from(`jwtPart0.${encoded}.jwtPart2`) + Buffer.from(JSON.stringify(Fixtures.Credentials.Anoncreds.credentialIssued)) ); const issueCredential = new IssueCredential( @@ -570,7 +669,7 @@ describe("Agent Tests", () => { { attach_id: "attach_id", format: CredentialType.AnonCreds }, ], }, - [{ id: "attach_1", format: "anoncreds/credential@v1.0", data: { base64: base64Data } }], + [new AttachmentDescriptor({ base64: base64Data }, "attach_1", undefined, undefined, "anoncreds/credential@v1.0")], new DID("did", "prism", "from"), new DID("did", "prism", "to"), "thid" @@ -685,7 +784,10 @@ describe("Agent Tests", () => { { attach_id: "attach_id", format: CredentialType.AnonCreds }, ], }, - [{ id: "attach_1", format: "anoncreds/credential-offer@v1.0", data: { base64: base64Data } }], + + [new AttachmentDescriptor({ base64: base64Data }, "attach_1", undefined, undefined, "anoncreds/credential-offer@v1.0")], + + new DID("did", "prism", "from"), new DID("did", "prism", "to"), "thid" @@ -732,7 +834,7 @@ describe("Agent Tests", () => { .to.have.length(1); const attached = result.attachments[0]; // TODO: what should this be? - // expect(attached).to.have.property("mediaType", "prism/jwt"); + // expect(attached).to.have.property("mediaType", CredentialType.JWT); expect(attached).to.have.property("data"); expect(attached.data).to.have.property("base64").to.be.a("string"); @@ -756,7 +858,7 @@ describe("Agent Tests", () => { }); test("JWTCredential + JWTPresentationRequest - returns Presentation", async () => { - const jwt = new JWT(CastorMock); + const jwt = new JWT(new Apollo(), CastorMock); const payload: JWTCredentialPayload = { iss: "did:prism:da61cf65fbf04b6b9fe06fa3b577fca3e05895a13902decaad419845a20d2d78:Ct8BCtwBEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIP0gMhTAVOk7SgWRluzmeJIjtm2-YMc6AbrD3ePKJQj-GiDZlsa5pQuXGzKvgK10D8SzuDvh79u5oMB7-ZeJNAh-ixJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIP0gMhTAVOk7SgWRluzmeJIjtm2-YMc6AbrD3ePKJQj-GiDZlsa5pQuXGzKvgK10D8SzuDvh79u5oMB7-ZeJNAh-iw", nbf: 23456754321, @@ -785,7 +887,7 @@ describe("Agent Tests", () => { .to.be.an("array") .to.have.length(1); const attached = result.attachments[0]; - // expect(attached).to.have.property("mediaType", "prism/jwt"); + // expect(attached).to.have.property("mediaType", CredentialType.JWT); expect(attached).to.have.property("data"); expect(attached.data).to.have.property("base64").to.be.a("string"); @@ -814,7 +916,7 @@ describe("Agent Tests", () => { test("Credential.subjectDID - invalid - throws", async () => { - const jwt = new JWT(CastorMock); + const jwt = new JWT(new Apollo(), CastorMock); const payload: JWTCredentialPayload = { iss: "did:test:123", sub: undefined as any, @@ -844,7 +946,7 @@ describe("Agent Tests", () => { test("Credential.subjectDID - doesn't match PrivateKey - throws", async () => { stubGetDIDPrivateKeysByDID.resolves([]); - const jwt = new JWT(CastorMock); + const jwt = new JWT(new Apollo(), CastorMock); const payload: JWTCredentialPayload = { iss: "did:test:123", nbf: 23456754321, @@ -874,7 +976,7 @@ describe("Agent Tests", () => { describe("Fail cases", () => { test("RequestPresentation.attachments - empty - throws", async () => { - const jwt = new JWT(CastorMock); + const jwt = new JWT(new Apollo(), CastorMock); const payload: JWTCredentialPayload = { iss: "did:test:123", nbf: 23456754321, @@ -915,7 +1017,7 @@ describe("Agent Tests", () => { sub: Fixtures.DIDs.prismDIDDefault.toString(), vc: {} as any }; - const jwt = new JWT(CastorMock); + const jwt = new JWT(new Apollo(), CastorMock); const jwtString = await jwt.sign({ issuerDID: DID.fromString("did:issuer:123"), privateKey: Fixtures.Keys.secp256K1.privateKey, diff --git a/tests/apollo/Apollo.createPrivateKey.test.ts b/tests/apollo/Apollo.createPrivateKey.test.ts index 00e83245f..945a87aee 100644 --- a/tests/apollo/Apollo.createPrivateKey.test.ts +++ b/tests/apollo/Apollo.createPrivateKey.test.ts @@ -5,6 +5,8 @@ import { ApolloError, Curve, KeyProperties, KeyTypes } from "../../src/domain/mo import { Secp256k1PrivateKey } from "../../src/apollo/utils/Secp256k1PrivateKey"; import { DerivationPath } from "../../src/apollo/utils/derivation/DerivationPath"; import * as Fixtures from "../fixtures"; +import { DeprecatedDerivationPath } from "../../src/domain/models/derivation/schemas/DeprecatedDerivation"; +import { PrismDerivationPath } from "../../src/domain/models/derivation/schemas/PrismDerivation"; describe("Apollo", () => { let apollo: Apollo; @@ -32,8 +34,8 @@ describe("Apollo", () => { expect(result.getProperty(KeyProperties.curve)).to.eq(Curve.SECP256K1); expect(result.getProperty(KeyProperties.chainCode)).to.eq("7e9952eb18d135283fd633180e31b202a5ec87e3e37cc66c6836f18bdf9684b2"); - // no derivationPath provided, defaults to `m/0'/0'/0'` hexed - expect(result.getProperty(KeyProperties.derivationPath)).to.eq("6d2f30272f30272f3027"); + // no derivationPath provided, defaults to `m/29'/29'/0'/4'/0'` hexed + expect(result.getProperty(KeyProperties.derivationPath)).to.eq("6d2f3239272f3239272f30272f34272f3027"); // no index provided, defaults to 0 expect(result.getProperty(KeyProperties.index)).to.eq("0"); @@ -99,8 +101,9 @@ describe("Apollo", () => { [KeyProperties.seed]: fixture.seed, }); + const derivationPath = DerivationPath.fromPath(fixture.path.toString(), [DeprecatedDerivationPath, PrismDerivationPath]) const child = master.isDerivable() - ? master.derive(DerivationPath.from(fixture.path)) + ? master.derive(derivationPath.toString()) : null; const derived = apollo.createPrivateKey({ @@ -149,15 +152,17 @@ describe("Apollo", () => { expect(result.getProperty(KeyProperties.curve)).to.eq(Curve.SECP256K1); expect(result.getProperty(KeyProperties.chainCode)).to.eq("fee48c5a862316d1ea59b77258850f64de2a316796db043a4ebca1616c1c0d24"); expect(result.getProperty(KeyProperties.derivationPath)).to.eq("6d2f31272f30272f3027"); - expect(result.getProperty(KeyProperties.index)).to.eq("1"); + expect(result.getProperty(KeyProperties.index)).to.eq("0"); }); it("KeyProperties.derivationPath - `m/2'/0'/0'` - returns key", () => { + const derivationPath = DerivationPath.fromPath(`m/2'/0'/0'`, [DeprecatedDerivationPath, PrismDerivationPath]); + const result = apollo.createPrivateKey({ [KeyProperties.type]: KeyTypes.EC, [KeyProperties.curve]: Curve.SECP256K1, [KeyProperties.seed]: seedHex, - [KeyProperties.derivationPath]: `m/2'/0'/0'` + [KeyProperties.derivationPath]: derivationPath.toString() }); expect(result).to.be.an.instanceOf(Secp256k1PrivateKey); @@ -167,7 +172,7 @@ describe("Apollo", () => { expect(result.getProperty(KeyProperties.curve)).to.eq(Curve.SECP256K1); expect(result.getProperty(KeyProperties.chainCode)).to.eq("6bfbb6d7bee48110dd0dd1437caa9e88dba86e4bc28585e8e8ab052c96414a48"); expect(result.getProperty(KeyProperties.derivationPath)).to.eq("6d2f32272f30272f3027"); - expect(result.getProperty(KeyProperties.index)).to.eq("2"); + expect(result.getProperty(KeyProperties.index)).to.eq(`${derivationPath.index}`); }); // ? what behaviour do we expect in these cases diff --git a/tests/apollo/Apollo.test.ts b/tests/apollo/Apollo.test.ts index d6b08f719..57336d2c1 100644 --- a/tests/apollo/Apollo.test.ts +++ b/tests/apollo/Apollo.test.ts @@ -1,8 +1,8 @@ -import { expect, assert } from "chai"; - +import chai, { assert } from "chai"; +import chaiAsPromised from "chai-as-promised"; import Apollo from "../../src/apollo/Apollo"; import { Secp256k1KeyPair } from "../../src/apollo/utils/Secp256k1KeyPair"; -import * as ECConfig from "../../src/config/ECConfig"; +import * as ECConfig from "../../src/domain/models/ECConfig"; import { bip39Vectors } from "./derivation/BipVectors"; import { DerivationPath } from "../../src/apollo/utils/derivation/DerivationPath"; @@ -19,9 +19,22 @@ import { KeyTypes, PrivateKey, StorableKey, - MnemonicWordList + MnemonicWordList, + curveToAlg, + JWT_ALG } from "../../src/domain/models"; import * as Fixtures from "../fixtures"; +import { PrismDerivationPath } from "../../src/domain/models/derivation/schemas/PrismDerivation"; +import { DeprecatedDerivationPath } from "../../src/domain/models/derivation/schemas/DeprecatedDerivation"; +import { DerivationAxis } from "../../src/domain/models/derivation/DerivationAxis"; +import ApolloPKG from "@atala/apollo"; +import { normaliseDER } from "../../src/domain/utils/DER"; +import { hash, hashSync, SupportedHashingAlg } from '../../src/domain/utils/hash' +import { randomBytes } from "../../src/domain/utils/randomBytes"; + +const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; +chai.use(chaiAsPromised); +const expect = chai.expect; describe("Apollo", () => { let apollo: Apollo; @@ -192,6 +205,46 @@ describe("Apollo", () => { } }); + it("Should test our hashing libraries", async () => { + const text = "test"; + const validHashing = [SupportedHashingAlg.SHA256, SupportedHashingAlg.SHA512]; + validHashing.forEach((alg) => { + expect(() => hashSync(text, alg)).to.not.be.undefined + expect(hash(text, alg)).to.eventually.not.be.undefined + }) + }) + + it("Should generate random bytes", async () => { + const initValue = new Uint8Array(64); + const initHex = Buffer.from(initValue).toString('hex') + const random = randomBytes(initValue) + expect(initHex).to.not.deep.eq(Buffer.from(random).toString('hex')) + }) + + it("Should should normalise SECP256K1 der signature from apollo", async () => { + const text = Buffer.from("test text"); + const seed = apollo.createRandomSeed() + const sk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.SECP256K1, + seed: Buffer.from(seed.seed.value).toString('hex'), + }); + const pk = sk.publicKey(); + const nativeSk = ApolloSDK.utils.KMMECSecp256k1PrivateKey.Companion.secp256k1FromByteArray( + Int8Array.from(sk.raw) + ); + const signed = Buffer.from( + nativeSk.sign( + Int8Array.from(text) + ) + ) + const normalisedRaw = normaliseDER(signed) + const signature = pk.canVerify() && pk.verify(text, signed) + expect(signature).to.eq(true) + const normalisedSignature = pk.canVerify() && pk.verify(text, normalisedRaw) + expect(normalisedSignature).to.eq(true) + }); + it("Should only verify signed message using the correct SECP256K1 KeyPair", async () => { const text = Buffer.from("test text"); const apollo = new Apollo(); @@ -278,7 +331,7 @@ describe("Apollo", () => { curve: Curve.ED25519, }) .isDerivable() - ).to.be.equal(false); + ).to.be.equal(true); expect( apollo @@ -300,92 +353,87 @@ describe("Apollo", () => { }); it("Should derive secp256k1 privateKey the same way as if we create a new key in Apollo.", async () => { - const seedHex = "a4dd58542e9959eccb56832a953c0e54b3321036b6165ec2f3c1ef533cd1d6da5fae8010c587535404534c192397483c765505f67e62b26026392f8a0cf8ba51"; + const path = DerivationPath.fromPath(`m/0'/0'/1'`, [DeprecatedDerivationPath, PrismDerivationPath]); const createKeyArgs = { type: KeyTypes.EC, curve: Curve.SECP256K1, - seed: seedHex, + seed: "a4dd58542e9959eccb56832a953c0e54b3321036b6165ec2f3c1ef533cd1d6da5fae8010c587535404534c192397483c765505f67e62b26026392f8a0cf8ba51", }; - const privateKey = apollo.createPrivateKey({ ...createKeyArgs }); - const path = DerivationPath.from(`m/0'/0'/1'`); - const derived = privateKey.isDerivable() && privateKey.derive(path); + const privateKey = apollo.createPrivateKey(createKeyArgs); + const derived = privateKey.isDerivable() && privateKey.derive(path.toString()); expect(derived).to.not.equal(false); const withDerivationPath = apollo.createPrivateKey({ ...createKeyArgs, - derivationPath: path, + derivationPath: path.toString(), }); - const raw1 = Buffer.from((derived as PrivateKey).getEncoded()).toString("base64url"); - const raw2 = Buffer.from(withDerivationPath.getEncoded()).toString("base64url"); - const raw3 = Buffer.from(privateKey.getEncoded()).toString("base64url"); + const raw1 = (derived as PrivateKey).getEncoded().toString(); + const raw2 = withDerivationPath.getEncoded().toString(); + const raw3 = privateKey.getEncoded().toString(); expect(raw1).to.equal(raw2); expect(raw1).to.not.equal(raw3); }); - describe("KeyRestoration", () => { - const privateIds = [StorableKey.recoveryId("ed25519", "priv"), StorableKey.recoveryId("x25519", "priv"), StorableKey.recoveryId("secp256k1", "priv")]; - const publicIds = [StorableKey.recoveryId("ed25519", "pub"), StorableKey.recoveryId("x25519", "pub"), StorableKey.recoveryId("secp256k1", "pub")]; - - /* - describe("isPrivateKeyData", () => { - privateIds.forEach(x => { - test(`${x} matches - returns true`, () => { - const key: StorableKey = { - recoveryId: x, - storableData: new Uint8Array() - }; - - const result = apollo.isPrivateKeyData(key); - - expect(result).to.be.true; - }); - }); - - publicIds.forEach(x => { - test(`${x} fails - returns false`, () => { - const key: StorableKey = { - recoveryId: x, - storableData: new Uint8Array() - }; - - const result = apollo.isPrivateKeyData(key); + describe("DerivationPath", () => { + it("Should throw an error when invalid path is used", async () => { + expect(() => DerivationAxis.normal("m/x" as any)).to.throws("Invalid axis, not a number") + }) + it("Should throw an error when invalid path is used", async () => { + expect(() => DerivationAxis.hardened("m/x" as any)).to.throws("Invalid axis, not a number") + }) + it("Should throw an error when invalid path is used", async () => { + expect(() => DerivationAxis.normal(-1)).to.throws("Number corresponding to the axis should be a positive number") + }) + it("Should throw an error when invalid path is used", async () => { + expect(() => DerivationAxis.hardened(-1)).to.throws("Number corresponding to the axis should be a positive number") + }) + it("Should throw an error when invalid path is used", async () => { + expect(() => DerivationPath.fromPath("m/x", [DeprecatedDerivationPath, PrismDerivationPath])).to.throws("DerivationPathErr Invalid axis, not a number") + }) + + it("Should throw an error when invalid (non string) path is used", async () => { + expect(() => DerivationPath.fromPath(null as any, [DeprecatedDerivationPath, PrismDerivationPath])).to.throws("DerivationPathErr Derivation path should be string") + }) + it("Should throw an error when empty derivation schema is used", async () => { + const path = DerivationPath.empty([DeprecatedDerivationPath, PrismDerivationPath]) + expect(() => path.toString()).to.throws("DerivationPathErr Derivation path is empty") + }) + it("Should throw an error when wrong path not starting with m or M", async () => { + expect(() => DerivationPath.fromPath("d/0", [DeprecatedDerivationPath, PrismDerivationPath]).toString()).to.throws("DerivationPathErr Path needs to start with m or M") + }) + it("Should throw an error when invalid derivation schema is used", async () => { + const path = DerivationPath.empty([DeprecatedDerivationPath, PrismDerivationPath]) + const derived = path + .derive(DerivationAxis.hardened(1)) + .derive(DerivationAxis.normal(1)) + .derive(DerivationAxis.hardened(1)) + .derive(DerivationAxis.hardened(1)) + + expect(() => derived.toString()).to.throws("DerivationPathErr Incompatible Derivation schema") + }) + + it("Should throw an error when invalid derivation schema is used", async () => { + expect(() => DerivationPath.fromPath("m/0", [DeprecatedDerivationPath, PrismDerivationPath]).toString()).to.throws("DerivationPathErr Incompatible Derivation schema") + }) + }) + + describe("Curve to alg", () => { + it("Should convert from curve to alg correctly", () => { + + expect(curveToAlg('and')).to.eq(JWT_ALG.unknown); + expect(curveToAlg(Curve.SECP256K1)).to.eq(JWT_ALG.ES256K) + + expect(curveToAlg(Curve.ED25519)).to.eq(JWT_ALG.EdDSA) + + expect(curveToAlg(Curve.X25519)).to.eq(JWT_ALG.EdDSA) + + }) + }) - expect(result).to.be.false; - }); - }); - }); - - describe("isPublicKeyData", () => { - publicIds.forEach(x => { - test(`${x} matches - returns true`, () => { - const key: StorableKey = { - recoveryId: x, - storableData: new Uint8Array() - }; - - const result = apollo.isPublicKeyData(key); - - expect(result).to.be.true; - }); - }); - - privateIds.forEach(x => { - test(`${x} fails - returns false`, () => { - const key: StorableKey = { - recoveryId: x, - storableData: new Uint8Array() - }; - - const result = apollo.isPublicKeyData(key); - - expect(result).to.be.false; - }); - }); - }); - //*/ + describe("KeyRestoration", () => { describe("restorePrivateKey", () => { test("recoveryId ed25519+priv - matches - returns Ed25519PrivateKey instance", () => { diff --git a/tests/apollo/keys/Ed25519.test.ts b/tests/apollo/keys/Ed25519.test.ts index 92a8b70d5..3c6b02d33 100644 --- a/tests/apollo/keys/Ed25519.test.ts +++ b/tests/apollo/keys/Ed25519.test.ts @@ -1,19 +1,31 @@ import { expect } from "chai"; -import { Curve, KeyTypes } from "../../../src/domain"; +import { Curve, KeyTypes, PrivateKey } from "../../../src/domain"; import { Ed25519PrivateKey } from "../../../src/apollo/utils/Ed25519PrivateKey"; import { Ed25519PublicKey } from "../../../src/apollo/utils/Ed25519PublicKey"; import { Ed25519KeyPair } from "../../../src/apollo/utils/Ed25519KeyPair"; +import { DerivationPath } from "../../../src/apollo/utils/derivation/DerivationPath"; +import Apollo from "../../../src/apollo/Apollo"; +import ApolloPKG from "@atala/apollo"; +import { PrismDerivationPath } from "../../../src/domain/models/derivation/schemas/PrismDerivation"; +import { DeprecatedDerivationPath } from "../../../src/domain/models/derivation/schemas/DeprecatedDerivation"; + +const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; +const EdHDKey = ApolloSDK.derivation.EdHDKey; + +let apollo: Apollo = new Apollo(); describe("Keys", () => { describe("Ed25519", () => { describe("PrivateKey", () => { + const seedHex = "a4dd58542e9959eccb56832a953c0e54b3321036b6165ec2f3c1ef533cd1d6da5fae8010c587535404534c192397483c765505f67e62b26026392f8a0cf8ba51"; + const raw = Buffer.from([234, 155, 38, 115, 124, 211, 171, 185, 149, 186, 77, 255, 240, 94, 209, 65, 63, 214, 168, 213, 146, 68, 68, 196, 167, 211, 183, 80, 14, 166, 239, 217]); const encoded = Buffer.from([54, 112, 115, 109, 99, 51, 122, 84, 113, 55, 109, 86, 117, 107, 51, 95, 56, 70, 55, 82, 81, 84, 95, 87, 113, 78, 87, 83, 82, 69, 84, 69, 112, 57, 79, 51, 85, 65, 54, 109, 55, 57, 107]); const privateKey = new Ed25519PrivateKey(encoded); + const chainCodeHex = "7e9952eb18d135283fd633180e31b202a5ec87e3e37cc66c6836f18bdf9684b2"; - // implementations - test("isDerivable - not implemented", () => { - expect(privateKey.isDerivable()).to.be.false; + test("isDerivable", () => { + expect(privateKey.isDerivable()).to.be.true; }); test("isExportable - implemented", () => { @@ -102,6 +114,59 @@ describe("Keys", () => { }); }); + describe("derive", () => { + test("keySpecification.chainCode missing - throws", () => { + expect(() => { + const derivationPath = DerivationPath.fromPath(0 as any, [DeprecatedDerivationPath, PrismDerivationPath]); + privateKey.derive(derivationPath.toString()) + }).to.throw; + }); + test("DerivationPath - m/0'/0'/0'", () => { + const path = DerivationPath.fromPath(`m/0'/0'/1'`, [DeprecatedDerivationPath, PrismDerivationPath]); + const createKeyArgs = { + type: KeyTypes.EC, + curve: Curve.ED25519, + seed: seedHex, + }; + const privateKey = apollo.createPrivateKey(createKeyArgs); + const derived = privateKey.isDerivable() && privateKey.derive(path.toString()); + expect(derived).to.not.equal(false); + + const withDerivationPath = apollo.createPrivateKey({ + ...createKeyArgs, + derivationPath: path.toString() + }); + + const raw1 = (derived as PrivateKey).getEncoded().toString(); + const raw2 = withDerivationPath.getEncoded().toString(); + const raw3 = privateKey.getEncoded().toString(); + expect(raw1).to.equal(raw2); + expect(raw1).to.not.equal(raw3); + }); + test("DerivationPath - m/1'/0'/1'", () => { + const path = DerivationPath.fromPath(`m/1'/0'/1'`, [DeprecatedDerivationPath, PrismDerivationPath]); + const createKeyArgs = { + type: KeyTypes.EC, + curve: Curve.ED25519, + seed: seedHex, + }; + const privateKey = apollo.createPrivateKey(createKeyArgs); + const derived = privateKey.isDerivable() && privateKey.derive(path.toString()); + expect(derived).to.not.equal(false); + + const withDerivationPath = apollo.createPrivateKey({ + ...createKeyArgs, + derivationPath: path.toString() + }); + + const raw1 = (derived as PrivateKey).getEncoded().toString(); + const raw2 = withDerivationPath.getEncoded().toString(); + const raw3 = privateKey.getEncoded().toString(); + expect(raw1).to.equal(raw2); + expect(raw1).to.not.equal(raw3); + }); + }) + // validation? describe("from", () => { test("Buffer", () => { diff --git a/tests/apollo/keys/Secp256k1.test.ts b/tests/apollo/keys/Secp256k1.test.ts index 4c8a8ad5b..1b5146f5b 100644 --- a/tests/apollo/keys/Secp256k1.test.ts +++ b/tests/apollo/keys/Secp256k1.test.ts @@ -1,5 +1,4 @@ import { expect } from "chai"; -import { BN } from "bn.js"; import { Curve, KeyProperties, KeyTypes } from "../../../src/domain"; import { Secp256k1PrivateKey } from "../../../src/apollo/utils/Secp256k1PrivateKey"; import { Secp256k1PublicKey } from "../../../src/apollo/utils/Secp256k1PublicKey"; @@ -8,6 +7,8 @@ import { ECPublicKeyInitialization } from "../../../src/domain/models/errors/Apo import { DerivationPath } from "../../../src/apollo/utils/derivation/DerivationPath"; import ApolloPKG from "@atala/apollo"; +import { DeprecatedDerivationPath } from "../../../src/domain/models/derivation/schemas/DeprecatedDerivation"; +import { PrismDerivationPath } from "../../../src/domain/models/derivation/schemas/PrismDerivation"; const ApolloSDK = ApolloPKG.org.hyperledger.identus.apollo; const HDKey = ApolloSDK.derivation.HDKey; const BigIntegerWrapper = ApolloSDK.derivation.BigIntegerWrapper; @@ -40,17 +41,19 @@ describe("Keys", () => { test("keySpecification.chainCode missing - throws", () => { const key = new Secp256k1PrivateKey(baseRaw); - const derivationPath = DerivationPath.from(0); - expect(() => key.derive(derivationPath)).to.throw; + expect(() => { + const derivationPath = DerivationPath.fromPath(0 as any, [DeprecatedDerivationPath, PrismDerivationPath]); + key.derive(derivationPath.toString()) + }).to.throw; }); test("DerivationPath - m/0'/0'/0'", () => { const key = new Secp256k1PrivateKey(raw); - const derivationPath = DerivationPath.from(`m/0'/0'/0'`); + const derivationPath = DerivationPath.fromPath(`m/0'/0'/0'`, [DeprecatedDerivationPath, PrismDerivationPath]); key.keySpecification.set(KeyProperties.chainCode, chainCodeHex); - const result = key.derive(derivationPath); + const result = key.derive(derivationPath.toString()); expect(result).to.be.an.instanceOf(Secp256k1PrivateKey); expect(result.raw).to.eql(Uint8Array.from([12, 175, 213, 208, 150, 154, 3, 194, 3, 156, 49, 33, 35, 255, 156, 238, 125, 190, 36, 208, 31, 209, 82, 108, 171, 255, 50, 80, 236, 226, 166, 255])); @@ -61,7 +64,7 @@ describe("Keys", () => { expect(result.getProperty(KeyProperties.chainCode)).to.not.eq(chainCodeHex); expect(result.getProperty(KeyProperties.chainCode)).to.eq("55c577fab08382958dcdfcfd6c34e4c45d9ec467c20abb81ce627991ac9e7863"); expect(result.getProperty(KeyProperties.curve)).to.eq(Curve.SECP256K1); - expect(result.getProperty(KeyProperties.index)).to.eq("0"); + expect(result.getProperty(KeyProperties.index)).to.eq(`${derivationPath.index}`); // expect(result.getProperty(KeyProperties.derivationPath)).to.eq(derivationPath.toString()); expect(result.getProperty(KeyProperties.derivationPath)).to.eq("6d2f30272f30272f3027"); @@ -69,29 +72,29 @@ describe("Keys", () => { test("DerivationPath - m/1'/0'/0'", () => { const key = new Secp256k1PrivateKey(raw); - const derivationPath = DerivationPath.from("m/1'/0'/0'"); + const derivationPath = DerivationPath.fromPath("m/1'/0'/0'", [DeprecatedDerivationPath, PrismDerivationPath]); key.keySpecification.set(KeyProperties.chainCode, chainCodeHex); - const result = key.derive(derivationPath); + const result = key.derive(derivationPath.toString()); expect(result.raw).to.eql(Uint8Array.from([220, 223, 118, 183, 102, 141, 198, 60, 221, 162, 132, 68, 233, 188, 169, 39, 128, 174, 202, 114, 4, 203, 31, 40, 35, 85, 166, 164, 178, 17, 158, 150])); expect(result.getProperty(KeyProperties.chainCode)).to.eq("f22fdc4dd573ce17243983faa6492fc33fab35ecfa3f8ad09aa958044a2752f7"); - expect(result.getProperty(KeyProperties.index)).to.eq("1"); + expect(result.getProperty(KeyProperties.index)).to.eq(`${derivationPath.index}`); // expect(result.getProperty(KeyProperties.derivationPath)).to.eq(`m/1'/0'/0'`); expect(result.getProperty(KeyProperties.derivationPath)).to.eq("6d2f31272f30272f3027"); }); test("DerivationPath - m/2'/0'/0'", () => { const key = new Secp256k1PrivateKey(raw); - const derivationPath = DerivationPath.from("m/2'/0'/0'"); + const derivationPath = DerivationPath.fromPath("m/2'/0'/0'", [DeprecatedDerivationPath, PrismDerivationPath]); key.keySpecification.set(KeyProperties.chainCode, chainCodeHex); - const result = key.derive(derivationPath); + const result = key.derive(derivationPath.toString()); expect(result.raw).to.eql(Uint8Array.from([58, 84, 10, 170, 72, 91, 146, 143, 203, 60, 169, 120, 33, 226, 221, 43, 96, 150, 44, 108, 105, 33, 243, 19, 115, 162, 33, 142, 129, 22, 122, 221])); expect(result.getProperty(KeyProperties.chainCode)).to.eq("e56cd109bae854dcf3fc0b766067f9e825901bf1bcfc67dc4f5eaee74cf9c8ea"); - expect(result.getProperty(KeyProperties.index)).to.eq("2"); + expect(result.getProperty(KeyProperties.index)).to.eq(`${derivationPath.index}`); // expect(result.getProperty(KeyProperties.derivationPath)).to.eq(`m/1'/0'/0'`); expect(result.getProperty(KeyProperties.derivationPath)).to.eq("6d2f32272f30272f3027"); }); @@ -109,7 +112,8 @@ describe("Keys", () => { const secp = new Secp256k1PrivateKey(baseRaw); secp.keySpecification.set(KeyProperties.chainCode, chainCodeHex); - const secpChild = secp.derive(DerivationPath.from(path)); + const derivationPath = DerivationPath.fromPath(path, [DeprecatedDerivationPath, PrismDerivationPath]) + const secpChild = secp.derive(derivationPath.toString()); const hdResult = Buffer.from(hdChild.privateKey!).toString("hex"); const spResult = Buffer.from(secpChild.raw).toString("hex"); @@ -172,10 +176,10 @@ describe("Keys", () => { }); test("instantiated through `derive` - index set", () => { - const path = DerivationPath.fromPath(`m/0'/0'/0'`); + const path = DerivationPath.fromPath(`m/0'/0'/0'`, [DeprecatedDerivationPath, PrismDerivationPath]); const key = new Secp256k1PrivateKey(privateKey.raw); key.keySpecification.set(KeyProperties.chainCode, chainCodeHex); - const derived = key.derive(path); + const derived = key.derive(path.toString()); expect(derived.index).to.equal(0); }); diff --git a/tests/castor/PrismDID.test.ts b/tests/castor/PrismDID.test.ts index cd6b206e1..418226d44 100644 --- a/tests/castor/PrismDID.test.ts +++ b/tests/castor/PrismDID.test.ts @@ -1,61 +1,112 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised"; import { base58btc } from "multiformats/bases/base58"; -import { Curve, KeyTypes, VerificationMethods } from "../../src/domain"; +import { Curve, getProtosUsage, getUsageId, JWT_ALG, KeyTypes, PublicKey, Usage, VerificationMethods } from "../../src/domain"; import Apollo from "../../src/apollo/Apollo"; import Castor from "../../src/castor/Castor"; -import * as ECConfig from "../../src/config/ECConfig"; +import * as ECConfig from "../../src/domain/models/ECConfig"; import { Secp256k1PublicKey } from "../../src/apollo/utils/Secp256k1PublicKey"; import * as Fixtures from "../fixtures"; +import * as Protos from "../../src/castor/protos/node_models"; +import { PrismDIDPublicKey } from "../../src/castor/did/prismDID/PrismDIDPublicKey"; chai.use(chaiAsPromised); const expect = chai.expect; +const apollo = new Apollo(); +const castor = new Castor(apollo); + describe("PRISMDID", () => { + describe("PrismDidPublicKey", () => { + + it("Should create getProtosUsageCorrectly", () => { + expect(getProtosUsage("any" as any)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.UNKNOWN_KEY) + expect(getProtosUsage(Usage.MASTER_KEY)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.MASTER_KEY) + expect(getProtosUsage(Usage.ISSUING_KEY)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.ISSUING_KEY) + expect(getProtosUsage(Usage.KEY_AGREEMENT_KEY)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.KEY_AGREEMENT_KEY) + expect(getProtosUsage(Usage.AUTHENTICATION_KEY)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.AUTHENTICATION_KEY) + expect(getProtosUsage(Usage.REVOCATION_KEY)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.REVOCATION_KEY) + expect(getProtosUsage(Usage.CAPABILITY_INVOCATION_KEY)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.CAPABILITY_INVOCATION_KEY) + expect(getProtosUsage(Usage.CAPABILITY_DELEGATION_KEY)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.CAPABILITY_DELEGATION_KEY) + expect(getProtosUsage(Usage.UNKNOWN_KEY)).to.eq(Protos.io.iohk.atala.prism.protos.KeyUsage.UNKNOWN_KEY) + }) + + it("Should create from and to valid key protos", () => { + const unsupportedRaw = new Array(32).fill(1); + const unsupportedCurve = "secp256r1"; + const otherTypePK: PublicKey = { + value: unsupportedRaw, + type: KeyTypes.unknown, + keySpecification: new Map(), + size: 0, + raw: unsupportedRaw, + curve: unsupportedCurve, + alg: JWT_ALG.unknown, + getEncoded() { + return unsupportedRaw + } + } as any + const keys = [ + Fixtures.Keys.secp256K1.publicKey, + Fixtures.Keys.ed25519.publicKey, + Fixtures.Keys.x25519.publicKey, + ]; + keys.forEach((key) => { + const masterPk = new PrismDIDPublicKey( + getUsageId(Usage.MASTER_KEY), + Usage.MASTER_KEY, + key + ); + const masterPkProto = masterPk.toProto() + const recoveredPk = PrismDIDPublicKey.fromProto(apollo, masterPkProto) + expect(masterPk.keyData.raw).to.deep.eq(recoveredPk.keyData.raw) + expect(masterPk.usage).to.eq(recoveredPk.usage) + expect(masterPk.id).to.eq(recoveredPk.id) + }) + + const masterPk = new PrismDIDPublicKey( + getUsageId(Usage.MASTER_KEY), + Usage.MASTER_KEY, + otherTypePK + ); + const masterPkProto = masterPk.toProto() + + expect(() => PrismDIDPublicKey.fromProto(apollo, masterPkProto)).to.throw(`Invalid key curve: ${unsupportedCurve}. Valid options are: X25519,Ed25519,Secp256k1`) + + }) + }) describe("createPrismDID", () => { it("Should create a prismDID from a PublicKey (SECP256K1)", async () => { - const castor = new Castor({} as any); - const result = await castor.createPrismDID(Fixtures.Keys.secp256K1.publicKey, []); - + await castor.resolveDID(result.toString()) expect(result).not.to.be.null; expect(result.toString()).to.equal(Fixtures.Keys.expectedDIDSecp256K1); }); it("Should create a prismDID from a KeyPair (SECP256K1)", async () => { - const castor = new Castor({} as any); - const result = await castor.createPrismDID(Fixtures.Keys.secp256K1, []); - + await castor.resolveDID(result.toString()) expect(result).not.to.be.null; expect(result.toString()).to.equal(Fixtures.Keys.expectedDIDSecp256K1); }); - it("does not create a prismDID from a PublicKey (ED25519)", async () => { - const castor = new Castor({} as any); - expect(castor.createPrismDID(Fixtures.Keys.ed25519.publicKey, [])).to.eventually.be.rejected; - }); - - it("does not create a prismDID from a KeyPair (ED25519)", async () => { - const castor = new Castor({} as any); - expect(castor.createPrismDID(Fixtures.Keys.ed25519, [])).to.eventually.be.rejected; - }); - - it("does not create a prismDID from a PublicKey (X25519)", async () => { - const castor = new Castor({} as any); - expect(castor.createPrismDID(Fixtures.Keys.x25519.publicKey, [])).to.eventually.be.rejected; + it("Should create a prismDID from a KeyPair (Ed25519)", async () => { + const result = await castor.createPrismDID(Fixtures.Keys.secp256K1, [], [Fixtures.Keys.ed25519]); + await castor.resolveDID(result.toString()) + expect(result).not.to.be.null; + expect(result.toString()).to.equal(Fixtures.Keys.expectedDIDEd25519); }); - it("does not create a prismDID from a KeyPair (X25519)", async () => { - const castor = new Castor({} as any); - expect(castor.createPrismDID(Fixtures.Keys.x25519, [])).to.eventually.be.rejected; + it("Should create a prismDID from a KeyPair (X25519)", async () => { + const result = await castor.createPrismDID(Fixtures.Keys.secp256K1, [], [Fixtures.Keys.x25519]); + await castor.resolveDID(result.toString()) + expect(result.toString()).to.equal(Fixtures.Keys.expectedDIDX25519); }); }); describe("Integration Tests", () => { it("Should correctly create a prismDID from an existing HexKey", async () => { - const apollo = new Apollo(); - const castor = new Castor(apollo); + const didExample = "did:prism:733e594871d7700d35e6116011a08fc11e88ff9d366d8b5571ffc1aa18d249ea:Ct8BCtwBEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpxJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIDS5zeYUkLCSAJLI6aLXRTPRxstCLPUEI6TgBrAVCHkwGiDk-ffklrHIFW7pKkT8i-YksXi-XXi5h31czUMaVClcpw"; @@ -86,8 +137,7 @@ describe("PRISMDID", () => { }); it("Create a PrismDID and verify a signature", async () => { - const apollo = new Apollo(); - const castor = new Castor(apollo); + const privateKey = apollo.createPrivateKey({ type: KeyTypes.EC, curve: Curve.SECP256K1, @@ -112,9 +162,40 @@ describe("PRISMDID", () => { } }); + it("Create a ED25519 PrismDID and verify a signature", async () => { + + const issuerSeed = apollo.createRandomSeed().seed; + + const sk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.ED25519, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const masterSk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.SECP256K1, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + + const did = await castor.createPrismDID(masterSk.publicKey(), [], [sk.publicKey()]); + const text = "The quick brown fox jumps over the lazy dog"; + const signature = + sk.isSignable() && sk.sign(Buffer.from(text)); + + expect(signature).to.not.be.equal(false); + + if (signature) { + const result = await castor.verifySignature( + did, + Buffer.from(text), + Buffer.from(signature) + ); + expect(result).to.be.equal(true); + } + }); + it("Should resolve prismDID key correctly", async () => { - const apollo = new Apollo(); - const castor = new Castor(apollo); + const did = "did:prism:2c6e089b137b566e97bf8e1c234755f9f8690194c3bc52c6431ff4bb960394b1:CtADCs0DElsKBmF1dGgtMRAEQk8KCXNlY3AyNTZrMRIgvMs2bdoiICUhwR4BGk2hip8QWzG0YUfKaOa1xDyxMNUaIHm3gJ0eaeiqadY0NFlXOcAidM1SUyupvouHKsaCr0IaEmAKC2Fzc2VydGlvbi0xEAJCTwoJc2VjcDI1NmsxEiCr03dJu2xHHYCOBKNK4JNwh3ypp2JX6-Cr8tXiI17KnBogK9A6g0btjurK8n1R2ZeACOFmZkzPs2wDUy01UtqLH4sSXAoHbWFzdGVyMBABQk8KCXNlY3AyNTZrMRIgA1ltJZ4-5OmDYoiP2ZiKg-MMDR3BfDdw-oHYCvpGZEQaIAh1R73E0DW_wi4Ng5xxkDQ77ocpSz_iiEGE9svSPxtaGjoKE2h0dHBzOi8vZm9vLmJhci5jb20SDUxpbmtlZERvbWFpbnMaFGh0dHBzOi8vZm9vLmJhci5jb20vGjgKEmh0dHBzOi8vdXBkYXRlLmNvbRINTGlua2VkRG9tYWlucxoTaHR0cHM6Ly91cGRhdGUuY29tLxo4ChJodHRwczovL3JlbW92ZS5jb20SDUxpbmtlZERvbWFpbnMaE2h0dHBzOi8vcmVtb3ZlLmNvbS8"; const resolved = await castor.resolveDID(did); diff --git a/tests/fixtures/credentials/index.ts b/tests/fixtures/credentials/index.ts index 00fbb28a0..fddc1e0ab 100644 --- a/tests/fixtures/credentials/index.ts +++ b/tests/fixtures/credentials/index.ts @@ -1,2 +1,3 @@ export * as Anoncreds from "./anoncreds"; export * as JWT from "./jwt"; +export * as SDJWT from "./sdjwt"; diff --git a/tests/fixtures/credentials/jwt.ts b/tests/fixtures/credentials/jwt.ts index 26d398052..ee037f055 100644 --- a/tests/fixtures/credentials/jwt.ts +++ b/tests/fixtures/credentials/jwt.ts @@ -1,13 +1,15 @@ -import Apollo from "../../../src/apollo"; -import { AttachmentDescriptor, JWTCredentialPayload, W3CVerifiableCredential, W3CVerifiableCredentialContext, W3CVerifiableCredentialType } from "../../../src/domain"; +import { AttachmentDescriptor, CredentialType, JWT_ALG, JWTCredentialPayload, W3CVerifiableCredential, W3CVerifiableCredentialContext, W3CVerifiableCredentialType } from "../../../src/domain"; import { OfferCredential } from "../../../src/edge-agent/protocols/issueCredential/OfferCredential"; import { list } from "../dids"; -const apollo = new Apollo(); - export const credentialOfferMessage = new OfferCredential( { - "formats": [], + "formats": [ + { + attach_id: "321905d1-5f01-42b0-b0ba-39b09645eeaa", + format: CredentialType.JWT + } + ], "credential_preview": { "body": { "attributes": [ @@ -31,33 +33,16 @@ export const credentialOfferMessage = new OfferCredential( [ new AttachmentDescriptor({ data: JSON.stringify({ - "data": { - "options": { - "challenge": "fedac0c2-3250-4fb1-bfcb-b5e904058e1f", - "domain": "domain" - }, - "presentation_definition": { - "format": { - "jwt": { - "alg": [ - "ES256K" - ], - "proof_type": [] - }, - "ldp": null - }, - "id": "b8945a8a-c8e3-44af-9506-4a49c0096b31", - "input_descriptors": [], - "name": null, - "purpose": null - } + "options": { + "challenge": "fedac0c2-3250-4fb1-bfcb-b5e904058e1f", + "domain": "domain" } }) }, undefined, "321905d1-5f01-42b0-b0ba-39b09645eeaa", undefined, - "prism/jwt" + CredentialType.JWT ) ], list[2], @@ -104,12 +89,5 @@ export const presentationRequest = { "options": { "challenge": "11c91493-01b3-4c4d-ac36-b336bab5bddf", "domain": "http://localhost:8000/prism-agent" - }, - "presentation_definition": { - "format": null, - "id": "b2a49475-f8ba-4952-a719-a28e909858fa", - "input_descriptors": [], - "name": null, - "purpose": null } }; diff --git a/tests/fixtures/credentials/sdjwt.ts b/tests/fixtures/credentials/sdjwt.ts new file mode 100644 index 000000000..011ab9e5f --- /dev/null +++ b/tests/fixtures/credentials/sdjwt.ts @@ -0,0 +1,62 @@ +import { OfferCredential } from "../../../src"; +import { AttachmentDescriptor, CredentialType, JWT_ALG } from "../../../src/domain"; + +import { list } from "../dids"; + + +export const credentialPayloadEncoded = "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6cHJpc206NzE2OGEwN2I5NGQxMzAyNjg5MDU2ZTkyN2I0ZGVmOTNjNDIzYjk1NTNmNzcxZTQ4NmM4ZDkxODg4M2UyNTZmMTpDcTBCQ3FvQkVsc0tIMkYxZEdobGJuUnBZMkYwYVc5dVlYVjBhR1Z1ZEdsallYUnBiMjVMWlhrUUJFSTJDZ2RGWkRJMU5URTVFaXRrYlRWbU1rZGtValZDWVVod1VuaENPR0pVUld4MlJWOHdaMGxETW5BME1EUk5jM2c1YzNkS09URTBFa3NLRDIxaGMzUmxjbTFoYzNSbGNrdGxlUkFCUWpZS0IwVmtNalUxTVRrU0syUnROV1l5UjJSU05VSmhTSEJTZUVJNFlsUkZiSFpGWHpCblNVTXljRFF3TkUxemVEbHpkMG81TVRRIiwiaWF0IjoxNzE3Nzc2MTY3NTg5LCJ2Y3QiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJmaXJzdG5hbWUiOiJKb2huIiwibGFzdG5hbWUiOiJEb2UiLCJzc24iOiIxMjMtNDUtNjc4OSIsImlkIjoiMTIzNCIsIl9zZF9hbGciOiJzaGEtMjU2In0.RThFQjlENjJDN0Y5NjlCOEM0NERFNkU3RDg4MDg5RkRBQjg0RTUzNzUyNTZFRUI5NUQyQTUwQ0U1MTdDNzQ2NjU5REExOTM4Njc0RjhFMkQ2QjFCNzNFNEZCRDZBNkQ4NjIzNDFEQkFERjY0MTBERUJCRENDRkVBRjhCMkMwMDU~"; + +export const credentialOfferMessage = new OfferCredential( + { + "formats": [ + { + attach_id: "321905d1-5f01-42b0-b0ba-39b09645eeaa", + format: CredentialType.SDJWT + } + ], + "credential_preview": { + "body": { + "attributes": [ + { + "media_type": null, + "name": "familyName", + "value": "JWT" + }, + { + "media_type": null, + "name": "emailAddress", + "value": "jwt@wonderland.com" + } + ] + }, + "schema_id": null, + "type": "https://didcomm.org/issue-credential/3.0/credential-credential" + }, + "comment": null + } as any, + [ + new AttachmentDescriptor({ + data: JSON.stringify({ + "options": { + "challenge": "fedac0c2-3250-4fb1-bfcb-b5e904058e1f", + "domain": "domain" + } + }) + }, + undefined, + "321905d1-5f01-42b0-b0ba-39b09645eeaa", + undefined, + CredentialType.SDJWT + ) + ], + list[2], + list[3], + "e0670d7d-933f-4408-9dfb-340cd6230584", + "f8fe3752-710a-4d76-8d9b-87d7d045c85e" +); + +export const presentationRequest = { + "claims": { + firstname: {} + } +} diff --git a/tests/fixtures/keys.ts b/tests/fixtures/keys.ts index 05bd2db98..465dd2b59 100644 --- a/tests/fixtures/keys.ts +++ b/tests/fixtures/keys.ts @@ -8,6 +8,12 @@ import { X25519PrivateKey } from "../../src/apollo/utils/X25519PrivateKey"; export const expectedDIDSecp256K1 = "did:prism:da61cf65fbf04b6b9fe06fa3b577fca3e05895a13902decaad419845a20d2d78:Ct8BCtwBEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIP0gMhTAVOk7SgWRluzmeJIjtm2-YMc6AbrD3ePKJQj-GiDZlsa5pQuXGzKvgK10D8SzuDvh79u5oMB7-ZeJNAh-ixJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIP0gMhTAVOk7SgWRluzmeJIjtm2-YMc6AbrD3ePKJQj-GiDZlsa5pQuXGzKvgK10D8SzuDvh79u5oMB7-ZeJNAh-iw"; +export const expectedDIDX25519 = + "did:prism:ba32baedae5964a54603f05a10ecb0939131e3160b2e92ec9eda22d202bf2ad4:Cq0CCqoCEkwKEWlzc3Vpbmdpc3N1aW5nS2V5EAJKNQoGWDI1NTE5EitfUGpIZWZGaDlIN3FIM1Z0N01POFZFTi1GMlBsV2NYemR4dzZMUGt4RUdFEnQKH2F1dGhlbnRpY2F0aW9uYXV0aGVudGljYXRpb25LZXkQBEJPCglzZWNwMjU2azESIP0gMhTAVOk7SgWRluzmeJIjtm2-YMc6AbrD3ePKJQj-GiDZlsa5pQuXGzKvgK10D8SzuDvh79u5oMB7-ZeJNAh-ixJkCg9tYXN0ZXJtYXN0ZXJLZXkQAUJPCglzZWNwMjU2azESIP0gMhTAVOk7SgWRluzmeJIjtm2-YMc6AbrD3ePKJQj-GiDZlsa5pQuXGzKvgK10D8SzuDvh79u5oMB7-ZeJNAh-iw"; + +export const expectedDIDEd25519 = + "did:prism:969fdc2ae434b4db3e6dd8df42231c719610bd10df93fc6139a6f9d3144e15fd:Cq4CCqsCEk0KEWlzc3Vpbmdpc3N1aW5nS2V5EAJKNgoHRWQyNTUxORIrZG01ZjJHZFI1QmFIcFJ4QjhiVEVsdkVfMGdJQzJwNDA0TXN4OXN3SjkxNBJ0Ch9hdXRoZW50aWNhdGlvbmF1dGhlbnRpY2F0aW9uS2V5EARCTwoJc2VjcDI1NmsxEiD9IDIUwFTpO0oFkZbs5niSI7ZtvmDHOgG6w93jyiUI_hog2ZbGuaULlxsyr4CtdA_Es7g74e_buaDAe_mXiTQIfosSZAoPbWFzdGVybWFzdGVyS2V5EAFCTwoJc2VjcDI1NmsxEiD9IDIUwFTpO0oFkZbs5niSI7ZtvmDHOgG6w93jyiUI_hog2ZbGuaULlxsyr4CtdA_Es7g74e_buaDAe_mXiTQIfos" + const secpPrivateKey = new Secp256k1PrivateKey( new Uint8Array([ 45, 182, 188, 189, 107, 229, 136, 180, 199, 177, 110, 84, 98, 140, 121, 84, diff --git a/tests/pluto/Pluto.Migrations.test.ts b/tests/pluto/Pluto.Migrations.test.ts index a99e628ab..9c7689341 100644 --- a/tests/pluto/Pluto.Migrations.test.ts +++ b/tests/pluto/Pluto.Migrations.test.ts @@ -12,7 +12,7 @@ import { makeCollections } from "../../src/pluto/rxdb/collections"; import * as Fixtures from "../fixtures"; import { schemaFactory } from "../../src/pluto/models/Schema"; import { Credential } from "../../src/pluto/models"; -import { CredentialRepository } from "../../src/pluto/repositories"; +import { CredentialRepository, KeyRepository } from "../../src/pluto/repositories"; addRxPlugin(RxDBDevModePlugin); @@ -34,8 +34,55 @@ describe("Pluto", () => { describe("Migrations", () => { + test("Should migrate old keys to new keys to add the derivationSchema", async () => { + const store = new Store({ + name: "randomdb", + storage: InMemory, + password: 'random12434', + ignoreDuplicate: true + }, { + keys: { + schema: schemaFactory(schema => { + schema.setRequired("recoveryId", "rawHex"); + schema.addProperty("string", "recoveryId"); + schema.addProperty("string", "rawHex"); + schema.addProperty("string", "alias"); + schema.addProperty("number", "index"); + schema.setEncrypted("rawHex"); + schema.setVersion(0); + }) + } + }); + + const keyRepository = new KeyRepository(store, apollo); + const key = keyRepository.toModel(Fixtures.Keys.secp256K1.privateKey) + + delete (key as any).derivationSchema; + delete (key as any).derivationSchema + + await store.start(); + + await store.insert("keys", key); + const oldKeys = await store.query("keys") + + expect(oldKeys).not.toBe(undefined); + + const currentStore = new Store({ + name: "randomdb", + storage: InMemory, + password: 'random12434', + ignoreDuplicate: true + }); + + await currentStore.start(); + + const keys = await currentStore.query("keys") + expect(keys).not.toBe(undefined); + expect(keys.length).toBe(1); + }); + test("Should migrate old anoncreds v0Credentials into v1 credentials", async () => { - const pollux = new Pollux(castor); + const pollux = new Pollux(apollo, castor); const encodeToBuffer = (cred: object) => { const json = JSON.stringify(cred); return Buffer.from(json); @@ -115,7 +162,7 @@ describe("Pluto", () => { }); test("Should migrate old jwt v0Credentials into v1 credentials", async () => { - const pollux = new Pollux(castor); + const pollux = new Pollux(apollo, castor); const jwtParts = [ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwidHlwZSI6Imp3dCJ9", diff --git a/tests/pluto/Pluto.test.ts b/tests/pluto/Pluto.test.ts index 411c06988..7e4c94e03 100644 --- a/tests/pluto/Pluto.test.ts +++ b/tests/pluto/Pluto.test.ts @@ -100,7 +100,7 @@ describe("Pluto", () => { it("should store message", async function () { const messageId = randomUUID(); - const message = { + const message = Domain.Message.fromJson({ uuid: "abc-123", piuri: "test a", from: Domain.DID.fromString("did:prism:100"), @@ -115,18 +115,18 @@ describe("Pluto", () => { direction: MessageDirection.RECEIVED, fromPrior: "sdomasd", extraHeaders: ["askdpaks"], - }; + }); await instance.storeMessage(message); const values = await instance.getAllMessages(); const value = await instance.getMessage(values[0].id); - expect(value?.from?.toString()).equal(message.from.toString()); + expect(value?.from?.toString()).equal(message.from!.toString()); }); it("should store messages", async function () { const messageId = randomUUID(); - const message = { + const message = Domain.Message.fromJson({ uuid: "abc-123", piuri: "test a", from: Domain.DID.fromString("did:prism:100"), @@ -141,11 +141,15 @@ describe("Pluto", () => { direction: MessageDirection.RECEIVED, fromPrior: "sdomasd", extraHeaders: ["askdpaks"], - }; + }); + + const messages = Array(10) + .fill("_") + .map((x, i) => (Domain.Message.fromJson({ ...message, id: `${message.uuid}-${i}` }))); + + await instance.storeMessages( - Array(10) - .fill("_") - .map((x, i) => ({ ...message, uuid: `${message.uuid}-${i}` })) + messages ); const values = await instance.getAllMessages(); @@ -341,7 +345,7 @@ describe("Pluto", () => { }); // it("should get all messages", async function () { - await instance.storeMessage({ + await instance.storeMessage(Domain.Message.fromJson({ uuid: randomUUID(), id: randomUUID(), thid: "", @@ -356,7 +360,7 @@ describe("Pluto", () => { piuri: "qwerty", extraHeaders: ["x-extra-header"], expiresTimePlus: new Date().toISOString(), - }); + })); const messages = await instance.getAllMessages(); expect(messages).not.empty; }); @@ -365,7 +369,7 @@ describe("Pluto", () => { const to = Domain.DID.fromString("did:prism:123"); const from = Domain.DID.fromString("did:prism:321"); - const message = { + const message = Domain.Message.fromJson({ uuid: randomUUID(), id: randomUUID(), thid: "", @@ -380,7 +384,7 @@ describe("Pluto", () => { piuri: "type-example", extraHeaders: ["x-extra-header"], expiresTimePlus: new Date().toISOString(), - }; + }); await instance.storeMessage(message); const messages = await instance.getAllMessages(); diff --git a/tests/pollux/Pollux.test.ts b/tests/pollux/Pollux.test.ts index 44fca9dea..c155d3343 100644 --- a/tests/pollux/Pollux.test.ts +++ b/tests/pollux/Pollux.test.ts @@ -4,29 +4,26 @@ import * as sinon from "sinon"; import SinonChai from "sinon-chai"; import { expect, assert } from "chai"; -import { AttachmentDescriptor, AttachmentFormats, Claims, Credential, CredentialRequestOptions, CredentialType, Curve, DID, JWTCredentialPayload, JWTPresentationPayload, JWTVerifiableCredentialProperties, KeyTypes, LinkSecret, Message, PolluxError, PresentationClaims, PresentationOptions, PrivateKey, W3CVerifiableCredentialContext, W3CVerifiableCredentialType } from "../../src/domain"; +import { AttachmentDescriptor, AttachmentFormats, Claims, Credential, CredentialRequestOptions, CredentialType, Curve, DID, JWT_ALG, JWTCredentialPayload, JWTPresentationPayload, JWTVerifiableCredentialProperties, KeyTypes, LinkSecret, Message, PolluxError, PresentationClaims, PresentationOptions, PrivateKey, W3CVerifiableCredentialContext, W3CVerifiableCredentialType } from "../../src/domain"; import { JWTCredential } from "../../src/pollux/models/JWTVerifiableCredential"; import Castor from "../../src/castor/Castor"; -import { Apollo } from "../../src/domain/buildingBlocks/Apollo"; +import Apollo from "../../src/apollo/Apollo"; + import { InvalidJWTString } from "../../src/domain/models/errors/Pollux"; import Pollux from "../../src/pollux/Pollux"; -import { base64 } from "multiformats/bases/base64"; import { AnonCredsCredential, AnonCredsRecoveryId } from "../../src/pollux/models/AnonCredsVerifiableCredential"; import { PresentationRequest } from "../../src/pollux/models/PresentationRequest"; import * as Fixtures from "../fixtures"; +import { JWTCore } from "../../src/pollux/utils/jwt/JWTCore"; +import { JWTInstanceType } from "../../src/pollux/utils/jwt/types"; +import { SDJWT } from "../../src/pollux/utils/SDJWT"; import { JWT } from "../../src/pollux/utils/JWT"; +import { SDJWTCredential } from "../../src/pollux/models/SDJWTVerifiableCredential"; chai.use(SinonChai); chai.use(chaiAsPromised); let sandbox: sinon.SinonSandbox; -jest.mock("../../src/pollux/utils/JWT", () => ({ - JWT: jest.fn(() => ({ - sign: jest.fn(() => "JWT.sign.result"), - verify: jest.fn(() => true) - })) -})); - const jwtParts = [ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwidHlwZSI6Imp3dCJ9", @@ -34,11 +31,11 @@ const jwtParts = [ ]; const jwtString = jwtParts.join("."); -type VerificationTestCase = { +type JWTVerificationTestCase = { challenge?: string, apollo: Apollo, castor: Castor, - jwt: JWT, + jwt: JWTCore, pollux: Pollux, issuer: DID, holder: DID, @@ -68,7 +65,7 @@ async function createAnoncredsVerificationTestCase(options: AnoncredsVerificatio const presentationDefinition = await pollux.createPresentationDefinitionRequest( CredentialType.AnonCreds, claims, - new PresentationOptions({}, CredentialType.AnonCreds) + new PresentationOptions({}, CredentialType.AnonCreds) ); const presentationSubmissionJSON = await pollux.createPresentationSubmission( presentationDefinition, @@ -81,7 +78,7 @@ async function createAnoncredsVerificationTestCase(options: AnoncredsVerificatio } } -async function createVerificationTestCase(options: VerificationTestCase) { +async function createJWTVerificationTestCase(options: JWTVerificationTestCase) { const { pollux, issuer, @@ -132,6 +129,11 @@ async function createVerificationTestCase(options: VerificationTestCase) { }) ); + const disclosed = await pollux.revealCredentialFields(jwtCredential, ['course']) + + expect(disclosed).to.not.be.undefined; + expect(Object.keys(disclosed).length).to.gte(1) + const presentationSubmissionJSON = await pollux.createPresentationSubmission( presentationDefinition, jwtCredential, @@ -152,9 +154,9 @@ describe("Pollux", () => { beforeEach(async () => { sandbox = sinon.createSandbox(); - apollo = {} as Apollo; + apollo = new Apollo(); castor = new Castor(apollo); - pollux = new Pollux(castor); + pollux = new Pollux(apollo, castor); await pollux.start(); }); @@ -182,7 +184,7 @@ describe("Pollux", () => { new AttachmentDescriptor({} as any, undefined, undefined, undefined, format) ); - const result = pollux.extractCredentialFormatFromMessage(msg); + const result = msg.credentialFormat; expect(result).to.eql(expected); }); @@ -209,7 +211,7 @@ describe("Pollux", () => { new AttachmentDescriptor({} as any, undefined, undefined, undefined, "secondFormat"), ); - const result = pollux.extractCredentialFormatFromMessage(msg); + const result = msg.credentialFormat; expect(result).to.eql(expected); }); @@ -222,6 +224,16 @@ describe("Pollux", () => { }); describe("parseCredential", () => { + it("Should throw an error if the credential unknown type is parsed", async () => { + expect( + pollux.parseCredential(Buffer.from(JSON.stringify({ claims: { name: 'any' } })), { type: CredentialType.Unknown }) + ).to.eventually.be.rejected; + }) + it("Should throw an error if the credential unknown type is undefined", async () => { + expect( + pollux.parseCredential(Buffer.from(JSON.stringify({ claims: { name: 'any' } }))) + ).to.eventually.be.rejected; + }) describe("AnonCreds", () => { const encodeToBuffer = (cred: object) => { const json = JSON.stringify(cred); @@ -484,272 +496,6 @@ describe("Pollux", () => { }); }); - describe("processCredentialRequest", () => { - describe("processJWTCredential", () => { - it("options not provided - throws", () => { - const body = { formats: [{ format: CredentialType.JWT }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - - const result = pollux.processJWTCredential(msg); - - expect(result).to.eventually.be.rejected; - }); - - it("options missing did - throws", () => { - const body = { formats: [{ format: CredentialType.JWT }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const options = {}; - - const result = pollux.processJWTCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("options missing keyPair - throws", () => { - const body = { formats: [{ format: CredentialType.JWT }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const options = { did: new DID("did", "peer", "test") }; - - const result = pollux.processJWTCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("options correct - returns JWT.sign result", () => { - const body = { formats: [{ format: CredentialType.JWT }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const options: CredentialRequestOptions = { - did: new DID("did", "peer", "test"), - keyPair: { privateKey: {} } as any - }; - - const result = pollux.processJWTCredential(msg, options); - - expect(result).to.eventually.eql("JWT.sign.result"); - }); - }); - - describe("processAnonCredsCredential", () => { - // it("options not provided - throws", () => { - // const body = { formats: [{ format: CredentialType.AnonCreds }] }; - // const msg = new Message(JSON.stringify(body), undefined, "piuri"); - - // const result = pollux.processAnonCredsCredential(msg); - - // expect(result).to.eventually.be.rejected; - // }); - - it("options missing linkSecret - throws", () => { - const body = { formats: [{ format: CredentialType.AnonCreds }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const options = {}; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("options missing linkSecretName - throws", () => { - const body = { formats: [{ format: CredentialType.AnonCreds }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const linkSecret = new LinkSecret("3"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - describe("extractAttachment", () => { - it("no message attachments - throws", () => { - const body = { formats: [{ format: CredentialType.AnonCreds }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("attach_id undefined on body.formats[0] - throws", () => { - const attach_id = "123"; - const body = { formats: [{ format: CredentialType.AnonCreds }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - msg.attachments.push({ - id: attach_id, - data: { base64: "" } - }); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("attach_id on body.formats[0] doesn't match msg.attachments[].id - throws", () => { - const attach_id = "123"; - const body = { formats: [{ format: CredentialType.AnonCreds, attach_id }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - msg.attachments.push({ - id: "not_attach_id", - data: { base64: "" } - }); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - }); - - describe("isAnonCredsBody", () => { - it("anonCredsBody missing cred_def_id - throws", () => { - const anonCredsBody = {}; - const attach_id = "13"; - const body = { formats: [{ format: CredentialType.AnonCreds, attach_id }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const b64Data = base64.baseEncode(Buffer.from(JSON.stringify(anonCredsBody))); - msg.attachments.push({ - id: attach_id, - data: { base64: b64Data } - }); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("anonCredsBody missing schema_id - throws", () => { - const anonCredsBody = { - cred_def_id: "cred_def_id" - }; - const attach_id = "13"; - const body = { formats: [{ format: CredentialType.AnonCreds, attach_id }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const b64Data = base64.baseEncode(Buffer.from(JSON.stringify(anonCredsBody))); - msg.attachments.push({ - id: attach_id, - data: { base64: b64Data } - }); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("anonCredsBody missing nonce - throws", () => { - const anonCredsBody = { - cred_def_id: "cred_def_id", - schema_id: "schema_id" - }; - const attach_id = "13"; - const body = { formats: [{ format: CredentialType.AnonCreds, attach_id }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const b64Data = base64.baseEncode(Buffer.from(JSON.stringify(anonCredsBody))); - msg.attachments.push({ - id: attach_id, - data: { base64: b64Data } - }); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("anonCredsBody missing key_correctness_proof - throws", () => { - const anonCredsBody = { - cred_def_id: "cred_def_id", - schema_id: "schema_id", - nonce: "nonce", - }; - const attach_id = "13"; - const body = { formats: [{ format: CredentialType.AnonCreds, attach_id }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const b64Data = base64.baseEncode(Buffer.from(JSON.stringify(anonCredsBody))); - msg.attachments.push({ - id: attach_id, - data: { base64: b64Data } - }); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - - it("anonCredsBody missing method_name - throws", () => { - const anonCredsBody = { - cred_def_id: "cred_def_id", - schema_id: "schema_id", - nonce: "nonce", - key_correctness_proof: { - c: "c", - xr_cap: [["first", "second"]], - xz_cap: "xz_cap", - } - }; - const attach_id = "13"; - const body = { formats: [{ format: CredentialType.AnonCreds, attach_id }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const b64Data = base64.baseEncode(Buffer.from(JSON.stringify(anonCredsBody))); - msg.attachments.push({ - id: attach_id, - data: { base64: b64Data } - }); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = pollux.processAnonCredsCredential(msg, options); - - expect(result).to.eventually.be.rejected; - }); - }); - - it("returns JSON.stringify CredentialRequest", async () => { - const createCredentialRequestResult = { a: 1 }; - const stubCreateCredentialRequest = sandbox.stub().returns([createCredentialRequestResult, {}]); - - sandbox.stub(pollux, "anoncreds").get(() => ({ - createCredentialRequest: stubCreateCredentialRequest - })); - - const credDef = Fixtures.Credentials.Anoncreds.credentialDefinition; - const stubFetchCredentialDefinition = sandbox.stub(pollux as any, "fetchCredentialDefinition") - .resolves(credDef); - - const anonCredsBody = Fixtures.Credentials.Anoncreds.credentialOffer; - const attach_id = "13"; - const body = { formats: [{ format: CredentialType.AnonCreds, attach_id }] }; - const msg = new Message(JSON.stringify(body), undefined, "piuri"); - const b64Data = base64.baseEncode(Buffer.from(JSON.stringify(anonCredsBody))); - msg.attachments.push({ - id: attach_id, - data: { base64: b64Data } - }); - const linkSecret = new LinkSecret("123", "linkSecretName"); - const options: CredentialRequestOptions = { linkSecret }; - - const result = await pollux.processAnonCredsCredential(msg, options); - - expect(result).to.be.an("array").to.have.length(2); - expect(result[0]).to.equal(createCredentialRequestResult); - expect(stubFetchCredentialDefinition).to.have.been.calledOnceWith(anonCredsBody.cred_def_id); - expect(stubCreateCredentialRequest).to.have.been.calledOnceWith(anonCredsBody, credDef, options.linkSecret?.secret); - }); - }); - }); - describe("createPresentationProof", () => { describe("Anoncreds", () => { beforeEach(async () => { @@ -769,22 +515,6 @@ describe("Pollux", () => { }); }); - describe("JWT", () => { - test("ok", async () => { - - const pr = new PresentationRequest(AttachmentFormats.JWT, Fixtures.Credentials.JWT.presentationRequest); - const cred = JWTCredential.fromJWS(Fixtures.Credentials.JWT.credentialPayloadEncoded); - const did = Fixtures.DIDs.peerDID1; - const privateKey = Fixtures.Keys.secp256K1.privateKey; - - const result = await pollux.createPresentationProof(pr, cred, { - did, - privateKey - }); - - expect(result).not.to.be.null; - }); - }); }); describe("Anoncreds", () => { @@ -951,10 +681,10 @@ describe("Pollux", () => { }); }); - describe("SDK Verification", () => { + describe("JWT SDK Verification", () => { let apollo: Apollo; let castor: Castor; - let jwt: JWT; + let jwt: JWTCore; let pollux: Pollux; beforeEach(async () => { @@ -967,8 +697,9 @@ describe("Pollux", () => { apollo = new Apollo(); castor = new Castor(apollo); - jwt = new JWT(castor) + jwt = new JWT(apollo, castor) pollux = new Pollux( + apollo, castor, undefined, jwt @@ -978,6 +709,265 @@ describe("Pollux", () => { }) + + describe("JWT", () => { + test("secp256k1 ok", async () => { + + const pr = new PresentationRequest(AttachmentFormats.JWT, Fixtures.Credentials.JWT.presentationRequest); + const cred = JWTCredential.fromJWS(Fixtures.Credentials.JWT.credentialPayloadEncoded); + const did = Fixtures.DIDs.peerDID1; + const privateKey = Fixtures.Keys.secp256K1.privateKey; + + const result = await pollux.createPresentationProof(pr, cred, { + did, + privateKey + }); + + expect(result).not.to.be.null; + }); + + test("ed25519 ok", async () => { + const jwt = new JWT(apollo, castor); + const issuerSeed = apollo.createRandomSeed().seed; + const sk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.ED25519, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const masterSk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.SECP256K1, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const issuerDID = await castor.createPrismDID( + masterSk.publicKey(), + [], + [ + sk.publicKey() + ] + ) + const validPayload = { + iss: issuerDID.toString(), + sub: undefined as any, + vc: {} as any + } + + const validJWTString = await jwt.sign({ + issuerDID: issuerDID, + privateKey: sk, + payload: validPayload, + }) + + const pr = new PresentationRequest(AttachmentFormats.JWT, Fixtures.Credentials.JWT.presentationRequest); + const cred = JWTCredential.fromJWS(validJWTString); + const did = Fixtures.DIDs.peerDID1; + const privateKey = sk; + + const result = await pollux.createPresentationProof(pr, cred, { + did, + privateKey + }); + + expect(result).not.to.be.null; + }); + }); + + describe("SDJWT", () => { + test("X25519 not ok", async () => { + + const sdjwt = new SDJWT(apollo, castor); + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + + const issuerSeed = apollo.createRandomSeed().seed; + const sk = apollo.createPrivateKey({ + type: KeyTypes.Curve25519, + curve: Curve.X25519, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const masterSk = apollo.createPrivateKey({ + type: KeyTypes.Curve25519, + curve: Curve.X25519, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const issuerDID = await castor.createPrismDID( + masterSk.publicKey(), + [], + [ + sk.publicKey() + ] + ) + + const payload = { + issuerDID: issuerDID, + payload: { + iss: issuerDID.toString(), + iat: new Date().getTime(), + vct: 'http://example.com', + ...claims + }, + disclosureFrame: {}, + privateKey: sk + } + expect(sdjwt.sign(payload)).to.eventually.be.rejectedWith("Cannot sign with this key") + + + }) + + test("Ed25519 ok", async () => { + + const sdjwt = new SDJWT(apollo, castor); + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + + const issuerSeed = apollo.createRandomSeed().seed; + const sk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.ED25519, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const masterSk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.ED25519, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const issuerDID = await castor.createPrismDID( + masterSk.publicKey(), + [], + [ + sk.publicKey() + ] + ) + const credential = await sdjwt.sign({ + issuerDID: issuerDID, + payload: { + iss: issuerDID.toString(), + iat: new Date().getTime(), + vct: 'http://example.com', + ...claims + }, + disclosureFrame: {}, + privateKey: sk + }); + + const [header, payload, signature] = credential.replace("~", "").split("."); + const [onlySignature, ...disclosures] = signature.split(","); + + const parseCredential = await pollux.parseCredential( + Buffer.from(JSON.stringify({ + protected: header, + payload: payload, + signature: onlySignature, + disclosures: disclosures + })), + { + type: CredentialType.SDJWT + } + ) + + const isCorrectCredential = parseCredential instanceof SDJWTCredential + expect(isCorrectCredential).to.eq(true) + + const presentation = await sdjwt.createPresentationFor( + { + jws: credential, + frame: { firstname: true, id: true }, + privateKey: sk + } + ) + + const verified = await sdjwt.verify({ + issuerDID: issuerDID, + jws: presentation, + requiredClaimKeys: ['firstname', 'id'] + }); + + expect(verified).to.equal(true) + + }) + + + test("Secp256k1 ok", async () => { + + const sdjwt = new SDJWT(apollo, castor); + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + + const issuerSeed = apollo.createRandomSeed().seed; + const sk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.SECP256K1, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const issuerDID = await castor.createPrismDID( + sk.publicKey(), + [] + ) + const credential = await sdjwt.sign({ + issuerDID: issuerDID, + payload: { + iss: issuerDID.toString(), + iat: new Date().getTime(), + vct: 'http://example.com', + ...claims + }, + disclosureFrame: {}, + privateKey: sk + }); + + const presentation = await sdjwt.createPresentationFor( + { + jws: credential, + frame: { firstname: true, id: true }, + privateKey: sk + } + ) + const verified = await sdjwt.verify({ + issuerDID: issuerDID, + jws: presentation, + requiredClaimKeys: ['firstname', 'id'] + }); + + expect(verified).to.equal(true) + + const pr = new PresentationRequest( + AttachmentFormats.SDJWT, Fixtures.Credentials.SDJWT.presentationRequest + ); + const [header, payload, signature] = credential.replace("~", "").split("."); + const [onlySignature, ...disclosures] = signature.split(","); + const parseCredential = await pollux.parseCredential( + Buffer.from(JSON.stringify({ + protected: header, + payload: payload, + signature: onlySignature, + disclosures: disclosures + })), + { + type: CredentialType.SDJWT + } + ) + const result = await pollux.createPresentationProof(pr, parseCredential, { + did: issuerDID, + privateKey: sk + }); + expect(result).not.to.be.null; + }) + + + }) + it("Should throw an error a non signable key is used", async () => { const issuer = DID.fromString("did:issuer:123") const payload: JWTCredentialPayload = { @@ -1505,6 +1495,11 @@ describe("Pollux", () => { payload: payload, }) + const decoded = await jwt.decode(signed); + expect(decoded).to.haveOwnProperty("header"); + + expect(decoded.header).to.deep.equal({ alg: JWT_ALG.ES256K, typ: "JWT" }) + const verified = await jwt.verify({ issuerDID: issuerDID, jws: signed @@ -1513,9 +1508,57 @@ describe("Pollux", () => { expect(verified).to.equal(true) }) + it("Should create and verify an Ed25519 prism did JWS", async () => { + const issuerSeed = apollo.createRandomSeed().seed; + const sk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.ED25519, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const masterSk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.SECP256K1, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); + const issuerDID = await castor.createPrismDID( + masterSk.publicKey(), + [], + [ + sk.publicKey() + ] + ) + const payload: JWTCredentialPayload = { + iss: issuerDID.toString(), + sub: issuerDID.toString(), + nbf: 23456543222, + exp: 2134564321, + vc: {} as any + } + const signed = await jwt.sign({ + issuerDID: issuerDID, + privateKey: sk, + payload: payload, + }) + const decoded = await jwt.decode(signed); + expect(decoded).to.haveOwnProperty("header"); + expect(decoded.header).to.deep.equal({ alg: JWT_ALG.EdDSA, typ: "JWT" }) + const verified = await jwt.verify({ + issuerDID: issuerDID, + holderDID: issuerDID, + jws: signed + }) + expect(verified).to.equal(true) + }) + it("Should create and fail verifying an Secp256k1 prism did JWS with wrong issuer", async () => { + const issuerSeed = apollo.createRandomSeed().seed; + const sk = apollo.createPrivateKey({ + type: KeyTypes.EC, + curve: Curve.SECP256K1, + seed: Buffer.from(issuerSeed.value).toString("hex"), + }); const issuerDID = await castor.createPrismDID( - Fixtures.Keys.secp256K1.privateKey.publicKey(), + sk.publicKey(), [] ) const payload: JWTCredentialPayload = { @@ -1525,22 +1568,16 @@ describe("Pollux", () => { exp: 2134564321, vc: {} as any } - const signed = await jwt.sign({ issuerDID: issuerDID, - privateKey: Fixtures.Keys.secp256K1.privateKey, + privateKey: sk, payload: payload, }) - - const verified = await jwt.verify({ issuerDID: DID.fromString("did:test:12345"), jws: signed }) - expect(verified).to.equal(false) - - }) it("Should create and fail verifying an Secp256k1 prism did JWS with wrong issuer", async () => { @@ -1572,70 +1609,70 @@ describe("Pollux", () => { }) - it("Should Verify false when the presentation contains a credential that has been issued by an issuer with keys that don't match", async () => { - - const issuerSeed = apollo.createRandomSeed().seed; - const holderSeed = apollo.createRandomSeed().seed; - const wrongIssuerSeed = apollo.createRandomSeed().seed; - - const issuerPrv = apollo.createPrivateKey({ - type: KeyTypes.EC, - curve: Curve.SECP256K1, - seed: Buffer.from(issuerSeed.value).toString("hex"), - }); - - const wrongIssuerPrv = apollo.createPrivateKey({ - type: KeyTypes.EC, - curve: Curve.SECP256K1, - seed: Buffer.from(wrongIssuerSeed.value).toString("hex"), - }); - - const holderPrv = apollo.createPrivateKey({ - type: KeyTypes.EC, - curve: Curve.SECP256K1, - seed: Buffer.from(holderSeed.value).toString("hex"), - }); - - const issuerDID = await castor.createPrismDID( - issuerPrv.publicKey() - ) - - const holderDID = await castor.createPrismDID( - holderPrv.publicKey() - ) - - const { - presentationDefinition, - presentationSubmissionJSON, - issuedJWS - } = await createVerificationTestCase({ - apollo, - castor, - jwt, - pollux, - issuer: issuerDID, - holder: holderDID, - holderPrv: holderPrv, - issuerPrv: wrongIssuerPrv, - subject: { - course: 'Identus Training course Certification 2024' - }, - claims: { - course: { - type: 'string', - pattern: 'Identus Training course Certification 2024' - } - } - }); - - expect(pollux.verifyPresentationSubmission(presentationSubmissionJSON, { - presentationDefinitionRequest: presentationDefinition - })).to.eventually.be.rejectedWith( - `Verification failed for credential (${issuedJWS.slice(0, 10)}...): reason -> Invalid Presentation Credential JWS Signature` - ); - - - }) + // it("Should Verify false when the presentation contains a credential that has been issued by an issuer with keys that don't match", async () => { + + // const issuerSeed = apollo.createRandomSeed().seed; + // const holderSeed = apollo.createRandomSeed().seed; + // const wrongIssuerSeed = apollo.createRandomSeed().seed; + + // const issuerPrv = apollo.createPrivateKey({ + // type: KeyTypes.EC, + // curve: Curve.SECP256K1, + // seed: Buffer.from(issuerSeed.value).toString("hex"), + // }); + + // const wrongIssuerPrv = apollo.createPrivateKey({ + // type: KeyTypes.EC, + // curve: Curve.SECP256K1, + // seed: Buffer.from(wrongIssuerSeed.value).toString("hex"), + // }); + + // const holderPrv = apollo.createPrivateKey({ + // type: KeyTypes.EC, + // curve: Curve.SECP256K1, + // seed: Buffer.from(holderSeed.value).toString("hex"), + // }); + + // const issuerDID = await castor.createPrismDID( + // issuerPrv.publicKey() + // ) + + // const holderDID = await castor.createPrismDID( + // holderPrv.publicKey() + // ) + + // const { + // presentationDefinition, + // presentationSubmissionJSON, + // issuedJWS + // } = await createJWTVerificationTestCase({ + // apollo, + // castor, + // jwt, + // pollux, + // issuer: issuerDID, + // holder: holderDID, + // holderPrv: holderPrv, + // issuerPrv: wrongIssuerPrv, + // subject: { + // course: 'Identus Training course Certification 2024' + // }, + // claims: { + // course: { + // type: 'string', + // pattern: 'Identus Training course Certification 2024' + // } + // } + // }); + + // expect(pollux.verifyPresentationSubmission(presentationSubmissionJSON, { + // presentationDefinitionRequest: presentationDefinition + // })).to.eventually.be.rejectedWith( + // `Verification failed for credential (${issuedJWS.slice(0, 10)}...): reason -> Invalid Presentation Credential JWS Signature` + // ); + + + // }) it("Should reject creating a PresentationDefinitionRequest is no AnoncredsPresentationOptions instance is sent", async () => { expect( @@ -1671,7 +1708,7 @@ describe("Pollux", () => { } }, }, - new PresentationOptions({}, CredentialType.AnonCreds) + new PresentationOptions({}, CredentialType.AnonCreds) ); expect(presentation).to.haveOwnProperty("name"); @@ -1732,7 +1769,7 @@ describe("Pollux", () => { presentationDefinition, presentationSubmissionJSON, issuedJWS - } = await createVerificationTestCase({ + } = await createJWTVerificationTestCase({ apollo, castor, jwt, @@ -1789,7 +1826,7 @@ describe("Pollux", () => { presentationDefinition, presentationSubmissionJSON, issuedJWS - } = await createVerificationTestCase({ + } = await createJWTVerificationTestCase({ apollo, castor, jwt, @@ -1872,7 +1909,7 @@ describe("Pollux", () => { presentationDefinition, presentationSubmissionJSON, issuedJWS - } = await createVerificationTestCase({ + } = await createJWTVerificationTestCase({ apollo, castor, jwt, @@ -1926,7 +1963,7 @@ describe("Pollux", () => { const { presentationDefinition, presentationSubmissionJSON, - } = await createVerificationTestCase({ + } = await createJWTVerificationTestCase({ apollo, castor, jwt, @@ -2060,12 +2097,20 @@ describe("Pollux", () => { it("Should Verify to true when an Anoncreds Presentation submission with all valid attributes and predicates are used", async () => { const credential = new AnonCredsCredential(Fixtures.Credentials.Anoncreds.credential); + const wrong = JWTCredential.fromJWS(Fixtures.Credentials.JWT.credentialPayloadEncoded); sandbox.stub(pollux as any, "fetchSchema").resolves(Fixtures.Credentials.Anoncreds.schema); sandbox.stub(pollux as any, "fetchCredentialDefinition").resolves(Fixtures.Credentials.Anoncreds.credentialDefinition); const issuerDID = DID.fromString('did:web:xyz') + const disclosed = await pollux.revealCredentialFields(credential, ['name'], Fixtures.Credentials.Anoncreds.linkSecret.secret) + expect(disclosed).to.not.be.undefined; + expect(disclosed).to.haveOwnProperty("name"); + expect(disclosed.name).to.eq("test"); + + expect(pollux.revealCredentialFields(credential, ['name'])).to.eventually.be.rejected + const { presentationDefinition, @@ -2122,7 +2167,7 @@ describe("Pollux", () => { }, } }, - new PresentationOptions({}, CredentialType.AnonCreds) + new PresentationOptions({}, CredentialType.AnonCreds) ); expect(pollux.verifyPresentationSubmission(Fixtures.Credentials.Anoncreds.underAgeSubmission, { presentationDefinitionRequest: presentationDefinition