├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.ts ├── jsdoc.json ├── package.json ├── rollup.config.js ├── setup-jest.js ├── src ├── README.md ├── attributes.json ├── constants.ts ├── id.ts ├── index.ts ├── interface.ts ├── poa.ts └── utils.ts ├── tests ├── data │ ├── ids.json │ ├── keys.js │ ├── old-ids.json │ └── test-vectors.json ├── id.test.js ├── index.test.js ├── regression.test.js └── utils.test.js ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | [ 7 | "@babel/plugin-transform-runtime" 8 | ], 9 | [ 10 | "@babel/plugin-proposal-class-properties", 11 | { 12 | "loose": true 13 | } 14 | ] 15 | ], 16 | "ignore": [], 17 | "sourceMaps": true, 18 | "retainLines": false, 19 | "comments": false 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "browser": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module", 11 | "allowImportExportEverywhere": true 12 | }, 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/eslint-recommended", 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "plugins": [ 19 | "@typescript-eslint" 20 | ], 21 | "rules": { 22 | "arrow-body-style": ["error", "always"], 23 | "class-methods-use-this": "off", 24 | "func-names": "off", 25 | "max-len": ["error", { 26 | "code": 120, 27 | "ignoreComments": true 28 | }], 29 | "prefer-template": "off", 30 | "no-await-in-loop": "off", 31 | "no-console": "off", 32 | "no-control-regex": "off", 33 | "no-new-func": "off", 34 | "no-param-reassign": "off", 35 | "no-plusplus": "off", 36 | "no-prototype-builtins": "off", 37 | "no-underscore-dangle": "off", 38 | "no-undef": "off", 39 | "no-useless-escape": "off", 40 | "import/extensions": "off", 41 | "import/no-extraneous-dependencies": "off", 42 | "import/prefer-default-export": "off", 43 | "@typescript-eslint/no-explicit-any": "off", 44 | "@typescript-eslint/ban-ts-comment": "off" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase 11 | .firebase/ 12 | .firebaserc 13 | firebase.json 14 | 15 | # Firebase config 16 | 17 | # Uncomment this if you'd like others to create their own Firebase project. 18 | # For a team working on the same Firebase project(s), it is recommended to leave 19 | # it commented so all members can deploy to the same project(s) in .firebaserc. 20 | # .firebaserc 21 | 22 | # Runtime data 23 | pids 24 | *.pid 25 | *.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | lib-cov 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | 40 | # Optional npm cache directory 41 | .npm 42 | 43 | # Optional eslint cache 44 | .eslintcache 45 | 46 | # Optional REPL history 47 | .node_repl_history 48 | 49 | # Output of 'npm pack' 50 | *.tgz 51 | 52 | # Yarn Integrity file 53 | .yarn-integrity 54 | 55 | # dotenv environment variables file 56 | .env 57 | 58 | # WebStorm 59 | .idea 60 | .DS_Store 61 | todo.md 62 | 63 | # Deployment 64 | dist 65 | coverage 66 | module 67 | docs 68 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | test 4 | *.log 5 | .DS_Store 6 | .idea 7 | examples/ 8 | .github 9 | .firebase 10 | .firebase/ 11 | .firebaserc 12 | firebase.json 13 | todo.md 14 | dist 15 | .goreleaser.yml 16 | Makefile 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Open BSV License 2 | Copyright (c) 2019 Bitcoin Association 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | 1 - The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 2 - The Software, and any software that is derived from the Software or parts thereof, 14 | can only be used on the Bitcoin SV blockchains. The Bitcoin SV blockchains are defined, 15 | for purposes of this license, as the Bitcoin blockchain containing block height #556767 16 | with the hash "000000000000000001d956714215d96ffc00e0afda4cd0a96c96f8d802b1662b" and 17 | the test blockchains that are supported by the un-modified Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Attestation Protocol - BAP 2 | > A simple protocol to create a chain of trust for any kind of data on the Bitcoin blockchain 3 | 4 | Authors: Siggi 5 | 6 | Special thanks to Attila Aros & Satchmo 7 | 8 | Inspired by the [AUTHOR IDENTITY Protocol](https://github.com/BitcoinFiles/AUTHOR_IDENTITY_PROTOCOL) 9 | 10 | NOTE: All examples in this document use fake identity keys, addresses and signatures. See the test data sets for real world examples that can be used in a test suite when developing a library. 11 | 12 | - [Intro](#intro) 13 | - [Protocol](#protocol) 14 | * [URN - Uniform Resource Names](#urn---uniform-resource-names) 15 | * [Attributes are defined at schema.org](#attributes-are-defined-at-schemaorg) 16 | - [Creating an identity (ID)](#creating-an-identity-id) 17 | - [Usage in an identity system (BAP-ID)](#usage-in-an-identity-system-bap-id) 18 | * [Attesting (ATTEST)](#attesting-attest) 19 | * [Verifying an identity attribute](#verifying-an-identity-attribute) 20 | * [Delegating signing to another identity](#delegating-signing-to-another-identity) 21 | * [Publishing Identity information (ALIAS)](#publishing-identity-information-alias) 22 | * [BAP uniKey](#bap-unikey) 23 | - [Publishing data (DATA)](#publishing-data) 24 | - [Using as a Power of Attorney](#using-as-a-power-of-attorney) 25 | - [Blacklisting](#blacklisting) 26 | * [Blacklisting transactions / addresses](#blacklisting-transactions--addresses) 27 | * [Blacklisting IP addresses](#blacklisting-ip-addresses) 28 | * [Final note on blacklisting](#final-note-on-blacklisting) 29 | - [Giving consent to access of data](#giving-consent-to-access-of-data) 30 | - [Simple asserts](#simple-asserts) 31 | - [Revoking an attestation](#revoking-an-attestation) 32 | - [BAP on the BSV Metanet - PROVISIONAL](#bap-on-the-bsv-metanet---provisional) 33 | - [BAP w3c DID - PROVISIONAL](#bap-w3c-did---provisional) 34 | - [Extending the protocol](#extending-the-protocol) 35 | 36 | # TODO 37 | - Finish DID specs - help needed 38 | - Request feedback 39 | 40 | # Intro 41 | 42 | The design goals: 43 | 44 | 1. A simple protocol for generic attestation of data, without the need to publish the data itself 45 | 2. Decouple the signing with an address from the funding source address (ie: does not require any on-chain transactions from the signing identity address) 46 | 3. Allow for rotation of signing keys without having to change the existing attestations 47 | 4. Allow for creation of an infinite amount of identities, but still allow for proving of attested attributes between the identities 48 | 49 | # Protocol 50 | 51 | The protocol is defined using the [Bitcom](https://bitcom.bitdb.network/) convention. The signing is done using the [AUTHOR IDENTITY Protocol](https://github.com/BitcoinFiles/AUTHOR_IDENTITY_PROTOCOL). 52 | 53 | - The prefix of the protocol is `1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT`; 54 | 55 | ``` 56 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 57 | [ID|ATTEST|ALIAS|DATA|REVOKE] 58 | [ID Key|URN Attestation Hash] 59 | [Sequence|Address|Data] 60 | | 61 | [AIP protocol address] 62 | [AIP Signing Algorithm] 63 | [AIP Signing Address] 64 | [AIP Signature] 65 | ``` 66 | By default, all fields are signed, so the optional indices of the AIP can be left out. 67 | 68 | The `Sequence` is added to the transaction to prevent replay of the transaction in case of a revocation. The transaction from the same signatory, with the highest `Sequence` is the current one. 69 | 70 | The fourth field is used for the bitcoin signing address in an `ID` transaction only. 71 | 72 | Example: 73 | 74 | ``` 75 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 76 | ATTEST 77 | d4bcdd0f437d0d3bc588bb4e861d2e83e26e8bf9566ae541a5d43329213b1b13 78 | 0 79 | | 80 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 81 | BITCOIN_ECDSA 82 | 1Po6MLAPJsSAGyE8sXw3CgWcgxumNGjyqm 83 | G8wW0NOCPFgGSoCtB2DZ+0CrLrh9ywAl0C5hnx78Q+7QNyPYzsFtkt4/TXkzkTwqbOT3Ofb1CYdNUv5a/jviPVA= 84 | ``` 85 | 86 | ## URN - Uniform Resource Names 87 | The protocol makes use of URN's as a data carrier for the attestation data, as defined by the [w3c](https://www.w3.org/TR/uri-clarification/). 88 | 89 | URN's look like: 90 | 91 | ``` 92 | urn:[namespace identifier]:[...URN] 93 | ``` 94 | 95 | Examples for use in BAP: 96 | 97 | Identity: 98 | ``` 99 | urn:bap:id:[Attribute name]:[Attribute value]:[Nonce] 100 | 101 | urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa 102 | ``` 103 | 104 | Attestations: 105 | ``` 106 | urn:bap:attest:[Attribute hash]:[Identity key] 107 | 108 | urn:bap:attest:42d2396ddfc3dec6acbd96830b844a10b8b2f065e60fbd5238b5267ab086bf4f:1CCWY6EXZwNqbrtW1SXGNFWdwipYT7Ur1Q 109 | ``` 110 | 111 | The URN is hashed using sha256 when used in a transaction sent to the blockchain. 112 | 113 | ## Attributes are defined at schema.org 114 | 115 | Attributes used in BAP should be defined at https://schema.org. 116 | 117 | Especially the Person attributes, found at https://schema.org/Person, should be used in BAP. 118 | 119 | # Creating an identity (ID) 120 | 121 | To create a new identity in BAP, we need to create 2 private keys and compute they public keys and the corresponding addresses. For easy management of the keys, it is recommended to use an [HD Private key](https://docs.moneybutton.com/docs/bsv-hd-private-key.html) with known derivations. 122 | 123 | ``` 124 | rootAddress: 1WffojxvgpQBmUTigoss7VUdfN45JiiRK 125 | firstAddress: 1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo 126 | ``` 127 | 128 | 129 | 130 | Signing identities in BAP are created by linking a unique identity key with bitcoin signing addresses. The identity key should be computed as a hash of the rootAddress. This links the 2 together and prevents others from creating an identity using the same identity key to create confusion. 131 | 132 | The identity key follows the way a Bitcoin address is hashed to reduce the size of the key. 133 | 134 | ``` 135 | identityKey = base58( ripemd160 ( sha256 ( rootAddress ) ) ) 136 | ``` 137 | 138 | Example identity key, note the hex values are fed as binary buffers to the hash functions and not as a string: 139 | 140 | ``` 141 | sha256(1WffojxvgpQBmUTigoss7VUdfN45JiiRK) = c38bc59316de9783b5f7a8ba19bc5d442f6c9b0988c48a241d1c58a1f4e9ae19 142 | 143 | ripemd160(c38bc59316de9783b5f7a8ba19bc5d442f6c9b0988c48a241d1c58a1f4e9ae19) = afb3dcf52c2c661c35c8ec6a92cecbfc691ba371 144 | 145 | base58(afb3dcf52c2c661c35c8ec6a92cecbfc691ba371) = 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 146 | 147 | identityKey: 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 148 | ``` 149 | 150 | **NOTE:** This has been changed with the release of the BAP library. This allows a verifier to link the root address to the identity key. In the past the identity key was random. Older identites created at random will still work with the BAP library, but new identities should be created in this way. 151 | 152 | To link this identity key to the root address and the signing address, an `ID` transaction is sent to the blockchain: 153 | 154 | ``` 155 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 156 | ID 157 | 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 158 | 1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo 159 | | 160 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 161 | BITCOIN_ECDSA 162 | 1WffojxvgpQBmUTigoss7VUdfN45JiiRK 163 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 164 | ``` 165 | 166 | The address `1WffojxvgpQBmUTigoss7VUdfN45JiiRK` associated with the first instance of the identity key on-chain, is the identity control address (or rootAddress). This address should no be used anywhere, but can be used to destroy the identity, in case the latest linked key has been compromised. 167 | 168 | When the signing address is rotated to a new key, a new ID transaction is created, this time signed by the previous address: 169 | 170 | ``` 171 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 172 | ID 173 | 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 174 | 1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA 175 | | 176 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 177 | BITCOIN_ECDSA 178 | 1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo 179 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 180 | ``` 181 | 182 | In this way, we have created a way to rotate the signing keys for a certain identity as often as we want, with each signing key being immutably saved on the blockchain. 183 | 184 | Any signatures done for the identity key should be done using the active key at that time. 185 | 186 | To destroy the identity, an ID transaction is sent to 0, signed with the address from the first ever transaction `1WffojxvgpQBmUTigoss7VUdfN45JiiRK`; 187 | 188 | ``` 189 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 190 | ID 191 | 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 192 | 0 193 | | 194 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 195 | BITCOIN_ECDSA 196 | 1WffojxvgpQBmUTigoss7VUdfN45JiiRK 197 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 198 | ``` 199 | 200 | # Usage in an identity system (BAP-ID) 201 | 202 | A BAP identity is defined as an identity key that has attested identity attributes, verified by one or more authorities. These authorities are outside the scope of this description, but are not governed or controlled. 203 | 204 | All identity attributes have the following characteristics: 205 | 206 | ``` 207 | urn:bap:id:[Attribute name]:[Attribute value]:[Nonce] 208 | ``` 209 | 210 | Attribute | Description 211 | --------- | ---------- 212 | Attribute name | The name of the attribute being described 213 | Attribute value | The value of the attribute being described with the name 214 | Nonce | A unique random string to make sure the entropy of hashing the urn will not cause collision and not allow for dictionary attacks 215 | 216 | A user may want to create multiple identities with a varying degree of details available about that identity. Let's take a couple of examples: 217 | 218 | Identity 1 (`3SyWUZXvhidNcEHbAC3HkBnKoD2Q`): 219 | ``` 220 | urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa 221 | urn:bap:id:birthday:1990-05-22:e61f23cbbb2284842d77965e2b0e32f0ca890b1894ca4ce652831347ee3596d9 222 | urn:bap:id:over18:1:480ca17ccaacd671b28dc811332525f2f2cd594d8e8e7825de515ce5d52d30e8 223 | urn:bap:id:address:51391 Moorpark Ave #104, San Jose, CA 95129, United States:44d47d2375c8346c7ceeab1904360aaf572b1c940c1bd66ffd5cf88fdf06bc05 224 | urn:bap:id:passportNr:US2343242:9c06a0fb0e2d9cef4928855076255e4df3375e2807cf37bc028ddb282f811ac8 225 | urn:bap:id:passportExpiration:2022-02-23:d61a39afb463b42c3e419463a028deb3e9e2cebf67953864e9f9e7869677e7cb 226 | ``` 227 | 228 | Identity 2 (`b71a658ec49a9cb099fd5d3cf0aafce28f1d464fa6e496f61c8048d8ed56edc1`): 229 | ``` 230 | urn:bap:id:name:John Doe:6637be9df2e114ce19a287ff48841899ef4a5762a5f9dc47aef62fe4f579bf93 231 | urn:bap:id:email:john.doen@example.com:2864fd138ab1e9ddaaea763c77a45898dac64a26229f9f3d0f2280e4bfa915de 232 | urn:bap:id:over18:1:5f48f9be1644834933cec74a299d109d18f01e77c9552545d2eae4d0c929000b 233 | ``` 234 | 235 | Identity 3 (`10ef2b1bb05185d0dbae41e1bfefe0c2deb2d389f38fe56daa2cc28a9ba82fc7`): 236 | ``` 237 | urn:bap:id:alternateName:Johnny:7a8d693bce6b6c1cf1dd81468a52b69829e465ff9b0762cf77965309df3ad4c8 238 | ``` 239 | 240 | NOTE: The random nonce should not be re-used across identities. Always create a new random secret for each attribute. 241 | 242 | ## Attesting (ATTEST) 243 | 244 | Anyone can attest for any identity by broadcasting a bitcoin transaction with a signature from their private key of the attributes of the identity. 245 | 246 | All attestations have the following characteristics: 247 | 248 | ``` 249 | urn:bap:attest:[Attribute hash]:[Identity key] 250 | ``` 251 | 252 | Attribute | Description 253 | --------- | ---------- 254 | Attribute hash | A hash of the urn attribute being attested 255 | Identity key | The unique identity key of the owner of the attestation 256 | 257 | Take for example a bank, Banco De Bitcoin, with a known and trusted identity key of `ezY2h8B5sj7SHGw8i1KhHtRvgM5` which is linked via an `ID` transaction to `1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo`. To attest that the bank has seen the information in the identity attribute and that it is correct, the bank would sign an attestation with the identity information together with the given identity key. 258 | 259 | ``` 260 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 261 | ATTEST 262 | [Attestation hash] 263 | [Sequence] 264 | | 265 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 266 | [Signature algorithm] 267 | [Address of signer] 268 | [Signature] 269 | ``` 270 | 271 | For the name urn for the Identity 1 (`3SyWUZXvhidNcEHbAC3HkBnKoD2Q`) in above example: 272 | 273 | - We take the hash of `urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa` = `b17c8e606afcf0d8dca65bdf8f33d275239438116557980203c82b0fae259838` 274 | - Then create an attestation urn for the address: `urn:bap:id:attest:b17c8e606afcf0d8dca65bdf8f33d275239438116557980203c82b0fae259838:3SyWUZXvhidNcEHbAC3HkBnKoD2Q` 275 | - Then hash the attestation for our transaction: `89cd658c0ce3ff62db4270a317c35f8a7dfe1242e2cc94232aa3947d77f82431` 276 | - Then the attestation is signed with the private key belonging to the trusted authority (with address `1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo`); 277 | 278 | ``` 279 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 280 | ATTEST 281 | 89cd658c0ce3ff62db4270a317c35f8a7dfe1242e2cc94232aa3947d77f82431 282 | 0 283 | | 284 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 285 | BITCOIN_ECDSA 286 | 1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo 287 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 288 | ``` 289 | 290 | Since the hash of our attestation is always the same, any authority attesting the identity attribute will broadcast a transaction where the 3rd item is the same. In this way it is possible to search (using for instance Planaria) through the blockchain for all attestations of the identity attribute and select the one most trusted. 291 | 292 | ## Verifying an identity attribute 293 | 294 | For a user to prove their identity, that has been verified by a trusted authority, the user does the following. 295 | 296 | He shares his identity key `3SyWUZXvhidNcEHbAC3HkBnKoD2Q`, the full urn `urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa` and signs a challenge message from the party that request an identity verification. 297 | 298 | The receiving party can now verify: 299 | - That the user is the owner of the address `1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA` by verifying the signature 300 | - That the identity `3SyWUZXvhidNcEHbAC3HkBnKoD2Q` is linked to the address via an `ID` record 301 | - That the attestation urn has been signed by that latest valid address of Banco De Bitcoin. 302 | - Thereby verifying that the user signing the message has been attested by the bank to have the name `John Doe`. 303 | 304 | NOTE: No unneeded sensitive information has been shared and it is not possible to infer any other information from the information sent. The only thing the receiver now knows is that the person doing the signing is called John Doe. 305 | 306 | ## Delegating signing to another identity 307 | 308 | With BAP, it is very easy to create an infinite number of identities for a user. This is even encouraged to preserve privacy. 309 | 310 | When for instance a KYC check is done, this check is done for a certain identity. Replicating this KYC check for all identities for a user, to be able to use in all access applications, is impractical. To solve this we must be able to link, or delegate, from one identity to another. 311 | 312 | `urn:bap:delegate:::` 313 | 314 | Example: 315 | ``` 316 | var attestation = 'urn:bap:delegate:3SyWUZXvhidNcEHbAC3HkBnKoD2Q:341d782c56a588ccdd3ebb181d1dbb4699bdb5fb9956b7bd07e917d955acdb04:7f2fe5aac07e2d4c43bdb232029ed157acf0272eac94a2f75cc17566c01a5e89'; 317 | var attestationHash = sha256(attestation); // 2dbb381888f973a0db3bf311e551a6ac2f3ab792420262d8a6f65ef4feb8c1ef 318 | ``` 319 | 320 | This links, or delegates, from identity `3SyWUZXvhidNcEHbAC3HkBnKoD2Q` to identity `341d782c56a588ccdd3ebb181d1dbb4699bdb5fb9956b7bd07e917d955acdb04`; 321 | 322 | The attestation can be published to the blockchain, signed by the identity `3SyWUZXvhidNcEHbAC3HkBnKoD2Q`; 323 | 324 | ``` 325 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 326 | ATTEST 327 | 2dbb381888f973a0db3bf311e551a6ac2f3ab792420262d8a6f65ef4feb8c1ef 328 | 0 329 | | 330 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 331 | BITCOIN_ECDSA 332 | 1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA 333 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 334 | ``` 335 | 336 | Showing that it is possible to use the verified attributes from `3SyWUZXvhidNcEHbAC3HkBnKoD2Q` in identity `341d782c56a588ccdd3ebb181d1dbb4699bdb5fb9956b7bd07e917d955acdb04` can now be done by sharing the delegation. The delegation attestation will be available to check on-chain. 337 | 338 | The challenge sent by the application requesting the attributes should be signed by **both (!)** identites, to proof access to the private keys of both identities. 339 | 340 | NOTE: The grant published on-chain for the attributes shared must be signed by the delegating identity `3SyWUZXvhidNcEHbAC3HkBnKoD2Q`, and not `341d782c56a588ccdd3ebb181d1dbb4699bdb5fb9956b7bd07e917d955acdb04`; 341 | 342 | NOTE: The receiving end should store both identity keys to be able to proof they have received access to the data of the user. The could be stored as a concatenation of the two identity strings: `341d782c56a588ccdd3ebb181d1dbb4699bdb5fb9956b7bd07e917d955acdb04<-3SyWUZXvhidNcEHbAC3HkBnKoD2Q`; 343 | 344 | NOTE: The main identity, for which a KYC has been done, should never directly be used in any application. A new identity should be created for each and every application accessed. 345 | 346 | ## Publishing Identity information (ALIAS) 347 | 348 | It is possible to publish information about an identity by using the BAP ALIAS keyword. This tells the world that an identity key should be seen as belonging to the entity published in the alias. 349 | 350 | It is recommended to only use the ALIAS keyword for companies that need (or want) to link an identity key to their business. It is not recommended for normal users to publish their identity using the ALIAS keyword. 351 | 352 | The ALIAS data is a stringified JSON object that uses the w3c attributes from https://schema.org/. Example: 353 | ``` 354 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 355 | ALIAS 356 | 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 357 | {"@type":"Organization","name":"Banco De Bitcoin","address":{"@type":"PostalAddress","addressLocality":"Mexico Beach","addressRegion":"FL","streetAddress":"3102 Highway 98"},"url":https://bancodebitcoin.com","logo":{"@type":"ImageObject","contentUrl":"data:image/png;base64,..."} 358 | | 359 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 360 | BITCOIN_ECDSA 361 | 1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo 362 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 363 | ``` 364 | 365 | Organizations are encouraged to use attributes defined at https://schema.org/Organization when using the ALIAS keyword. 366 | 367 | ## BAP uniKey 368 | 369 | The BAP uniKey is a unique hash of identity attributes of a person to create a uniquely identifiable hash to use across services. The uniKey can also be used in a custodial identity service where the only thing shared with a service is the uniKey and the uniKey expiration. The real identity can be looked up at the custodial service. 370 | 371 | The BAP uniKey is defined as follows: 372 | ``` 373 | privateUniKey := sha256( 374 | fullName 375 | nationality 376 | birthDate 377 | socialSecurityNr 378 | passportNr 379 | passportExpirationDate 380 | base64(passport image) 381 | ) 382 | uniKey := sha256(privateUniKey) 383 | ``` 384 | The privateUniKey should only be defined based on a digital (NFC) passport that supports all the attributes mentioned. Adding the base64 data of the image on the passport is especially important to create enough entropy for the hashing function to safe guard against dictionary attacks. The uniKey is a hash of the privateUniKey. 385 | 386 | The privateUniKey should __never__ be shared. The uniKey should only be shared in situations where the user would normally share all his personal info (KYC check). 387 | 388 | The `uniKeyExpirationDate` is equal to the `passportExpirationDate`. 389 | 390 | Example: 391 | ``` 392 | privateUniKey = "27dacfd5d0eefd927263a502bd2dea7b9b6193ba7f8bf98c2f4855b45e7d0008"; 393 | uniKey = "dce3ba1199b74d74a5fb941f92ed7e6adb97acc4dffdb055cf6867162dc8fa74"; 394 | 395 | urn:bap:id:uniKey:dce3ba1199b74d74a5fb941f92ed7e6adb97acc4dffdb055cf6867162dc8fa74:c490de4c40fecd9fa7c3979b9134d0a419fa77388d1535b48a24cdb69425b5d5 396 | 397 | hash = "bc47cdfc44cc89519dca5f12bc10c53492c57090a012d2540257bf02772f82a5"; 398 | ``` 399 | `c490de4c40...` is just a random nonce to increase entropy. 400 | ``` 401 | uniKeyExpirationDate = "2025-04-20"; 402 | 403 | urn:bap:id:uniKeyExpirationDate:2025-04-20:dce3ba1199b74d74a5fb941f92ed7e6adb97acc4dffdb055cf6867162dc8fa74 404 | 405 | hash = "a67e363c4163b49f81529b8b5118670ffbf6d76e2b47a03a01e6e21805388f7f"; 406 | ``` 407 | The urn of the uniKeyExpirationDate should use the uniKey as the nonce. This links them together and makes it verifiable for third parties. 408 | 409 | # Publishing data 410 | 411 | The BAP protocol defines a way to publish data on-chain, optionally in a way that only the sender and receiver can read the data. 412 | 413 | The data can be encrypted using the ECIES encryption scheme (specifically electrum-ecies, https://www.npmjs.com/package/electrum-ecies). 414 | 415 | ``` 416 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 417 | DATA 418 | [Attestation hash] 419 | [[Optionally ECIES encrypted] data] 420 | | 421 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 422 | [Signature algorithm] 423 | [Address of signer] 424 | [Signature] 425 | ``` 426 | 427 | Example: 428 | ``` 429 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 430 | DATA 431 | 2dbb381888f973a0db3bf311e551a6ac2f3ab792420262d8a6f65ef4feb8c1ef 432 | QklFMQMFmPdvjFe8Wfo+JWmTpo+33LXc+4G8ThfaucU72kieb6lWEv4layTb0x5tzpi6lA2it8rO/ELrXomJqC53uBOd+DZSzDhCSpK6SwR+Itt+Pw== 433 | | 434 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 435 | BITCOIN_ECDSA 436 | 1WffojxvgpQBmUTigoss7VUdfN45JiiRK 437 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 438 | ``` 439 | When adding data like this to an attestation, the identity of the signing party should match the identity of the attestation, otherwise the data should be ignored. 440 | 441 | Or as a part of an attestation transaction: 442 | ``` 443 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 444 | ATTEST 445 | 5e991865273588e8be0b834b013b7b3b7e4ff2c7517c9fcdf77da84502cebef1 446 | 0 447 | | 448 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 449 | DATA 450 | 5e991865273588e8be0b834b013b7b3b7e4ff2c7517c9fcdf77da84502cebef1 451 | QklFMQMFmPdvjFe8Wfo+JWmTpo+33LXc+4G8ThfaucU72kieb6lWEv4layTb0x5tzpi6lA2it8rO/ELrXomJqC53uBOd+DZSzDhCSpK6SwR+Itt+Pw== 452 | | 453 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 454 | BITCOIN_ECDSA 455 | 1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo 456 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 457 | ``` 458 | 459 | This can be used to append data to an attestation, for instance to add context to the attestation for a UI of an application. 460 | 461 | For maximum compatibility with ECIES, the data should be encrypted using the private key of the attester and the public key of the owner of the identity for which the attestation is being done (Traditional 2 keys ECIES, https://www.npmjs.com/package/electrum-ecies#traditional-2-keys-ecies). 462 | 463 | # Using as a Power of Attorney 464 | 465 | All users that have an identity and an address registered, should be able to, for instance, give another user temporary rights to sign on their behalf. A Power of Attorney could be defined with the BAP protocol in the following way. 466 | All users that have an identity and an address registered, should be able to, for instance, give another user temporary rights to sign on their behalf. A Power of Attorney could be defined with the BAP protocol in the following way. 467 | 468 | (A power of attorney of this kind is only valid in the real world, but does not allow anyone else to sign anything on-chain) 469 | 470 | The Power of Attorney would have the following characteristics: 471 | 472 | ``` 473 | urn:bap:poa:[PoA Attribute]:[Address]:[Nonce] 474 | ``` 475 | Attribute | Description 476 | --------- | ---------- 477 | PoA Attribute | Power of Attorney attribute being handed over to the person with the identity associates with Address 478 | Address | The bitcoin address of the person (or organisation) being handed the PoA 479 | Nonce | A unique random string to make sure the entropy of hashing the urn will not cause collision and not allow for dictionary attacks 480 | 481 | PoA attributes: 482 | 483 | Attribute | Description 484 | --------- | ---------- 485 | real-estate | To buy, sell, rent, or otherwise manage residential, commercial, and personal real estate 486 | business | To invest, trade, and manage any and all business transactions and decisions, as well as handle any claim or litigation matters 487 | finance | To control banking, tax, and government and retirement transactions, as well as living trust and estate decisions 488 | family | To purchase gifts, employ professionals, and to buy, sell or trade any of your personal property 489 | general | This grants the authority to make any decisions that you would be able to if you were personally present 490 | 491 | Example, give the bank the Power of Attorney over finances: 492 | 493 | For the Identity 1 (`3SyWUZXvhidNcEHbAC3HkBnKoD2Q`) given PoA to the bank `ezY2h8B5sj7SHGw8i1KhHtRvgM5`: 494 | 495 | - We take the hash of `urn:bap:poa:finance:3SyWUZXvhidNcEHbAC3HkBnKoD2Q:ef4ef3b8847cf9533cc044dc032269f80ecf6fcbefbd4d6ac81dddc0124f50e7` 496 | - Then hash the poa for the transaction: `77cdec21e1025f85a5cb3744d5515c54783c739b8fa7c72c9e24d83900261d7f` 497 | - Then the poa is signed with the private key belonging to the identity handing over the PoA (with address `1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA`); 498 | 499 | ``` 500 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 501 | ATTEST 502 | 77cdec21e1025f85a5cb3744d5515c54783c739b8fa7c72c9e24d83900261d7f 503 | 0 504 | | 505 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 506 | BITCOIN_ECDSA 507 | 1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA 508 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 509 | ``` 510 | 511 | The bank will save the urn and can prove that the PoA is still valid on the blockchain. 512 | 513 | The user can always revoke the PoA with a REVOKE transaction. 514 | 515 | # Blacklisting 516 | 517 | With more and more data being posted to the bitcoin blockchain it becomes ever more important to be able to block data from being viewed on sites offering that service. Especially illegal data needs to be filtered from viewing to prevent liability claims. 518 | 519 | A proposed format for blacklisting any type of data could have the following format: 520 | ``` 521 | urn:bap:blacklist:[type]:[attribute]:[key] 522 | ``` 523 | 524 | ## Blacklisting transactions / addresses 525 | 526 | Using the blacklisting format, a transaction ID blacklist would be of the following format for a transaction ID: 527 | ``` 528 | urn:bap:blacklist:bitcoin:tx-id:[Transaction ID] 529 | ``` 530 | 531 | Example, blacklisting transaction ID `9e4b52ca8abe317d246ae2e742898df0956eaf1cc8df7c02154d20c1f55f3f9b`: 532 | ``` 533 | urn:bap:blacklist:bitcoin:tx-id:9e4b52ca8abe317d246ae2e742898df0956eaf1cc8df7c02154d20c1f55f3f9b 534 | ``` 535 | 536 | The hash of this blacklisting is: `8a6bc20369171516fb9155a10f11caff8a51dbd8ae90c5bf3443fc4c83bdc8e8` 537 | 538 | The attestation looks like: 539 | ``` 540 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 541 | ATTEST 542 | 8a6bc20369171516fb9155a10f11caff8a51dbd8ae90c5bf3443fc4c83bdc8e8 543 | 0 544 | | 545 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 546 | BITCOIN_ECDSA 547 | 1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA 548 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 549 | ``` 550 | 551 | Which would indicate that the ID signing with `1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA` is blacklisting the transaction. This way services can blacklist transactions and publish that on-chain for other services to see. 552 | 553 | Third party services could be using this to check whether services they trust are blacklisting transactions and based on that decide not to show them in their viewer. A Simple query of the attestastion hash `8a6bc20369171516fb9155a10f11caff8a51dbd8ae90c5bf3443fc4c83bdc8e8` in a BAP index would return all the services that have blacklisted the transaction. 554 | 555 | For a bitcoin address, the blacklist urn would look like: 556 | ``` 557 | urn:bap:blacklist:bitcoin:address:[Address] 558 | ``` 559 | Example: 560 | ``` 561 | urn:bap:blacklist:bitcoin:address:1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA 562 | ``` 563 | 564 | ## Blacklisting IP addresses 565 | 566 | This blacklisting urn could also be used to signal blacklisting of IP addresses, for instance IP addresses being used by known bot networks. 567 | 568 | NOTE: Because IP addresses are personally identifiable information, we need to take more care when hashing these and publishing them on-chain to prevent reverse lookups of the IP addresses. 569 | 570 | For IP addresses we could use a concatenation of the idKey of the signing party to add entropy to the hashing: 571 | ``` 572 | urn:bap:blacklist:ip-address:[IP Address]:[ID key] 573 | ``` 574 | 575 | This would prevent direct lookups and force services to only search for blacklisting by services they trust. A lookup of all attestations and all id's would be an extremly CPU intensive task. 576 | 577 | For the Identity 1 (`3SyWUZXvhidNcEHbAC3HkBnKoD2Q`) blacklisting IP address `1.1.1.1`: 578 | ``` 579 | urn:bap:blacklist:ip-address:1.1.1.1:3SyWUZXvhidNcEHbAC3HkBnKoD2Q 580 | ``` 581 | 582 | The hash of this blacklisting is: `73df789478993f8f4e100be416811860d6fc2ae208fdfaf256788cd522f21219` 583 | 584 | The attestation looks like: 585 | ``` 586 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 587 | ATTEST 588 | 73df789478993f8f4e100be416811860d6fc2ae208fdfaf256788cd522f21219 589 | 0 590 | | 591 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 592 | BITCOIN_ECDSA 593 | 1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA 594 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 595 | ``` 596 | 597 | A third party service that wants to make use of this information is forced to look through a BAP index in a targeted way, per IP address and for each attesting service separately and is not able to recreate a list of blocked IP addresses. 598 | 599 | ## Final note on blacklisting 600 | 601 | Using attestations for blacklists is a good way of creating one-way blacklists. It's easy to lookup whether some service has blacklisted something (transaction, address, IP address), but it is very hard to create a list of all things a service has blacklisted. 602 | 603 | Also because the blacklist attestations look just like any other attestation, the blacklistings can not be identified as such which increases the difficulty of creating a list of blacklistings of a service. 604 | 605 | # Simple asserts 606 | 607 | BAP can also be used for simple assertions of any type of data or action. 608 | 609 | Example: 610 | ``` 611 | urn:bap:assert:[Some assertion you want to make]:[nonce] 612 | ``` 613 | 614 | Example, asserting ownership of a file: 615 | ``` 616 | urn:bap:assert:John Doe (john@example.com) owns the file with the sha256 hash 70eed96ac2900c68998a99166b4b4833ec311454dbbab82fc4ae028bbb802a35:02a043948c8185dab98456c20e5c68149e2fb9d52079068aeacb377240045515 617 | ``` 618 | This has a hash of `e4d1c8868d143cb8eaf240c0191c19e513a4f58a8eb8243af00b5626fe2eb764`. 619 | 620 | The user then needs to attest to this on-chain, and sign: 621 | ``` 622 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 623 | ATTEST 624 | e4d1c8868d143cb8eaf240c0191c19e513a4f58a8eb8243af00b5626fe2eb764 625 | 0 626 | | 627 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 628 | BITCOIN_ECDSA 629 | 1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo 630 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 631 | ``` 632 | 633 | # Giving consent to access of data 634 | 635 | When a website requests data from a user, the user should leave a record on-chain that this data was freely given to the service by the user. The user should also be able to revoke the access to the data, which would imply that the service needs to delete any copy's of the data shared. This is the only way the user is (legally) in charge of what data the service has access to. 636 | 637 | A possible way to do this, using BAP: 638 | ``` 639 | urn:bap:grant:[Attribute names]:[Identity key] 640 | ``` 641 | 642 | Example, for a service with identity key `ezY2h8B5sj7SHGw8i1KhHtRvgM5`: 643 | ``` 644 | urn:bap:grant:name,email,alternateName:ezY2h8B5sj7SHGw8i1KhHtRvgM5 645 | ``` 646 | This has a hash of `b88bd23005be7e0737f02e67de8b392df834ba27caed1e7774aec77c9dcb85d0`. 647 | 648 | The user then needs to attest to this on-chain: 649 | ``` 650 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 651 | ATTEST 652 | b88bd23005be7e0737f02e67de8b392df834ba27caed1e7774aec77c9dcb85d0 653 | 0 654 | | 655 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 656 | BITCOIN_ECDSA 657 | 1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo 658 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 659 | ``` 660 | 661 | NOTE: this could be given to the service directly as proof of granting access to the data by the user. The service could then put the data on-chain, paying the necessary fees. 662 | 663 | The service should be monitoring the blockchain for a revocation of the data sharing grant and remove any data related to this user of the revocation is seen. Alternatively, the user could notify the service of a revocation transaction when made on-chain. 664 | 665 | # Revoking an attestation 666 | 667 | In rare cases when the attestation needs to be revoked, this can be done using the `REVOKE` keyword. The revocation transaction has exactly the same format as the attestation transaction, except for the REVOKE keyword. 668 | 669 | ``` 670 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 671 | REVOKE 672 | 77cdec21e1025f85a5cb3744d5515c54783c739b8fa7c72c9e24d83900261d7f 673 | 1 674 | | 675 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 676 | BITCOIN_ECDSA 677 | 1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA 678 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 679 | ``` 680 | 681 | The sequence number is important here to prevent replays of the transaction. 682 | 683 | # BAP on the BSV Metanet - PROVISIONAL 684 | 685 | NOTE: This is still very much work in progress. Feedback is very welcome. 686 | 687 | https://bitcoinsv.io/wp-content/uploads/2020/10/The-Metanet-Technical-Summary-v1.0.pdf 688 | 689 | BAP transactions can also be published on-chain using the BSV metanet protocol. The biggest difference is that those transactions will not use the AIP signing protocol, but the native BSV transaction signing. 690 | 691 | ## An example. 692 | 693 | A transaction like this, creating a new identity with root address `1WffojxvgpQBmUTigoss7VUdfN45JiiRK`: 694 | 695 | ``` 696 | 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT 697 | ID 698 | 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 699 | 1KJ8vx8adZeznoDfoGf632rNkzxK2ZwzSG 700 | | 701 | 15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva 702 | BITCOIN_ECDSA 703 | 1WffojxvgpQBmUTigoss7VUdfN45JiiRK 704 | HB6Ye7ekxjKDkblJYL9lX3J2vhY75vl+WfVCq+wW3+y6S7XECkgYwUEVH3WEArRuDb/aVZ8ntLI/D0Yolb1dhD8= 705 | ``` 706 | 707 | Would then look more like this: 708 | 709 | ``` 710 | Input: 711 | <𝑆𝑖𝑔 𝑃𝑝𝑎𝑟𝑒𝑛𝑡> <𝑃𝑝𝑎𝑟𝑒𝑛𝑡> 712 | 713 | Output: 714 | OP_RETURN meta <𝑃𝑛𝑜𝑑𝑒> <𝑇𝑥𝐼𝐷𝑝𝑎𝑟𝑒𝑛𝑡> 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT ID 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 715 | ``` 716 | 717 | Where `Pparent` is the public key associated with the root address `1WffojxvgpQBmUTigoss7VUdfN45JiiRK`, and `Pnode` is the public key associated with the new signing address `1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo`. 718 | 719 | Rotating to a new signing key (`1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA`) is now possible by creating a new metanet edge: 720 | 721 | ``` 722 | Input: 723 | <𝑆𝑖𝑔 𝑃𝑝𝑎𝑟𝑒𝑛𝑡> <𝑃𝑝𝑎𝑟𝑒𝑛𝑡> 724 | 725 | Output: 726 | OP_RETURN meta <𝑃𝑛𝑜𝑑𝑒> <𝑇𝑥𝐼𝐷𝑝𝑎𝑟𝑒𝑛𝑡> 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT ID 3SyWUZXvhidNcEHbAC3HkBnKoD2Q 727 | ``` 728 | 729 | Where `Pparent` is the public key associated with the address `1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo`, and `Pnode` is the public key associated with the new signing address `1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA`. 730 | 731 | An attestation could then be written like this on-chain: 732 | 733 | ``` 734 | Input: 735 | <𝑆𝑖𝑔 𝑃𝑝𝑎𝑟𝑒𝑛𝑡> <𝑃𝑝𝑎𝑟𝑒𝑛𝑡> 736 | 737 | Output: 738 | OP_RETURN meta <𝑃𝑝𝑎𝑟𝑒𝑛𝑡> <𝑇𝑥𝐼𝐷𝑝𝑎𝑟𝑒𝑛𝑡> 1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT ATTEST 2dbb381888f973a0db3bf311e551a6ac2f3ab792420262d8a6f65ef4feb8c1ef 0 739 | ``` 740 | 741 | Where `Pparent` is the public key associated with the latest valid address `1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA`. Note that the node links back to itself, creating a metanet leaf and not a new edge, where the `TxIDParent` points to the transaction where the edge was created. 742 | 743 | # BAP w3c DID - PROVISIONAL 744 | 745 | A w3c compatible DID can be created from a BAP ID in the following manner: 746 | 747 | ``` 748 | { 749 | "@context": ["https://w3id.org/did/v0.11", "https://w3id.org/bap/v1"], 750 | "id": "did:bap:id:", 751 | "publicKey": [ 752 | { 753 | "id": "did:bap:id:#", 754 | "controller": "did:bap:id:", 755 | "type": "EcdsaSecp256k1VerificationKey2019", 756 | "bitcoinAddress": "" 757 | } 758 | ], 759 | "authentication": ["#key1"], 760 | "assertionMethod": ["#key1"] 761 | } 762 | ``` 763 | Example: 764 | ``` 765 | { 766 | "@context": ["https://w3id.org/did/v0.11", "https://w3id.org/bap/v1"], 767 | "id": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q", 768 | "publicKey": [ 769 | { 770 | "id": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q#root", 771 | "controller": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q", 772 | "type": "EcdsaSecp256k1VerificationKey2019", 773 | "bitcoinAddress": "1WffojxvgpQBmUTigoss7VUdfN45JiiRK" 774 | }, 775 | { 776 | "id": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q#key1", 777 | "controller": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q", 778 | "type": "EcdsaSecp256k1VerificationKey2019", 779 | "bitcoinAddress": "1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo" 780 | } 781 | ], 782 | "authentication": ["#key1"], 783 | "assertionMethod": ["#key1"] 784 | } 785 | ``` 786 | 787 | When keys are rotated to a new signing key the new key can be added to the DID and all authentication and assertion set to that new key. 788 | ``` 789 | { 790 | "@context": ["https://w3id.org/did/v0.11", "https://w3id.org/bap/v1"], 791 | "id": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q", 792 | "publicKey": [ 793 | { 794 | "id": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q#root", 795 | "controller": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q", 796 | "type": "EcdsaSecp256k1VerificationKey2019", 797 | "bitcoinAddress": "1WffojxvgpQBmUTigoss7VUdfN45JiiRK" 798 | }, 799 | { 800 | "id": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q#key1", 801 | "controller": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q", 802 | "type": "EcdsaSecp256k1VerificationKey2019", 803 | "bitcoinAddress": "1K4c6YXR1ixNLAqrL8nx5HUQAPKbACTwDo" 804 | }, 805 | { 806 | "id": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q#key2", 807 | "controller": "did:bap:id:3SyWUZXvhidNcEHbAC3HkBnKoD2Q", 808 | "type": "EcdsaSecp256k1VerificationKey2019", 809 | "bitcoinAddress": "1JfMQDtBKYi6z65M9uF2gxgLv7E8pPR6MA" 810 | } 811 | ], 812 | "authentication": ["#key2"], 813 | "assertionMethod": ["#key2"] 814 | } 815 | ``` 816 | 817 | # Extending the protocol 818 | 819 | The protocol could be extended for other use cases, by introducing new keywords (next to ATTEST, REVOKE, ID AND ALIAS) or introducing other `urn:bap:...` schemes. 820 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/9k/tb80d9g96wddlz7zqg43nhwm0000gp/T/jest_dy", 15 | 16 | // Automatically clear mock calls, instances and results before every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | // coverageProvider: "babel", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state before every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state and implementation before every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | setupFiles: [ 129 | "./setup-jest.js", 130 | ], 131 | 132 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 133 | // setupFilesAfterEnv: [], 134 | 135 | // The number of seconds after which a test is considered as slow and reported as such in the results. 136 | // slowTestThreshold: 5, 137 | 138 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 139 | // snapshotSerializers: [], 140 | 141 | // The test environment that will be used for testing 142 | // testEnvironment: "jest-environment-node", 143 | 144 | // Options that will be passed to the testEnvironment 145 | // testEnvironmentOptions: {}, 146 | 147 | // Adds a location field to test results 148 | // testLocationInResults: false, 149 | 150 | // The glob patterns Jest uses to detect test files 151 | // testMatch: [ 152 | // "**/__tests__/**/*.[jt]s?(x)", 153 | // "**/?(*.)+(spec|test).[tj]s?(x)" 154 | // ], 155 | 156 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 157 | // testPathIgnorePatterns: [ 158 | // "/node_modules/" 159 | // ], 160 | 161 | // The regexp pattern or array of patterns that Jest uses to detect test files 162 | // testRegex: [], 163 | 164 | // This option allows the use of a custom results processor 165 | // testResultsProcessor: undefined, 166 | 167 | // This option allows use of a custom test runner 168 | // testRunner: "jest-circus/runner", 169 | 170 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 171 | // testURL: "http://localhost", 172 | 173 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 174 | // timers: "real", 175 | 176 | // A map from regular expressions to paths to transformers 177 | // transform: undefined, 178 | 179 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 180 | // transformIgnorePatterns: [ 181 | // "/node_modules/", 182 | // "\\.pnp\\.[^\\/]+$" 183 | // ], 184 | 185 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 186 | // unmockedModulePathPatterns: undefined, 187 | 188 | // Indicates whether each individual test should be reported during the run 189 | // verbose: undefined, 190 | 191 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 192 | // watchPathIgnorePatterns: [], 193 | 194 | // Whether to use watchman for file crawling 195 | // watchman: true, 196 | }; 197 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Bux Javascript client", 3 | "source": { 4 | "includePattern": ".+\\.ts(doc|x)?$", 5 | "excludePattern": ".+\\.(test|spec).ts" 6 | }, 7 | "plugins": [ 8 | "node_modules/better-docs/typescript" 9 | ], 10 | "tags": { 11 | "allowUnknownTags": ["optional"] 12 | }, 13 | "opts": { 14 | "template": "node_modules/better-docs" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcoin-bap", 3 | "version": "1.3.1", 4 | "description": "BAP npm module", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/icellan/bap.git" 8 | }, 9 | "license": "Open BSV", 10 | "precommit": "test", 11 | "main": "dist/typescript-npm-package.cjs.js", 12 | "module": "dist/typescript-npm-package.esm.js", 13 | "browser": "dist/typescript-npm-package.umd.js", 14 | "scripts": { 15 | "build": "rollup -c", 16 | "build:types": "tsc -p ./tsconfig.json --outDir build --declaration true && api-extractor run", 17 | "clean": "rimraf ./build ./dist ./docs", 18 | "deploy": "yarn pub --access public", 19 | "dev": "rollup -c -w", 20 | "docs": "rimraf ./docs && jsdoc src -r -c jsdoc.json -d docs", 21 | "prebuild:types": "rimraf ./build", 22 | "predocs": "rimraf ./docs", 23 | "pub": "yarn build && yarn publish", 24 | "lint": "eslint ./src", 25 | "lintfix": "eslint ./src --fix", 26 | "testquiet": "jest --detectOpenHandles --forceExit --silent", 27 | "testonly": "jest --collectCoverage --detectOpenHandles --forceExit", 28 | "testwatch": "jest --watchAll --collectCoverage --logHeapUsage --detectOpenHandles", 29 | "test": "yarn lint && yarn testonly", 30 | "test-watch": "yarn testwatch", 31 | "check": "./node_modules/.bin/npm-check -u", 32 | "prepare": "yarn clean && yarn lint && yarn test && yarn build" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.17.5", 36 | "@babel/plugin-transform-runtime": "^7.19.1", 37 | "@babel/preset-env": "^7.16.11", 38 | "@babel/preset-typescript": "^7.16.7", 39 | "@rollup/plugin-babel": "^5.3.1", 40 | "@rollup/plugin-commonjs": "^22.0.0", 41 | "@rollup/plugin-json": "^4.1.0", 42 | "@rollup/plugin-node-resolve": "^14.0.0", 43 | "@rollup/plugin-typescript": "^8.3.1", 44 | "@types/bsv": "github:chainbow/bsv-types", 45 | "@types/jest": "^29.0.0", 46 | "@typescript-eslint/eslint-plugin": "^5.38.1", 47 | "@typescript-eslint/parser": "^5.38.1", 48 | "babel-jest": "^29.0.0", 49 | "bsv": "^1.5.6", 50 | "eslint": "^8.24.0", 51 | "eslint-config-airbnb-base": "^14.2.1", 52 | "eslint-plugin-import": "^2.22.1", 53 | "jest": "^26.6.3", 54 | "jest-cli": "^26.6.3", 55 | "jest-fetch-mock": "^3.0.3", 56 | "jest-slow-test-reporter": "^1.0.0", 57 | "npm-check": "5.9.2", 58 | "pre-commit": "^1.2.2", 59 | "regenerator-runtime": "^0.13.7", 60 | "rimraf": "3.0.2", 61 | "rollup": "^2.70.0", 62 | "rollup-plugin-dts": "^4.2.0", 63 | "rollup-plugin-exclude-dependencies-from-bundle": "^1.1.22", 64 | "rollup-plugin-node-builtins": "^2.1.2", 65 | "rollup-plugin-polyfill-node": "^0.10.0", 66 | "run-sequence": "2.2.1", 67 | "ts-node": "^10.4.0", 68 | "tslib": "^2.3.1", 69 | "typedoc": "^0.23.1", 70 | "typescript": "^4.6.2" 71 | }, 72 | "dependencies": { 73 | "node-fetch": "^2.6.1" 74 | }, 75 | "peerDependencies": { 76 | "bsv": "^1.5.6" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import typescript from "@rollup/plugin-typescript"; 4 | import json from "@rollup/plugin-json"; 5 | import nodePolyfills from 'rollup-plugin-polyfill-node'; 6 | import excludeDependenciesFromBundle from "rollup-plugin-exclude-dependencies-from-bundle"; 7 | import dts from 'rollup-plugin-dts'; 8 | import builtins from 'rollup-plugin-node-builtins'; 9 | import pkg from "./package.json"; 10 | 11 | export default [ 12 | // browser-friendly UMD build 13 | { 14 | input: "src/index.ts", 15 | output: { 16 | name: "typescriptNpmPackage", 17 | file: pkg.browser, 18 | format: "umd", 19 | sourcemap: true, 20 | }, 21 | external: ['bsv'], 22 | plugins: [ 23 | builtins(), 24 | resolve({ 25 | skip: ['bsv'], 26 | browser: true, 27 | preferBuiltins: true, 28 | }), 29 | commonjs(), 30 | json(), 31 | typescript({tsconfig: "./tsconfig.json", sourceMap: false}), 32 | nodePolyfills(), 33 | ], 34 | }, 35 | 36 | // CommonJS (for Node) and ES module (for bundlers) build. 37 | // (We could have three entries in the configuration array 38 | // instead of two, but it's quicker to generate multiple 39 | // builds from a single configuration where possible, using 40 | // an array for the `output` option, where we can specify 41 | // `file` and `format` for each target) 42 | { 43 | input: "src/index.ts", 44 | output: [ 45 | {file: pkg.main, format: "cjs", sourcemap: true}, 46 | {file: pkg.module, format: "es", sourcemap: true}, 47 | ], 48 | plugins: [ 49 | typescript({tsconfig: "./tsconfig.json", sourceMap: false}), 50 | excludeDependenciesFromBundle({peerDependencies: true}), 51 | ], 52 | }, 53 | 54 | { 55 | // path to your declaration files root 56 | input: './dist/src/index.d.ts', 57 | output: [ 58 | {file: 'dist/typescript-npm-package.cjs.d.ts', format: 'es'}, 59 | {file: 'dist/typescript-npm-package.esm.d.ts', format: 'es'}, 60 | {file: 'dist/typescript-npm-package.umd.d.ts', format: 'es'} 61 | ], 62 | plugins: [dts()], 63 | }, 64 | ]; 65 | -------------------------------------------------------------------------------- /setup-jest.js: -------------------------------------------------------------------------------- 1 | require('jest-fetch-mock').enableMocks() 2 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Attestation Protocol - BAP 2 | > A simple protocol to create a chain of trust for any kind of information on the Bitcoin blockchain 3 | 4 | Javascript classes for working with identities and attestations. 5 | 6 | **NOTE: This is work in progress and more documentation will follow.** 7 | 8 | # BAP 9 | 10 | The BAP class is a wrapper around all BAP functions, including managing IDs and attestations. 11 | 12 | ```shell 13 | npm install bitcoin-bap --save 14 | ``` 15 | 16 | Example creating a new ID: 17 | ```javascript 18 | const HDPrivateKey = 'xprv...'; 19 | const bap = new BAP(HDPrivateKey); 20 | 21 | // Create a new identity 22 | const newId = bap.newId(); 23 | // set the name of the ID 24 | newId.name = 'Social media identity'; 25 | // set a description for this ID 26 | newId.description = 'Pseudonymous identity to use on social media sites'; 27 | // set identity attributes 28 | newId.addAttribute('name', 'John Doe'); 29 | newId.addAttribute('email', 'john@doe.com'); 30 | 31 | // export the identities for storage 32 | const encryptedExport = bap.exportIds(); 33 | ``` 34 | 35 | Signing: 36 | ```javascript 37 | const HDPrivateKey = 'xprv...'; 38 | const bap = new BAP(HDPrivateKey); 39 | const identity = bap.getId(""); 40 | 41 | // B protocol data 42 | const opReturn = [ 43 | Buffer.from('19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut').toString('hex'), 44 | Buffer.from('Hello World!').toString('hex'), 45 | Buffer.from('text/plain').toString('hex'), 46 | Buffer.from('utf8').toString('hex'), 47 | ]; 48 | // signOpReturnWithAIP expects and returns hex values 49 | const signedOpReturn = identity.signOpReturnWithAIP(opReturn); 50 | ``` 51 | 52 | Encryption, every identity has a separate encryption/decryption key: 53 | ```javascript 54 | const HDPrivateKey = 'xprv...'; 55 | const bap = new BAP(HDPrivateKey); 56 | const identity = bap.getId(""); 57 | 58 | const publicKey = identity.getEncryptionPublicKey(); 59 | 60 | const cipherText = identity.encrypt('Hello World!'); 61 | 62 | const text = identity.decrypt(cipherText); 63 | ``` 64 | 65 | The encryption uses `ECIES` from the `bsv` library: 66 | ```javascript 67 | import ECIES from 'bsv/ecies'; 68 | 69 | const ecies = new ECIES() 70 | ecies.publicKey(publicKey); 71 | return ecies.encrypt(stringData).toString('base64'); 72 | ``` 73 | 74 | Other examples: 75 | ```javascript 76 | // List the identity keys of all id's 77 | const idKeys = bap.listIds(); 78 | 79 | // get a certain id 80 | const id = bap.getId(idKeys[0]); 81 | ``` 82 | -------------------------------------------------------------------------------- /src/attributes.json: -------------------------------------------------------------------------------- 1 | // https://schema.org/Person 2 | { 3 | "name": { 4 | "type": "string", 5 | "description": "Full name of user" 6 | }, 7 | "givenName": { 8 | "type": "string", 9 | "description": "Given name as shown in official documents" 10 | }, 11 | "familyName": { 12 | "type": "string", 13 | "description": "Family name as shown in official documents" 14 | }, 15 | "alternateName": { 16 | "type": "string", 17 | "description": "A self chosen nickname" 18 | }, 19 | "gender": { 20 | "type": "string", 21 | "description": "Male / Female / other / ... - https://schema.org/GenderType" 22 | }, 23 | "birthDate": { 24 | "type": "string", 25 | "description": "Birthday of the user - YYYY-MM-DD - https://schema.org/Date" 26 | }, 27 | "email": { 28 | "type": "string", 29 | "description": "Email address of the the user" 30 | }, 31 | "telephone": { 32 | "type": "string", 33 | "description": "Phone number of the user - international format +14255551212 (https://www.itu.int/rec/T-REC-E.164-201011-I/en)" 34 | }, 35 | "mobile": { 36 | "type": "string", 37 | "description": "Mobile phone number of the user - international format +14255551212 (https://www.itu.int/rec/T-REC-E.164-201011-I/en) - NOTE, not defined in schema.org" 38 | }, 39 | "address": { 40 | "type": "string", 41 | "description": "Address of the user - to be verified the address should be the full address as shown on local postage, from authorities - new lines allowed - https://schema.org/address" 42 | }, 43 | "country": { 44 | "type": "string", 45 | "description": "ISO 3166-1 3-letter country code of the users residence" 46 | }, 47 | "nationality": { 48 | "type": "string", 49 | "description": "ISO 3166-1 3-letter country code of the users nationality" 50 | }, 51 | "website": { 52 | "type": "string", 53 | "description": "Website address of the the user - not defined at schema.org" 54 | }, 55 | "avatar": { 56 | "type": "string", 57 | "description": "URL of the users avatar - can be a base64 URI - not defined at schema.org" 58 | }, 59 | "passportNr": { 60 | "type": "string", 61 | "description": "Document number of the passport of the user - not defined at schema.org" 62 | }, 63 | "passportExpiration": { 64 | "type": "string", 65 | "description": "Passport expiration date - YYYY-MM-DD - not defined at schema.org" 66 | }, 67 | "passportCountry": { 68 | "type": "string", 69 | "description": "ISO 3166-1 3-letter country code of the passport issuer - not defined at schema.org" 70 | }, 71 | "driversLicenseNr": { 72 | "type": "string", 73 | "description": "Document number of the drivers license of the user - not defined at schema.org" 74 | }, 75 | "driversLicenseExpiration": { 76 | "type": "string", 77 | "description": "Expiration date of the drivers license of the user - not defined at schema.org" 78 | }, 79 | "driversLicenseCountry": { 80 | "type": "string", 81 | "description": "ISO 3166-1 3-letter country code of the passport issuer - not defined at schema.org" 82 | }, 83 | "socialSecurityNr": { 84 | "type": "string", 85 | "description": "Social security number of the user - not defined at schema.org" 86 | }, 87 | "uniKey": { 88 | "type": "string", 89 | "description": "A hash of all the attributes of a persons passport, to create a deterministic unique id for the user that can be used across identities - not defined at schema.org" 90 | }, 91 | "uniKeyExpirationDate": { 92 | "type": "string", 93 | "description": "The expiration date of the document used to create the uniKey - the nonce of the uniKeyExpirationDate should be the uniKey hash" 94 | }, 95 | "isHuman": { 96 | "type": "number", 97 | "description": "Whether this ID is a real human - this can be used to attest this is not a bot - not defined at schema.org" 98 | }, 99 | "over18": { 100 | "type": "number", 101 | "description": "1 if the user is over 18 - not defined at schema.org" 102 | }, 103 | "over19": { 104 | "type": "number", 105 | "description": "1 if the user is over 19 - not defined at schema.org" 106 | }, 107 | "over20": { 108 | "type": "number", 109 | "description": "1 if the user is over 20 - not defined at schema.org" 110 | }, 111 | "over21": { 112 | "type": "number", 113 | "description": "1 if the user is over 21 - not defined at schema.org" 114 | }, 115 | "over25": { 116 | "type": "number", 117 | "description": "1 if the user is over 25 - not defined at schema.org" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const BAP_BITCOM_ADDRESS = '1BAPSuaPnfGnSBM3GLV9yhxUdYe4vGbdMT'; 2 | export const BAP_BITCOM_ADDRESS_HEX = '0x' + Buffer.from(BAP_BITCOM_ADDRESS).toString('hex'); 3 | export const AIP_BITCOM_ADDRESS = '15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva'; 4 | export const AIP_BITCOM_ADDRESS_HEX = '0x' + Buffer.from(AIP_BITCOM_ADDRESS).toString('hex'); 5 | export const BAP_SERVER = 'https://bap.network/api/v1'; 6 | export const MAX_INT = 2147483648 - 1; // 0x80000000 7 | 8 | // This is just a choice for this library and could be anything else if so needed/wanted 9 | // but it is advisable to use the same derivation between libraries for compatibility 10 | export const SIGNING_PATH_PREFIX = 'm/424150\'/0\'/0\''; // BAP in hex 11 | export const ENCRYPTION_PATH = `m/424150'/${MAX_INT}'/${MAX_INT}'`; 12 | -------------------------------------------------------------------------------- /src/id.ts: -------------------------------------------------------------------------------- 1 | import bsv from 'bsv'; 2 | // @ts-ignore 3 | import ECIES from 'bsv/ecies'; 4 | // @ts-ignore 5 | import Message from 'bsv/message'; 6 | import { 7 | MAX_INT, 8 | SIGNING_PATH_PREFIX, 9 | BAP_SERVER, 10 | BAP_BITCOM_ADDRESS, 11 | AIP_BITCOM_ADDRESS, ENCRYPTION_PATH, 12 | } from './constants'; 13 | import { Utils } from './utils'; 14 | import {Identity} from "./interface"; 15 | 16 | /** 17 | * BAP_ID class 18 | * 19 | * This class should be used in conjunction with the BAP class 20 | * 21 | * @type {BAP_ID} 22 | */ 23 | class BAP_ID { 24 | #HDPrivateKey: bsv.HDPrivateKey; 25 | #BAP_SERVER: string = BAP_SERVER; 26 | #BAP_TOKEN = ''; 27 | #rootPath: string; 28 | #previousPath: string; 29 | #currentPath: string; 30 | #idSeed: string; 31 | 32 | idName: string; 33 | description: string; 34 | 35 | rootAddress: string; 36 | identityKey: string; 37 | identityAttributes: { [key: string]: any } 38 | 39 | constructor(HDPrivateKey: bsv.HDPrivateKey, identityAttributes: { [key: string]: any } = {}, idSeed = '') { 40 | this.#idSeed = idSeed; 41 | if (idSeed) { 42 | // create a new HDPrivateKey based on the seed 43 | const seedHex = bsv.crypto.Hash.sha256(Buffer.from(idSeed)).toString('hex'); 44 | const seedPath = Utils.getSigningPathFromHex(seedHex); 45 | this.#HDPrivateKey = HDPrivateKey.deriveChild(seedPath); 46 | } else { 47 | this.#HDPrivateKey = HDPrivateKey; 48 | } 49 | 50 | this.idName = 'ID 1'; 51 | this.description = ''; 52 | 53 | this.#rootPath = `${SIGNING_PATH_PREFIX}/0/0/0`; 54 | this.#previousPath = `${SIGNING_PATH_PREFIX}/0/0/0`; 55 | this.#currentPath = `${SIGNING_PATH_PREFIX}/0/0/1`; 56 | 57 | const rootChild = this.#HDPrivateKey.deriveChild(this.#rootPath); 58 | this.rootAddress = rootChild.privateKey.publicKey.toAddress().toString(); 59 | this.identityKey = this.deriveIdentityKey(this.rootAddress); 60 | 61 | // unlink the object 62 | identityAttributes = { ...identityAttributes }; 63 | this.identityAttributes = this.parseAttributes(identityAttributes); 64 | } 65 | 66 | set BAP_SERVER(bapServer) { 67 | this.#BAP_SERVER = bapServer; 68 | } 69 | 70 | get BAP_SERVER(): string { 71 | return this.#BAP_SERVER; 72 | } 73 | 74 | set BAP_TOKEN(token) { 75 | this.#BAP_TOKEN = token; 76 | } 77 | 78 | get BAP_TOKEN(): string { 79 | return this.#BAP_TOKEN; 80 | } 81 | 82 | deriveIdentityKey(address: string): string { 83 | const rootAddressHash = bsv.crypto.Hash.sha256(Buffer.from(address)); 84 | // @ts-ignore 85 | return bsv.encoding.Base58(bsv.crypto.Hash.ripemd160(rootAddressHash)).toString(); 86 | } 87 | 88 | /** 89 | * Helper function to parse identity attributes 90 | * 91 | * @param identityAttributes 92 | * @returns {{}} 93 | */ 94 | parseAttributes(identityAttributes: { [key: string]: any } | string): { [key: string]: any } { 95 | if (typeof identityAttributes === 'string') { 96 | return this.parseStringUrns(identityAttributes); 97 | } 98 | 99 | Object.keys(identityAttributes).forEach((key) => { 100 | if ( 101 | !identityAttributes[key].hasOwnProperty('value') 102 | || !identityAttributes[key].hasOwnProperty('nonce') 103 | ) { 104 | throw new Error('Invalid identity attribute'); 105 | } 106 | }); 107 | 108 | return identityAttributes || {}; 109 | } 110 | 111 | /** 112 | * Parse a text of urn string into identity attributes 113 | * 114 | * urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa 115 | * urn:bap:id:birthday:1990-05-22:e61f23cbbb2284842d77965e2b0e32f0ca890b1894ca4ce652831347ee3596d9 116 | * urn:bap:id:over18:1:480ca17ccaacd671b28dc811332525f2f2cd594d8e8e7825de515ce5d52d30e8 117 | * 118 | * @param urnIdentityAttributes 119 | */ 120 | parseStringUrns(urnIdentityAttributes: string): { [key: string]: any } { 121 | const identityAttributes: { [key: string]: any } = {}; 122 | urnIdentityAttributes.replace(/^\s+/g, '').replace(/\r/gm, '') 123 | .split('\n') 124 | .forEach((line) => { 125 | // remove any whitespace from the string (trim) 126 | line = line.replace(/^\s+/g, '').replace(/\s+$/g, ''); 127 | const urn = line.split(':'); 128 | if (urn[0] === 'urn' && urn[1] === 'bap' && urn[2] === 'id' && urn[3] && urn[4] && urn[5]) { 129 | identityAttributes[urn[3]] = { 130 | value: urn[4], 131 | nonce: urn[5], 132 | }; 133 | } 134 | }); 135 | 136 | return identityAttributes; 137 | } 138 | 139 | /** 140 | * Returns the identity key 141 | * 142 | * @returns {*|string} 143 | */ 144 | getIdentityKey(): string { 145 | return this.identityKey; 146 | } 147 | 148 | /** 149 | * Returns all the attributes in the identity 150 | * 151 | * @returns {*} 152 | */ 153 | getAttributes(): { [key: string]: any } { 154 | return this.identityAttributes; 155 | } 156 | 157 | /** 158 | * Get the value of the given attribute 159 | * 160 | * @param attributeName 161 | * @returns {{}|null} 162 | */ 163 | getAttribute(attributeName: string): any { 164 | if (this.identityAttributes.hasOwnProperty(attributeName)) { 165 | return this.identityAttributes[attributeName]; 166 | } 167 | 168 | return null; 169 | } 170 | 171 | /** 172 | * Set the value of the given attribute 173 | * 174 | * If an empty value ('' || null || false) is given, the attribute is removed from the ID 175 | * 176 | * @param attributeName string 177 | * @param attributeValue any 178 | * @returns {{}|null} 179 | */ 180 | setAttribute(attributeName: string, attributeValue: any): void { 181 | if (attributeValue) { 182 | if (this.identityAttributes.hasOwnProperty(attributeName)) { 183 | this.identityAttributes[attributeName].value = attributeValue; 184 | } else { 185 | this.addAttribute(attributeName, attributeValue); 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * Unset the given attribute from the ID 192 | * 193 | * @param attributeName 194 | * @returns {{}|null} 195 | */ 196 | unsetAttribute(attributeName: string): void { 197 | delete this.identityAttributes[attributeName]; 198 | } 199 | 200 | /** 201 | * Get all attribute urn's for this id 202 | * 203 | * @returns {string} 204 | */ 205 | getAttributeUrns(): string { 206 | let urns = ''; 207 | Object.keys(this.identityAttributes) 208 | .forEach((key) => { 209 | const urn = this.getAttributeUrn(key); 210 | if (urn) { 211 | urns += `${urn}\n`; 212 | } 213 | }); 214 | 215 | return urns; 216 | } 217 | 218 | /** 219 | * Create an return the attribute urn for the given attribute 220 | * 221 | * @param attributeName 222 | * @returns {string|null} 223 | */ 224 | getAttributeUrn(attributeName: string) { 225 | const attribute = this.identityAttributes[attributeName]; 226 | if (attribute) { 227 | return `urn:bap:id:${attributeName}:${attribute.value}:${attribute.nonce}`; 228 | } 229 | 230 | return null; 231 | } 232 | 233 | /** 234 | * Add an attribute to this identity 235 | * 236 | * @param attributeName 237 | * @param value 238 | * @param nonce 239 | */ 240 | addAttribute(attributeName: string, value: any, nonce = ''): void { 241 | if (!nonce) { 242 | nonce = Utils.getRandomString(); 243 | } 244 | 245 | this.identityAttributes[attributeName] = { 246 | value, 247 | nonce, 248 | }; 249 | } 250 | 251 | /** 252 | * This should be called with the last part of the signing path (/.../.../...) 253 | * This library assumes the first part is m/424150'/0'/0' as defined at the top of this file 254 | * 255 | * @param path The second path of the signing path in the format [0-9]{0,9}/[0-9]{0,9}/[0-9]{0,9} 256 | */ 257 | set rootPath(path) { 258 | if (this.#HDPrivateKey) { 259 | if (path.split('/').length < 5) { 260 | path = `${SIGNING_PATH_PREFIX}${path}`; 261 | } 262 | 263 | if (!this.validatePath(path)) { 264 | throw new Error('invalid signing path given ' + path); 265 | } 266 | 267 | this.#rootPath = path; 268 | 269 | const derivedChild = this.#HDPrivateKey.deriveChild(path); 270 | this.rootAddress = derivedChild.publicKey.toAddress().toString(); 271 | // Identity keys should be derivatives of the root address - this allows checking 272 | // of the creation transaction 273 | this.identityKey = this.deriveIdentityKey(this.rootAddress); 274 | 275 | // we also set this previousPath / currentPath to the root as we seem to be (re)setting this ID 276 | this.#previousPath = path; 277 | this.#currentPath = path; 278 | } 279 | } 280 | 281 | get rootPath(): string { 282 | return this.#rootPath; 283 | } 284 | 285 | getRootPath(): string { 286 | return this.#rootPath; 287 | } 288 | 289 | /** 290 | * This should be called with the last part of the signing path (/.../.../...) 291 | * This library assumes the first part is m/424150'/0'/0' as defined at the top of this file 292 | * 293 | * @param path The second path of the signing path in the format [0-9]{0,9}/[0-9]{0,9}/[0-9]{0,9} 294 | */ 295 | set currentPath(path) { 296 | if (path.split('/').length < 5) { 297 | path = `${SIGNING_PATH_PREFIX}${path}`; 298 | } 299 | 300 | if (!this.validatePath(path)) { 301 | throw new Error('invalid signing path given'); 302 | } 303 | 304 | this.#previousPath = this.#currentPath; 305 | this.#currentPath = path; 306 | } 307 | 308 | get currentPath(): string { 309 | return this.#currentPath; 310 | } 311 | 312 | get previousPath(): string { 313 | return this.#previousPath; 314 | } 315 | 316 | /** 317 | * This can be used to break the deterministic way child keys are created to make it harder for 318 | * an attacker to steal the identites when the root key is compromised. This does however require 319 | * the seeds to be stored at all times. If the seed is lost, the identity will not be recoverable. 320 | */ 321 | get idSeed(): string { 322 | return this.#idSeed; 323 | } 324 | 325 | /** 326 | * Increment current path to a new path 327 | * 328 | * @returns {*} 329 | */ 330 | incrementPath(): void { 331 | this.currentPath = Utils.getNextPath(this.currentPath); 332 | } 333 | 334 | /** 335 | * Check whether the given path is a valid path for use with this class 336 | * The signing paths used here always have a length of 3 337 | * 338 | * @param path The last part of the signing path (example "/0/0/1") 339 | * @returns {boolean} 340 | */ 341 | validatePath(path: string) { 342 | /* eslint-disable max-len */ 343 | if (path.match(/\/[0-9]{1,10}'?\/[0-9]{1,10}'?\/[0-9]{1,10}'?\/[0-9]{1,10}'?\/[0-9]{1,10}'?\/[0-9]{1,10}'?/)) { 344 | const pathValues = path.split('/'); 345 | if (pathValues.length === 7 346 | && Number(pathValues[1].replace('\'', '')) <= MAX_INT 347 | && Number(pathValues[2].replace('\'', '')) <= MAX_INT 348 | && Number(pathValues[3].replace('\'', '')) <= MAX_INT 349 | && Number(pathValues[4].replace('\'', '')) <= MAX_INT 350 | && Number(pathValues[5].replace('\'', '')) <= MAX_INT 351 | && Number(pathValues[6].replace('\'', '')) <= MAX_INT 352 | ) { 353 | return true; 354 | } 355 | } 356 | 357 | return false; 358 | } 359 | 360 | /** 361 | * Get the OP_RETURN for the initial ID transaction (signed with root address) 362 | * 363 | * @returns {[]} 364 | */ 365 | getInitialIdTransaction() { 366 | return this.getIdTransaction(this.#rootPath); 367 | } 368 | 369 | /** 370 | * Get the OP_RETURN for the ID transaction of the current address / path 371 | * 372 | * @returns {[]} 373 | */ 374 | getIdTransaction(previousPath = '') { 375 | if (this.#currentPath === this.#rootPath) { 376 | throw new Error('Current path equals rootPath. ID was probably not initialized properly'); 377 | } 378 | 379 | const opReturn = [ 380 | Buffer.from(BAP_BITCOM_ADDRESS).toString('hex'), 381 | Buffer.from('ID').toString('hex'), 382 | Buffer.from(this.identityKey).toString('hex'), 383 | Buffer.from(this.getCurrentAddress()).toString('hex'), 384 | ]; 385 | 386 | previousPath = previousPath || this.#previousPath; 387 | 388 | return this.signOpReturnWithAIP(opReturn, previousPath); 389 | } 390 | 391 | /** 392 | * Get address for given path 393 | * 394 | * @param path 395 | * @returns {*} 396 | */ 397 | getAddress(path: string): string { 398 | const derivedChild = this.#HDPrivateKey.deriveChild(path); 399 | return derivedChild.privateKey.publicKey.toAddress().toString(); 400 | } 401 | 402 | /** 403 | * Get current signing address 404 | * 405 | * @returns {*} 406 | */ 407 | getCurrentAddress(): string { 408 | return this.getAddress(this.#currentPath); 409 | } 410 | 411 | /** 412 | * Get the public key for encrypting data for this identity 413 | */ 414 | getEncryptionPublicKey(): string { 415 | const HDPrivateKey = this.#HDPrivateKey.deriveChild(this.#rootPath); 416 | const encryptionKey = HDPrivateKey.deriveChild(ENCRYPTION_PATH).privateKey; 417 | // @ts-ignore 418 | return encryptionKey.publicKey.toString('hex'); 419 | } 420 | 421 | /** 422 | * Get the public key for encrypting data for this identity, using a seed for the encryption 423 | */ 424 | getEncryptionPublicKeyWithSeed(seed: string): string { 425 | const encryptionKey = this.getEncryptionPrivateKeyWithSeed(seed); 426 | // @ts-ignore 427 | return encryptionKey.publicKey.toString('hex'); 428 | } 429 | 430 | /** 431 | * Encrypt the given string data with the identity encryption key 432 | * @param stringData 433 | * @param counterPartyPublicKey Optional public key of the counterparty 434 | * @return string Base64 435 | */ 436 | encrypt(stringData: string, counterPartyPublicKey?: string): string { 437 | const HDPrivateKey = this.#HDPrivateKey.deriveChild(this.#rootPath); 438 | const encryptionKey = HDPrivateKey.deriveChild(ENCRYPTION_PATH).privateKey; 439 | const publicKey = encryptionKey.publicKey; 440 | 441 | const ecies = new ECIES(); 442 | if (counterPartyPublicKey) { 443 | ecies.privateKey(encryptionKey) 444 | ecies.publicKey(counterPartyPublicKey); 445 | } else { 446 | ecies.publicKey(publicKey); 447 | } 448 | return ecies.encrypt(stringData).toString('base64'); 449 | } 450 | 451 | /** 452 | * Decrypt the given ciphertext with the identity encryption key 453 | * @param ciphertext 454 | * @param counterPartyPublicKey Optional public key of the counterparty 455 | */ 456 | decrypt(ciphertext: string, counterPartyPublicKey?: string): string { 457 | const HDPrivateKey = this.#HDPrivateKey.deriveChild(this.#rootPath); 458 | const encryptionKey = HDPrivateKey.deriveChild(ENCRYPTION_PATH).privateKey; 459 | const ecies = new ECIES(); 460 | ecies.privateKey(encryptionKey); 461 | if (counterPartyPublicKey) { 462 | ecies.publicKey(counterPartyPublicKey); 463 | } 464 | return ecies.decrypt(Buffer.from(ciphertext, 'base64')).toString(); 465 | } 466 | 467 | /** 468 | * Encrypt the given string data with the identity encryption key 469 | * @param stringData 470 | * @param seed String seed 471 | * @param counterPartyPublicKey Optional public key of the counterparty 472 | * @return string Base64 473 | */ 474 | encryptWithSeed(stringData: string, seed: string, counterPartyPublicKey?: string): string { 475 | const encryptionKey = this.getEncryptionPrivateKeyWithSeed(seed); 476 | const publicKey = encryptionKey.publicKey; 477 | 478 | const ecies = new ECIES(); 479 | if (counterPartyPublicKey) { 480 | ecies.privateKey(encryptionKey); 481 | ecies.publicKey(counterPartyPublicKey); 482 | } else { 483 | ecies.publicKey(publicKey); 484 | } 485 | return ecies.encrypt(stringData).toString('base64'); 486 | } 487 | 488 | /** 489 | * Decrypt the given ciphertext with the identity encryption key 490 | * @param ciphertext 491 | * @param seed String seed 492 | * @param counterPartyPublicKey Public key of the counterparty 493 | */ 494 | decryptWithSeed(ciphertext: string, seed: string, counterPartyPublicKey?: string): string { 495 | const encryptionKey = this.getEncryptionPrivateKeyWithSeed(seed); 496 | const ecies = new ECIES(); 497 | ecies.privateKey(encryptionKey); 498 | if (counterPartyPublicKey) { 499 | ecies.publicKey(counterPartyPublicKey); 500 | } 501 | return ecies.decrypt(Buffer.from(ciphertext, 'base64')).toString(); 502 | } 503 | 504 | private getEncryptionPrivateKeyWithSeed(seed: string) { 505 | const pathHex = bsv.crypto.Hash.sha256(Buffer.from(seed)).toString('hex'); 506 | const path = Utils.getSigningPathFromHex(pathHex); 507 | 508 | const HDPrivateKey = this.#HDPrivateKey.deriveChild(this.#rootPath); 509 | return HDPrivateKey.deriveChild(path).privateKey; 510 | } 511 | 512 | /** 513 | * Get an attestation string for the given urn for this identity 514 | * 515 | * @param urn 516 | * @returns {string} 517 | */ 518 | getAttestation(urn: string) { 519 | const urnHash = bsv.crypto.Hash.sha256(Buffer.from(urn)); 520 | return `bap:attest:${urnHash.toString('hex')}:${this.getIdentityKey()}`; 521 | } 522 | 523 | /** 524 | * Generate and return the attestation hash for the given attribute of this identity 525 | * 526 | * @param attribute Attribute name (name, email etc.) 527 | * @returns {string} 528 | */ 529 | getAttestationHash(attribute: string) { 530 | const urn = this.getAttributeUrn(attribute); 531 | if (!urn) return null; 532 | 533 | const attestation = this.getAttestation(urn); 534 | const attestationHash = bsv.crypto.Hash.sha256(Buffer.from(attestation)); 535 | 536 | return attestationHash.toString('hex'); 537 | } 538 | 539 | /** 540 | * Sign a message with the current signing address of this identity 541 | * 542 | * @param message 543 | * @param signingPath 544 | * @returns {{address, signature}} 545 | */ 546 | signMessage(message: string | Buffer, signingPath = '') { 547 | if (!(message instanceof Buffer)) { 548 | message = Buffer.from(message); 549 | } 550 | 551 | signingPath = signingPath || this.#currentPath; 552 | const derivedChild = this.#HDPrivateKey.deriveChild(signingPath); 553 | const address = derivedChild.privateKey.publicKey.toAddress().toString(); 554 | const signature = Message(message).sign(derivedChild.privateKey); 555 | 556 | return { address, signature }; 557 | } 558 | 559 | /** 560 | * Sign a message using a key based on the given string seed 561 | * 562 | * This works by creating a private key from the root key of this identity. It will always 563 | * work with the rootPath / rootKey, to be deterministic. It will not change even if the keys 564 | * are rotated for this ID. 565 | * 566 | * This is used in for instance deterministic login systems, that do not support BAP. 567 | * 568 | * @param message 569 | * @param seed {string} String seed that will be used to generate a path 570 | */ 571 | signMessageWithSeed(message: string, seed: string): { address: string, signature: string } { 572 | const pathHex = bsv.crypto.Hash.sha256(Buffer.from(seed)).toString('hex'); 573 | const path = Utils.getSigningPathFromHex(pathHex); 574 | 575 | const HDPrivateKey = this.#HDPrivateKey.deriveChild(this.#rootPath); 576 | const derivedChild = HDPrivateKey.deriveChild(path); 577 | const address = derivedChild.privateKey.publicKey.toAddress().toString(); 578 | const signature = Message(message).sign(derivedChild.privateKey); 579 | 580 | return { address, signature }; 581 | } 582 | 583 | /** 584 | * Sign an op_return hex array with AIP 585 | * @param opReturn {array} 586 | * @param signingPath {string} 587 | * @param outputType {string} 588 | * @return {[]} 589 | */ 590 | signOpReturnWithAIP(opReturn: string[], signingPath = '', outputType: BufferEncoding = 'hex'): string[] { 591 | const aipMessageBuffer = this.getAIPMessageBuffer(opReturn); 592 | const { address, signature } = this.signMessage(aipMessageBuffer, signingPath); 593 | 594 | return opReturn.concat([ 595 | Buffer.from('|').toString(outputType), 596 | Buffer.from(AIP_BITCOM_ADDRESS).toString(outputType), 597 | Buffer.from('BITCOIN_ECDSA').toString(outputType), 598 | Buffer.from(address).toString(outputType), 599 | Buffer.from(signature, 'base64').toString(outputType), 600 | ]); 601 | } 602 | 603 | /** 604 | * Construct an AIP buffer from the op return data 605 | * @param opReturn 606 | * @returns {Buffer} 607 | */ 608 | getAIPMessageBuffer(opReturn: string[]): Buffer { 609 | const buffers = []; 610 | if (opReturn[0].replace('0x', '') !== '6a') { 611 | // include OP_RETURN in constructing the signature buffer 612 | buffers.push(Buffer.from('6a', 'hex')); 613 | } 614 | opReturn.forEach((op) => { 615 | buffers.push(Buffer.from(op.replace('0x', ''), 'hex')); 616 | }); 617 | // add a trailing "|" - this is the AIP way 618 | buffers.push(Buffer.from('|')); 619 | 620 | return Buffer.concat([...buffers]); 621 | } 622 | 623 | /** 624 | * Get all signing keys for this identity 625 | */ 626 | async getIdSigningKeys(): Promise { 627 | const signingKeys = await this.getApiData('/signing-keys', { 628 | idKey: this.identityKey, 629 | }); 630 | console.log('getIdSigningKeys', signingKeys); 631 | 632 | return signingKeys; 633 | } 634 | 635 | /** 636 | * Get all attestations for the given attribute 637 | * 638 | * @param attribute 639 | */ 640 | async getAttributeAttestations(attribute: string): Promise { 641 | // This function needs to make a call to a BAP server to get all the attestations for this 642 | // identity for the given attribute 643 | const attestationHash = this.getAttestationHash(attribute); 644 | 645 | // get all BAP ATTEST records for the given attestationHash 646 | const attestations = await this.getApiData('/attestations', { 647 | hash: attestationHash, 648 | }); 649 | console.log('getAttestations', attribute, attestationHash, attestations); 650 | 651 | return attestations; 652 | } 653 | 654 | /** 655 | * Helper function to get attestation from a BAP API server 656 | * 657 | * @param apiUrl 658 | * @param apiData 659 | * @returns {Promise} 660 | */ 661 | async getApiData(apiUrl: string, apiData: any): Promise { 662 | const url = `${this.#BAP_SERVER}${apiUrl}`; 663 | const response = await fetch(url, { 664 | method: 'post', 665 | headers: { 666 | 'Content-type': 'application/json; charset=utf-8', 667 | token: this.#BAP_TOKEN, 668 | format: 'json', 669 | }, 670 | body: JSON.stringify(apiData), 671 | }); 672 | return response.json(); 673 | } 674 | 675 | /** 676 | * Import an identity from a JSON object 677 | * 678 | * @param identity{{}} 679 | */ 680 | import(identity: Identity): void { 681 | this.idName = identity.name; 682 | this.description = identity.description || ''; 683 | this.identityKey = identity.identityKey; 684 | this.#rootPath = identity.rootPath; 685 | this.rootAddress = identity.rootAddress; 686 | this.#previousPath = identity.previousPath; 687 | this.#currentPath = identity.currentPath; 688 | this.#idSeed = identity.idSeed || ''; 689 | this.identityAttributes = this.parseAttributes(identity.identityAttributes); 690 | } 691 | 692 | /** 693 | * Export this identity to a JSON object 694 | * @returns {{}} 695 | */ 696 | export(): Identity { 697 | return { 698 | name: this.idName, 699 | description: this.description, 700 | identityKey: this.identityKey, 701 | rootPath: this.#rootPath, 702 | rootAddress: this.rootAddress, 703 | previousPath: this.#previousPath, 704 | currentPath: this.#currentPath, 705 | idSeed: this.#idSeed, 706 | identityAttributes: this.getAttributes(), 707 | lastIdPath: '' 708 | }; 709 | } 710 | } 711 | 712 | export { 713 | BAP_ID, 714 | } 715 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import bsv from 'bsv'; 2 | // @ts-ignore 3 | import Message from 'bsv/message'; 4 | // @ts-ignore 5 | import ECIES from 'bsv/ecies'; 6 | import 'node-fetch'; 7 | 8 | import { Utils } from './utils'; 9 | import { BAP_ID } from './id'; 10 | import { 11 | ENCRYPTION_PATH, 12 | BAP_SERVER, 13 | BAP_BITCOM_ADDRESS, 14 | BAP_BITCOM_ADDRESS_HEX, 15 | AIP_BITCOM_ADDRESS, 16 | } from './constants'; 17 | import {Attestation, Identity, PathPrefix} from "./interface"; 18 | 19 | /** 20 | * BAP class 21 | * 22 | * Creates an instance of the BAP class and uses the given HDPrivateKey for all BAP operations. 23 | * 24 | * @param HDPrivateKey 25 | */ 26 | export const BAP = class { 27 | #HDPrivateKey; 28 | #ids: { [key: string]: BAP_ID } = {}; 29 | #BAP_SERVER = BAP_SERVER; 30 | #BAP_TOKEN = ''; 31 | #lastIdPath = ''; 32 | 33 | constructor(HDPrivateKey: string, token = '') { 34 | if (!HDPrivateKey) { 35 | throw new Error('No HDPrivateKey given'); 36 | } else { 37 | // @ts-ignore 38 | this.#HDPrivateKey = bsv.HDPrivateKey(HDPrivateKey); 39 | } 40 | 41 | if (token) { 42 | this.#BAP_TOKEN = token; 43 | } 44 | } 45 | 46 | get lastIdPath(): string { 47 | return this.#lastIdPath; 48 | } 49 | 50 | /** 51 | * Get the public key of the given childPath, or of the current HDPrivateKey of childPath is empty 52 | * 53 | * @param childPath Full derivation path for this child 54 | * @returns {*} 55 | */ 56 | getPublicKey(childPath = ''): string { 57 | if (childPath) { 58 | return this.#HDPrivateKey.deriveChild(childPath).publicKey.toString(); 59 | } 60 | 61 | return this.#HDPrivateKey.publicKey.toString(); 62 | } 63 | 64 | /** 65 | * Get the public key of the given childPath, or of the current HDPrivateKey of childPath is empty 66 | * 67 | * @param childPath Full derivation path for this child 68 | * @returns {*} 69 | */ 70 | getHdPublicKey(childPath = ''): string { 71 | if (childPath) { 72 | return this.#HDPrivateKey.deriveChild(childPath).hdPublicKey.toString(); 73 | } 74 | 75 | return this.#HDPrivateKey.hdPublicKey.toString(); 76 | } 77 | 78 | set BAP_SERVER(bapServer) { 79 | this.#BAP_SERVER = bapServer; 80 | Object.keys(this.#ids).forEach((key) => { 81 | // @ts-ignore - does not recognize private fields that can be set 82 | this.#ids[key].BAP_SERVER = bapServer; 83 | }); 84 | } 85 | 86 | get BAP_SERVER(): string { 87 | return this.#BAP_SERVER; 88 | } 89 | 90 | set BAP_TOKEN(token) { 91 | this.#BAP_TOKEN = token; 92 | Object.keys(this.#ids).forEach((key) => { 93 | // @ts-ignore - does not recognize private fields that can be set 94 | this.#ids[key].BAP_TOKEN = token; 95 | }); 96 | } 97 | 98 | get BAP_TOKEN(): string { 99 | return this.#BAP_TOKEN; 100 | } 101 | 102 | /** 103 | * This function verifies that the given bapId matches the given root address 104 | * This is used as a data integrity check 105 | * 106 | * @param bapId BAP_ID instance 107 | */ 108 | checkIdBelongs(bapId: BAP_ID): boolean { 109 | const derivedChild = this.#HDPrivateKey.deriveChild(bapId.rootPath); 110 | const checkRootAddress = derivedChild.publicKey.toAddress().toString(); 111 | if (checkRootAddress !== bapId.rootAddress) { 112 | throw new Error('ID does not belong to this private key'); 113 | } 114 | 115 | return true; 116 | } 117 | 118 | /** 119 | * Returns a list of all the identity keys that are stored in this instance 120 | * 121 | * @returns {string[]} 122 | */ 123 | listIds(): string[] { 124 | return Object.keys(this.#ids); 125 | } 126 | 127 | /** 128 | * Create a new Id and link it to this BAP instance 129 | * 130 | * This function uses the length of the #ids of this class to determine the next valid path. 131 | * If not all ids related to this HDPrivateKey have been loaded, determine the path externally 132 | * and pass it to newId when creating a new ID. 133 | * 134 | * @param path 135 | * @param identityAttributes 136 | * @param idSeed 137 | * @returns {*} 138 | */ 139 | newId(path = '', identityAttributes: any = {}, idSeed = ''): BAP_ID { 140 | if (!path) { 141 | // get next usable path for this key 142 | path = this.getNextValidPath(); 143 | } 144 | 145 | const newIdentity = new BAP_ID(this.#HDPrivateKey, identityAttributes, idSeed); 146 | newIdentity.BAP_SERVER = this.#BAP_SERVER; 147 | newIdentity.BAP_TOKEN = this.#BAP_TOKEN; 148 | 149 | newIdentity.rootPath = path; 150 | newIdentity.currentPath = Utils.getNextPath(path); 151 | 152 | const idKey = newIdentity.getIdentityKey(); 153 | this.#ids[idKey] = newIdentity; 154 | this.#lastIdPath = path; 155 | 156 | return this.#ids[idKey]; 157 | } 158 | 159 | /** 160 | * Remove identity 161 | * 162 | * @param idKey 163 | * @returns {*} 164 | */ 165 | removeId(idKey: string): void { 166 | delete this.#ids[idKey]; 167 | } 168 | 169 | /** 170 | * Get the next valid path for the used HDPrivateKey and loaded #ids 171 | * 172 | * @returns {string} 173 | */ 174 | getNextValidPath(): PathPrefix { 175 | // prefer hardened paths 176 | if (this.#lastIdPath) { 177 | return Utils.getNextIdentityPath(this.#lastIdPath); 178 | } 179 | 180 | return `/0'/${Object.keys(this.#ids).length}'/0'`; 181 | } 182 | 183 | /** 184 | * Get a certain Id 185 | * 186 | * @param identityKey 187 | * @returns {null} 188 | */ 189 | getId(identityKey: string): BAP_ID | null { 190 | return this.#ids[identityKey] || null; 191 | } 192 | 193 | /** 194 | * This function is used when manipulating ID's, adding or removing attributes etc 195 | * First create an id through this class and then use getId to get it. Then you can add/edit or 196 | * increment the signing path and then re-set it with this function. 197 | * 198 | * Note: when you getId() from this class, you will be working on the same object as this class 199 | * has and any changes made will be propagated to the id in this class. When you call exportIds 200 | * your new changes will also be included, without having to setId(). 201 | * 202 | * @param bapId 203 | */ 204 | setId(bapId: BAP_ID): void { 205 | this.checkIdBelongs(bapId); 206 | this.#ids[bapId.getIdentityKey()] = bapId; 207 | } 208 | 209 | /** 210 | * This function is used to import IDs and attributes from some external storage 211 | * 212 | * The ID information should NOT be stored together with the HD private key ! 213 | * 214 | * @param idData Array of ids that have been exported 215 | * @param encrypted Whether the data should be treated as being encrypted (default true) 216 | */ 217 | importIds(idData: any, encrypted = true): void { 218 | if (encrypted) { 219 | // we first need to decrypt the ids array using ECIES 220 | const ecies = ECIES(); 221 | const derivedChild = this.#HDPrivateKey.deriveChild(ENCRYPTION_PATH); 222 | ecies.privateKey(derivedChild.privateKey); 223 | const decrypted = ecies.decrypt( 224 | Buffer.from(idData, Utils.isHex(idData) ? 'hex' : 'base64'), 225 | ).toString(); 226 | idData = JSON.parse(decrypted) as Identity[]; 227 | } 228 | 229 | let oldFormatImport = false; 230 | if (!idData.hasOwnProperty('ids')) { 231 | // old format id container 232 | oldFormatImport = true; 233 | idData = { 234 | lastIdPath: '', 235 | ids: idData, 236 | }; 237 | } 238 | 239 | idData.ids.forEach((id: Identity) => { 240 | if (!id.identityKey || !id.identityAttributes || !id.rootAddress) { 241 | throw new Error('ID cannot be imported as it is not complete'); 242 | } 243 | const importId = new BAP_ID(this.#HDPrivateKey, {}, id.idSeed); 244 | importId.BAP_SERVER = this.#BAP_SERVER; 245 | importId.BAP_TOKEN = this.#BAP_TOKEN; 246 | importId.import(id); 247 | 248 | this.checkIdBelongs(importId); 249 | 250 | this.#ids[importId.getIdentityKey()] = importId; 251 | 252 | if (oldFormatImport) { 253 | // overwrite with the last value on this array 254 | idData.lastIdPath = importId.currentPath; 255 | } 256 | }); 257 | 258 | this.#lastIdPath = idData.lastIdPath; 259 | } 260 | 261 | /** 262 | * Export all the IDs of this instance for external storage 263 | * 264 | * By default this function will encrypt the data, using a derivative child of the main HD key 265 | * 266 | * @param encrypted Whether the data should be encrypted (default true) 267 | * @returns {[]|*} 268 | */ 269 | exportIds(encrypted = true): any { 270 | const idData = { 271 | lastIdPath: this.#lastIdPath, 272 | ids: [] as Identity[], 273 | }; 274 | 275 | Object.keys(this.#ids) 276 | .forEach((key) => { 277 | idData.ids.push(this.#ids[key].export()); 278 | }); 279 | 280 | if (encrypted) { 281 | const ecies = ECIES(); 282 | const derivedChild = this.#HDPrivateKey.deriveChild(ENCRYPTION_PATH); 283 | ecies.publicKey(derivedChild.publicKey); 284 | return ecies.encrypt(JSON.stringify(idData)).toString('base64'); 285 | } 286 | 287 | return idData; 288 | } 289 | 290 | /** 291 | * Encrypt a string of data 292 | * 293 | * @param string 294 | * @returns {string} 295 | */ 296 | encrypt(string: string): string { 297 | const ecies = ECIES(); 298 | const derivedChild = this.#HDPrivateKey.deriveChild(ENCRYPTION_PATH); 299 | ecies.publicKey(derivedChild.publicKey); 300 | return ecies.encrypt(string).toString('base64'); 301 | } 302 | 303 | /** 304 | * Decrypt a string of data 305 | * 306 | * @param string 307 | * @returns {string} 308 | */ 309 | decrypt(string: string): string { 310 | const ecies = ECIES(); 311 | const derivedChild = this.#HDPrivateKey.deriveChild(ENCRYPTION_PATH); 312 | ecies.privateKey(derivedChild.privateKey); 313 | return ecies.decrypt(Buffer.from(string, 'base64')).toString(); 314 | } 315 | 316 | /** 317 | * Sign an attestation for a user 318 | * 319 | * @param attestationHash The computed attestation hash for the user - this should be calculated with the BAP_ID class for an identity for the user 320 | * @param identityKey The identity key we are using for the signing 321 | * @param counter 322 | * @param dataString Optional data string that will be appended to the BAP attestation 323 | * @returns {string[]} 324 | */ 325 | signAttestationWithAIP(attestationHash: string, identityKey: string, counter = 0, dataString = '') { 326 | const id = this.getId(identityKey); 327 | if (!id) { 328 | throw new Error('Could not find identity to attest with'); 329 | } 330 | 331 | const attestationBuffer = this.getAttestationBuffer(attestationHash, counter, dataString); 332 | const { address, signature } = id.signMessage(attestationBuffer); 333 | 334 | return this.createAttestationTransaction( 335 | attestationHash, 336 | counter, 337 | address, 338 | signature, 339 | dataString, 340 | ); 341 | } 342 | 343 | /** 344 | * Verify an AIP signed attestation for a user 345 | * 346 | * [ 347 | * '0x6a', 348 | * '0x31424150537561506e66476e53424d33474c56397968785564596534764762644d54', 349 | * '0x415454455354', 350 | * '0x33656166366361396334313936356538353831366439336439643034333136393032376633396661623034386333633031333663343364663635376462383761', 351 | * '0x30', 352 | * '0x7c', 353 | * '0x313550636948473232534e4c514a584d6f5355615756693757537163376843667661', 354 | * '0x424954434f494e5f4543445341', 355 | * '0x31477531796d52567a595557634638776f6f506a7a4a4c764d383550795a64655876', 356 | * '0x20ef60c5555001ddb1039bb0f215e46571fcb39ee46f48b089d1c08b0304dbcb3366d8fdf8bafd82be24b5ac42dcd6a5e96c90705dd42e3ad918b1b47ac3ce6ac2' 357 | * ] 358 | * 359 | * @param tx Array of hex values for the OP_RETURN values 360 | * @returns {{}} 361 | */ 362 | verifyAttestationWithAIP(tx: string[]): Attestation { 363 | if ( 364 | !Array.isArray(tx) 365 | || tx[0] !== '0x6a' 366 | || tx[1] !== BAP_BITCOM_ADDRESS_HEX 367 | ) { 368 | throw new Error('Not a valid BAP transaction'); 369 | } 370 | 371 | const dataOffset = tx[7] === '0x44415441' ? 5 : 0; // DATA 372 | const attestation: Attestation = { 373 | type: Utils.hexDecode(tx[2]), 374 | hash: Utils.hexDecode(tx[3]), 375 | sequence: Utils.hexDecode(tx[4]), 376 | signingProtocol: Utils.hexDecode(tx[7 + dataOffset]), 377 | signingAddress: Utils.hexDecode(tx[8 + dataOffset]), 378 | signature: Utils.hexDecode(tx[9 + dataOffset], 'base64'), 379 | }; 380 | 381 | if (dataOffset && tx[3] === tx[8]) { 382 | // valid data addition 383 | attestation.data = Utils.hexDecode(tx[9]); 384 | } 385 | 386 | try { 387 | const signatureBufferStatements = []; 388 | for (let i = 0; i < 6 + dataOffset; i++) { 389 | signatureBufferStatements.push(Buffer.from(tx[i].replace('0x', ''), 'hex')); 390 | } 391 | const attestationBuffer = Buffer.concat([ 392 | ...signatureBufferStatements, 393 | ]); 394 | attestation.verified = this.verifySignature( 395 | attestationBuffer, 396 | attestation.signingAddress, 397 | attestation.signature, 398 | ); 399 | } catch (e) { 400 | attestation.verified = false; 401 | } 402 | 403 | return attestation; 404 | } 405 | 406 | /** 407 | * For BAP attestations we use all fields for the attestation 408 | * 409 | * @param attestationHash 410 | * @param counter 411 | * @param address 412 | * @param signature 413 | * @param dataString Optional data string that will be appended to the BAP attestation 414 | * @returns {[string]} 415 | */ 416 | createAttestationTransaction( 417 | attestationHash: string, 418 | counter: number, 419 | address: string, 420 | signature: string, 421 | dataString = '', 422 | ): string[] { 423 | const transaction = ['0x6a', Utils.hexEncode(BAP_BITCOM_ADDRESS)]; 424 | transaction.push(Utils.hexEncode('ATTEST')); 425 | transaction.push(Utils.hexEncode(attestationHash)); 426 | transaction.push(Utils.hexEncode(`${counter}`)); 427 | transaction.push('0x7c'); // | 428 | if (dataString) { 429 | // data should be a string, either encrypted or stringified JSON if applicable 430 | transaction.push(Utils.hexEncode(BAP_BITCOM_ADDRESS)); 431 | transaction.push(Utils.hexEncode('DATA')); 432 | transaction.push(Utils.hexEncode(attestationHash)); 433 | transaction.push(Utils.hexEncode(dataString)); 434 | transaction.push('0x7c'); // | 435 | } 436 | transaction.push(Utils.hexEncode(AIP_BITCOM_ADDRESS)); 437 | transaction.push(Utils.hexEncode('BITCOIN_ECDSA')); 438 | transaction.push(Utils.hexEncode(address)); 439 | transaction.push('0x' + Buffer.from(signature, 'base64').toString('hex')); 440 | 441 | return transaction; 442 | } 443 | 444 | /** 445 | * This is a re-creation of how the bitcoinfiles-sdk creates a hash to sign for AIP 446 | * 447 | * @param attestationHash 448 | * @param counter 449 | * @param dataString Optional data string 450 | * @returns {Buffer} 451 | */ 452 | getAttestationBuffer(attestationHash: string, counter = 0, dataString = ''): Buffer { 453 | // re-create how AIP creates the buffer to sign 454 | let dataStringBuffer = Buffer.from(''); 455 | if (dataString) { 456 | dataStringBuffer = Buffer.concat([ 457 | Buffer.from(BAP_BITCOM_ADDRESS), 458 | Buffer.from('DATA'), 459 | Buffer.from(attestationHash), 460 | Buffer.from(dataString), 461 | Buffer.from('7c', 'hex'), 462 | ]); 463 | } 464 | return Buffer.concat([ 465 | Buffer.from('6a', 'hex'), // OP_RETURN 466 | Buffer.from(BAP_BITCOM_ADDRESS), 467 | Buffer.from('ATTEST'), 468 | Buffer.from(attestationHash), 469 | Buffer.from(`${counter}`), 470 | Buffer.from('7c', 'hex'), 471 | dataStringBuffer, 472 | ]); 473 | } 474 | 475 | /** 476 | * Verify that the identity challenge is signed by the address 477 | * 478 | * @param message Buffer or utf-8 string 479 | * @param address Bitcoin address of signee 480 | * @param signature Signature base64 string 481 | * 482 | * @return boolean 483 | */ 484 | verifySignature(message: string | Buffer, address: string, signature: string): boolean { 485 | // check the signature against the challenge 486 | const messageBuffer = Buffer.isBuffer(message) ? message : Buffer.from(message); 487 | return Message.verify( 488 | messageBuffer, 489 | address, 490 | signature, 491 | ); 492 | } 493 | 494 | /** 495 | * Check whether the given transaction (BAP OP_RETURN) is valid, is signed and that the 496 | * identity signing is also valid at the time of signing 497 | * 498 | * @param idKey 499 | * @param address 500 | * @param challenge 501 | * @param signature 502 | * 503 | * @returns {Promise} 504 | */ 505 | async verifyChallengeSignature( 506 | idKey: string, 507 | address: string, 508 | challenge: string, 509 | signature: string, 510 | ): Promise { 511 | // first we test locally before sending to server 512 | if (this.verifySignature(challenge, address, signature)) { 513 | const result = await this.getApiData('/attestation/valid', { 514 | idKey, 515 | challenge, 516 | signature, 517 | }); 518 | return result.data; 519 | } 520 | 521 | return false; 522 | } 523 | 524 | /** 525 | * Check whether the given transaction (BAP OP_RETURN) is valid, is signed and that the 526 | * identity signing is also valid at the time of signing 527 | * 528 | * @param tx 529 | * @returns {Promise} 530 | */ 531 | async isValidAttestationTransaction(tx: string[]): Promise { 532 | // first we test locally before sending to server 533 | if (this.verifyAttestationWithAIP(tx)) { 534 | return this.getApiData('/attestation/valid', { 535 | tx, 536 | }); 537 | } 538 | 539 | return false; 540 | } 541 | 542 | /** 543 | * Get all signing keys for the given idKey 544 | * 545 | * @param address 546 | * @returns {Promise<*>} 547 | */ 548 | async getIdentityFromAddress(address: string): Promise { 549 | return this.getApiData('/identity/from-address', { 550 | address, 551 | }); 552 | } 553 | 554 | /** 555 | * Get all signing keys for the given idKey 556 | * 557 | * @param idKey 558 | * @returns {Promise<*>} 559 | */ 560 | async getIdentity(idKey: string): Promise { 561 | return this.getApiData('/identity', { 562 | idKey, 563 | }); 564 | } 565 | 566 | /** 567 | * Get all attestations for the given attestation hash 568 | * 569 | * @param attestationHash 570 | */ 571 | async getAttestationsForHash(attestationHash: string): Promise { 572 | // get all BAP ATTEST records for the given attestationHash 573 | return this.getApiData('/attestations', { 574 | hash: attestationHash, 575 | }); 576 | } 577 | 578 | /** 579 | * Helper function to get attestation from a BAP API server 580 | * 581 | * @param apiUrl 582 | * @param apiData 583 | * @returns {Promise} 584 | */ 585 | async getApiData(apiUrl: string, apiData: any): Promise { 586 | const url = `${this.#BAP_SERVER}${apiUrl}`; 587 | const response = await fetch(url, { 588 | method: 'post', 589 | headers: { 590 | 'Content-type': 'application/json; charset=utf-8', 591 | token: this.#BAP_TOKEN, 592 | format: 'json', 593 | }, 594 | body: JSON.stringify(apiData), 595 | }); 596 | 597 | return response.json(); 598 | } 599 | }; 600 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Identity { 3 | name: string; 4 | description: string; 5 | identityKey: string; 6 | rootPath: string; 7 | rootAddress: string; 8 | previousPath: string; 9 | currentPath: string; 10 | lastIdPath: string; 11 | idSeed: string; 12 | identityAttributes: any; 13 | } 14 | 15 | export type PathPrefix = `/${number}/${number}/${number}` | `/${number}'/${number}'/${number}'` 16 | 17 | export interface Attestation { 18 | type: string; 19 | hash: string; 20 | sequence: string; 21 | signingProtocol: string; 22 | signingAddress: string; 23 | signature: string; 24 | data?: string; 25 | verified?: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /src/poa.ts: -------------------------------------------------------------------------------- 1 | import bsv from 'bsv'; 2 | 3 | export const BAP_POA = class { 4 | #HDPrivateKey: bsv.HDPrivateKey; 5 | 6 | constructor(HDPrivateKey: bsv.HDPrivateKey) { 7 | this.#HDPrivateKey = HDPrivateKey; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import bsv from 'bsv'; 2 | import {PathPrefix} from "./interface"; 3 | 4 | export const Utils = { 5 | /** 6 | * Helper function for encoding strings to hex 7 | * 8 | * @param string 9 | * @returns {string} 10 | */ 11 | hexEncode(string: string) { 12 | return '0x' + Buffer.from(string).toString('hex'); 13 | }, 14 | 15 | /** 16 | * Helper function for encoding strings to hex 17 | * 18 | * @param hexString string 19 | * @param encoding BufferEncoding 20 | * @returns {string} 21 | */ 22 | hexDecode(hexString: string, encoding: BufferEncoding = 'utf8') { 23 | return Buffer.from(hexString.replace('0x', ''), 'hex').toString(encoding); 24 | }, 25 | 26 | /** 27 | * Helper function to generate a random nonce 28 | * 29 | * @returns {string} 30 | */ 31 | getRandomString(length = 32) { 32 | return bsv.crypto.Random.getRandomBuffer(length).toString('hex'); 33 | }, 34 | 35 | /** 36 | * Test whether the given string is hex 37 | * 38 | * @param value any 39 | * @returns {boolean} 40 | */ 41 | isHex(value: any) { 42 | if (typeof value !== 'string') { 43 | return false; 44 | } 45 | return /^[0-9a-fA-F]+$/.test(value); 46 | }, 47 | 48 | /** 49 | * Get a signing path from a hex number 50 | * 51 | * @param hexString {string} 52 | * @param hardened {boolean} Whether to return a hardened path 53 | * @returns {string} 54 | */ 55 | getSigningPathFromHex(hexString: string, hardened = true) { 56 | // "m/0/0/1" 57 | let signingPath = 'm'; 58 | const signingHex = hexString.match(/.{1,8}/g); 59 | const maxNumber = 2147483648 - 1; // 0x80000000 60 | signingHex?.forEach((hexNumber) => { 61 | let number = Number('0x' + hexNumber); 62 | if (number > maxNumber) number -= maxNumber; 63 | signingPath += `/${number}${(hardened ? "'" : '')}`; 64 | }); 65 | 66 | return signingPath; 67 | }, 68 | 69 | /** 70 | * Increment that second to last part from the given part, set the last part to 0 71 | * 72 | * @param path string 73 | * @returns {*} 74 | */ 75 | getNextIdentityPath(path: string): PathPrefix { 76 | const pathValues = path.split('/'); 77 | const secondToLastPart = pathValues[pathValues.length - 2]; 78 | 79 | let hardened = false; 80 | if (secondToLastPart.match('\'')) { 81 | hardened = true; 82 | } 83 | 84 | const nextPath = (Number(secondToLastPart.replace(/[^0-9]/g, '')) + 1).toString(); 85 | pathValues[pathValues.length - 2] = nextPath + (hardened ? '\'' : ''); 86 | pathValues[pathValues.length - 1] = '0' + (hardened ? '\'' : ''); 87 | 88 | return pathValues.join('/') as PathPrefix; 89 | }, 90 | 91 | /** 92 | * Increment that last part of the given path 93 | * 94 | * @param path string 95 | * @returns {*} 96 | */ 97 | getNextPath(path: string) { 98 | const pathValues = path.split('/'); 99 | const lastPart = pathValues[pathValues.length - 1]; 100 | let hardened = false; 101 | if (lastPart.match('\'')) { 102 | hardened = true; 103 | } 104 | const nextPath = (Number(lastPart.replace(/[^0-9]/g, '')) + 1).toString(); 105 | pathValues[pathValues.length - 1] = nextPath + (hardened ? '\'' : ''); 106 | return pathValues.join('/'); 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /tests/data/ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "lastIdPath": "/26562456/876543/345346", 3 | "ids": [ 4 | { 5 | "name": "Social media id", 6 | "description": "Safe to share with social media companies", 7 | "identityKey": "4a59332b7d81c4c68a6edcb1160f4683037a97286b97cc500b5881632e921849z", 8 | "lastIdPath": "", 9 | "rootPath": "m/424150'/0'/0'/26562456/876543/0", 10 | "rootAddress": "12Ne5DNJLtXzR5TFGrZvRwJP4rxsBovgHj", 11 | "currentPath": "m/424150'/0'/0'/26562456/876543/345346", 12 | "previousPath": "m/424150'/0'/0'/26562456/876543/345345", 13 | "idSeed": "", 14 | "identityAttributes": { 15 | "uniKey": { 16 | "value": "042e65eedd59a4600919a985e5d74c4fb337059e03b17d6e270e149481fa3bc8", 17 | "nonce": "ea88e989c4d7383634057a21cfc0c5b854399f4f1955efdafc9292edf340d06b" 18 | }, 19 | "name": { 20 | "value": "John Doe", 21 | "nonce": "e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa" 22 | }, 23 | "email": { 24 | "value": "john.doe@example.com", 25 | "nonce": "2864fd138ab1e9ddaaea763c77a45898dac64a26229f9f3d0f2280e4bfa915de" 26 | } 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tests/data/keys.js: -------------------------------------------------------------------------------- 1 | export const HDPrivateKey = 'xprv9s21ZrQH143K2LcEfSnFRH1JvdKAcuZj2C8kAzCDnvqC4kgo417hYmAYQKdYDSzQSnQMLWXjDG42TgWwdYqwhAWTWpEBG1ighLLNnVHNKxx'; 2 | export const HDPublicKey = 'xpub661MyMwAqRbcEpghmUKFnQx3Uf9f2NHaPR4LyNbqMGNAwZ1wbYRx6ZV2FaimDygDPbrHYuii12mYCNwFRWnvXXKnh12CK17XMFGiqUYNwew'; 3 | -------------------------------------------------------------------------------- /tests/data/old-ids.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Social media id", 4 | "description": "Safe to share with social media companies", 5 | "identityKey": "4a59332b7d81c4c68a6edcb1160f4683037a97286b97cc500b5881632e921849z", 6 | "rootPath": "m/424150'/0'/0'/26562456/876543/0", 7 | "rootAddress": "12Ne5DNJLtXzR5TFGrZvRwJP4rxsBovgHj", 8 | "currentPath": "m/424150'/0'/0'/26562456/876543/345346", 9 | "previousPath": "m/424150'/0'/0'/26562456/876543/345345", 10 | "identityAttributes": { 11 | "uniKey": { 12 | "value": "042e65eedd59a4600919a985e5d74c4fb337059e03b17d6e270e149481fa3bc8", 13 | "nonce": "ea88e989c4d7383634057a21cfc0c5b854399f4f1955efdafc9292edf340d06b" 14 | }, 15 | "name": { 16 | "value": "John Doe", 17 | "nonce": "e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa" 18 | }, 19 | "email": { 20 | "value": "john.doe@example.com", 21 | "nonce": "2864fd138ab1e9ddaaea763c77a45898dac64a26229f9f3d0f2280e4bfa915de" 22 | } 23 | } 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /tests/data/test-vectors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "HDPrivateKey": "xprv9s21ZrQH143K3BcoP3NDU1L9HKbWim7fpQmLvVTJLWAiWWmxV4qxVadLRGJGFDWrGtQWGwvoLmo7GAGgTZuBcyqtRRRQxPsn78tZn97hndd", 4 | "rootPath": "m/424150'/0'/0'/0'/0'/0'", 5 | "rootAddress": "1wt1buQLx2G39adHovj2QJZnZK9vsXUjC", 6 | "rootSha256": "f8df2c6d1d7767fa1d26f8a0fb768420d86136b7a1b95622e9e971b59db0bb64", 7 | "rootRipemd160": "f8d991dcb289d17085544a3fcebff8042175f2bf", 8 | "rootBase58": "4U5eEMQSUdmPXeqmyQJtvELPNE8E", 9 | "idKey": "4U5eEMQSUdmPXeqmyQJtvELPNE8E", 10 | "tx": [ 11 | "31424150537561506e66476e53424d33474c56397968785564596534764762644d54", 12 | "4944", 13 | "34553565454d515355646d505865716d79514a7476454c504e453845", 14 | "314b314d316e71705752654a52314a546a7169334754456336535669327178455841", 15 | "7c", 16 | "313550636948473232534e4c514a584d6f5355615756693757537163376843667661", 17 | "424954434f494e5f4543445341", 18 | "317774316275514c78324733396164486f766a32514a5a6e5a4b39767358556a43", 19 | "20c84a5d41f9041841a5bec69c25d9b46c30705de07486c6b7bc22e0c806302b214a47783d43dfb5588d120a65b27a16b106483c130b4d381b7a12ab0e88cae5a9" 20 | ] 21 | }, 22 | { 23 | "HDPrivateKey": "xprv9s21ZrQH143K3GUBRVhWpwHTip7ejEBgU24rbJC6BdvqHtAoVopAGQj9K8FDvSVA9uSdvHtE1u5QNFND46VkyBQbtNvoqdQHN8s1cGV6NHX", 24 | "rootPath": "m/424150'/0'/0'/0'/0'/0'", 25 | "rootAddress": "17bBcqF6bfFWRRtLEvfup5Ez2z1UyJHQUi", 26 | "rootSha256": "cf96add68aa0d4203db2b5e534ec5fafb279fd40fb7889afe592739233652f10", 27 | "rootRipemd160": "d3278593d558f335a1b98fb773d81ea978a97f49", 28 | "rootBase58": "3wcztPg44xCLKi7pAt9qQHeb74Pr", 29 | "idKey": "3wcztPg44xCLKi7pAt9qQHeb74Pr", 30 | "tx": [ 31 | "31424150537561506e66476e53424d33474c56397968785564596534764762644d54", 32 | "4944", 33 | "3377637a745067343478434c4b693770417439715148656237345072", 34 | "315076755a3676464a4379325974476271366b4d4a677239375157624e7a58386351", 35 | "7c", 36 | "313550636948473232534e4c514a584d6f5355615756693757537163376843667661", 37 | "424954434f494e5f4543445341", 38 | "3137624263714636626646575252744c457666757035457a327a3155794a48515569", 39 | "206bb7e0d289b49d45fecf3e98d036d314d8d1d88b5f73b7e294b5b882183d9afb562f36d7a88891dda4a170f0df91305045f4ebf36eb6dc1fc9c8ec66c2b03cfd" 40 | ] 41 | }, 42 | { 43 | "HDPrivateKey": "xprv9s21ZrQH143K3QWj8yExWYjJx2cLrw2G353L9YC5DzYDGRWqGSFQ5nadQjmLSHidDiky8EAXU54XJeJEqPnZY7RbzZCuXTjHg2E97Gg787C", 44 | "rootPath": "m/424150'/0'/0'/0'/0'/0'", 45 | "rootAddress": "1DxxijXoBXrK2tiLLnr6uxQnfX53NQZN9T", 46 | "rootSha256": "708396bb94402a747895b484f8449e9d2032438900bcff050f3d20fc3d2df1ed", 47 | "rootRipemd160": "ac01980ce46158f40f79ea468770046f62d3f55f", 48 | "rootBase58": "3PzGsvDZXmm66mhVNLzSewHMW9in", 49 | "idKey": "3PzGsvDZXmm66mhVNLzSewHMW9in", 50 | "tx": [ 51 | "31424150537561506e66476e53424d33474c56397968785564596534764762644d54", 52 | "4944", 53 | "33507a477376445a586d6d36366d68564e4c7a536577484d5739696e", 54 | "314c6b7a694c773131524d6b57576e4c6b3379324842436d5835706d4b7647433566", 55 | "7c", 56 | "313550636948473232534e4c514a584d6f5355615756693757537163376843667661", 57 | "424954434f494e5f4543445341", 58 | "31447878696a586f4258724b3274694c4c6e72367578516e665835334e515a4e3954", 59 | "1f0028d13676149bb9b6cc927274e1ec410b5a66384c4aa03ddde91c7aabc5b06107a8e1f17ab5eac926869252e4f8ef11fae3d757cc8f9173191b0192db672990" 60 | ] 61 | }, 62 | { 63 | "HDPrivateKey": "xprv9s21ZrQH143K2n3FGVYiDqaFDQkbCxX8Z9MhLaHVmGsfr5HgCv5Y2vxkmwDeam7Fhb2zqGkfy5vyWL2RqDjHCoY5kf9p3ReV28WymTwsyiD", 64 | "rootPath": "m/424150'/0'/0'/0'/0'/0'", 65 | "rootAddress": "1DVN336g2rEfvrmWot5LufE47moMSSrguj", 66 | "rootSha256": "f99c96a904bd849d4a0aa489219a01d67d700f3005e7c86e932e287a3b245bc4", 67 | "rootRipemd160": "a18f9936007cf49d9f68c9f0b032786d3b6d3ad3", 68 | "rootBase58": "3FYk5crXc81pZZA17ffzofyTaqfp", 69 | "idKey": "3FYk5crXc81pZZA17ffzofyTaqfp", 70 | "tx": [ 71 | "31424150537561506e66476e53424d33474c56397968785564596534764762644d54", 72 | "4944", 73 | "3346596b35637258633831705a5a41313766667a6f66795461716670", 74 | "314370754474576b4545796835467837484b4c473474626e58694e34695053696977", 75 | "7c", 76 | "313550636948473232534e4c514a584d6f5355615756693757537163376843667661", 77 | "424954434f494e5f4543445341", 78 | "3144564e333336673272456676726d576f74354c75664534376d6f4d53537267756a", 79 | "1feb243ecdb32d091513c931337aba240f446ac6e01c777a05793e0d42e3d8ac3b23b088f5ef62da158428cdf0c4cb745e873ba9cfb0e6810d029fd473d0b4effe" 80 | ] 81 | }, 82 | { 83 | "HDPrivateKey": "xprv9s21ZrQH143K2xfRXanwL1Tx82Enucrq46ZKDqNsSsbW9X9smcDxGc16fnXQV4tMZg9HrskczDTthsS83XEwyWJHDTuxohTcZXEBxi6hTXT", 84 | "rootPath": "m/424150'/0'/0'/0'/0'/0'", 85 | "rootAddress": "16Kd8HcQ79fa6BFWVRK6PsDNhkxoubNWrS", 86 | "rootSha256": "2e76901a9712c3925d89ca778bc236e2c1d850a8f69fc0f23269ba70da7c34cd", 87 | "rootRipemd160": "95966d63808eb299f1492ff07078cffe3a51d7f3", 88 | "rootBase58": "35sbjSD55abC3N5hLtbN5kWXhLhx", 89 | "idKey": "35sbjSD55abC3N5hLtbN5kWXhLhx", 90 | "tx": [ 91 | "31424150537561506e66476e53424d33474c56397968785564596534764762644d54", 92 | "4944", 93 | "333573626a53443535616243334e35684c74624e356b5758684c6878", 94 | "31373867655678766a65464531576354636f6e6f6e734367537356353752645432", 95 | "7c", 96 | "313550636948473232534e4c514a584d6f5355615756693757537163376843667661", 97 | "424954434f494e5f4543445341", 98 | "31364b6438486351373966613642465756524b365073444e686b786f75624e577253", 99 | "1f690cbc0a1bbc7acee8e378d54d5e646011f8e36b08f6bb3f0eb5f972a4e9c49a4865eb97334805934fac37d7e3a3365b540d1008321d80bb9e0785b866569708" 100 | ] 101 | }, 102 | { 103 | "HDPrivateKey": "xprv9s21ZrQH143K3gjDedgKXZTj3VtS91p6rdm5Zcdu4WmT1zLcLTZUWhzbzNAAtZximg6s1ejJiaBCRt8qnmaAWu7SgFvTZvqmE3KDTPtrpBv", 104 | "rootPath": "m/424150'/0'/0'/0'/0'/0'", 105 | "rootAddress": "1M7NLLBwu6BzCWDeMFP18SqYAmUtyRfJwf", 106 | "rootSha256": "9003948e05cfb70b2e97626420cabf2eb874a09f7a6aceebd91dcdecb21a020e", 107 | "rootRipemd160": "c4b12bf6f7adfcebc546d036d868cfd5ad5497e7", 108 | "rootBase58": "3jwCu5ZcixQBfBDk3JhUbfj9ZdGr", 109 | "idKey": "3jwCu5ZcixQBfBDk3JhUbfj9ZdGr", 110 | "tx": [ 111 | "31424150537561506e66476e53424d33474c56397968785564596534764762644d54", 112 | "4944", 113 | "336a774375355a63697851426642446b334a685562666a395a644772", 114 | "314d726e725a74726967527632547652516767347973714a504a637539746f4c7654", 115 | "7c", 116 | "313550636948473232534e4c514a584d6f5355615756693757537163376843667661", 117 | "424954434f494e5f4543445341", 118 | "314d374e4c4c42777536427a435744654d46503138537159416d55747952664a7766", 119 | "1f09ddcbaf387768982a510533af8f6deb1d7d32e85f813388d931c07950b57fc0749e1b03772eb88245d78acf6fcafae367a9ac83d6a518faed23527361c9fc57" 120 | ] 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /tests/id.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, beforeEach, afterEach, test, } from '@jest/globals'; 2 | import { BAP } from '../src'; 3 | import { BAP_ID } from '../src/id'; 4 | import { HDPrivateKey } from './data/keys'; 5 | import bsv from 'bsv'; 6 | import { 7 | BAP_BITCOM_ADDRESS_HEX, 8 | AIP_BITCOM_ADDRESS_HEX, SIGNING_PATH_PREFIX, 9 | } from '../src/constants'; 10 | 11 | const identityAttributes = { 12 | name: { 13 | value: 'John Doe', 14 | nonce: 'e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa', 15 | }, 16 | email: { 17 | value: 'john.doe@example.com', 18 | nonce: '2864fd138ab1e9ddaaea763c77a45898dac64a26229f9f3d0f2280e4bfa915de', 19 | }, 20 | }; 21 | const identityAttributeStrings = ` 22 | urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa 23 | urn:bap:id:email:john.doe@example.com:2864fd138ab1e9ddaaea763c77a45898dac64a26229f9f3d0f2280e4bfa915de 24 | `; 25 | 26 | let bap; 27 | 28 | describe('bap-id', () => { 29 | beforeEach(() => { 30 | bap = new BAP(HDPrivateKey); 31 | }); 32 | 33 | test('new id', () => { 34 | const bapId = bap.newId(); 35 | 36 | const identityKey = bapId.getIdentityKey(); 37 | expect(typeof identityKey).toBe('string'); 38 | expect(identityKey).toHaveLength(27); 39 | expect(bapId.getAttributes()).toMatchObject({}); 40 | }); 41 | 42 | test('new id with known key', () => { 43 | const userId = new BAP_ID(bsv.HDPrivateKey(HDPrivateKey)); 44 | const rootAddress = userId.rootAddress; 45 | const identityKey = userId.getIdentityKey(); 46 | expect(rootAddress).toBe('1CSJiMMYzfW8gbhXXNYyEJ1NsWJohLXyet'); 47 | expect(identityKey).toBe('2cWvSXKfFQScCgDFssRPKvDLjNYx'); 48 | }); 49 | 50 | test('new id with seeded keys', () => { 51 | const userId = new BAP_ID(bsv.HDPrivateKey(HDPrivateKey), {}, 'test'); 52 | const rootAddress = userId.rootAddress; 53 | const identityKey = userId.getIdentityKey(); 54 | expect(rootAddress).toBe('189oxMiD6wFA4nD38CkoWBKragxXUfw26J'); 55 | expect(identityKey).toBe('ffw3VszEVByph2DuHUiswEMNjRm'); 56 | 57 | const userId2 = new BAP_ID(bsv.HDPrivateKey(HDPrivateKey), {}, 'testing 123'); 58 | const rootAddress2 = userId2.rootAddress; 59 | const identityKey2 = userId2.getIdentityKey(); 60 | expect(rootAddress2).toBe('18zrzzv2Nieve7QAj2AwGDcPYyBziz8vWk'); 61 | expect(identityKey2).toBe('2UKj9321g9pDExCjL7dPhXMtM326'); 62 | }); 63 | 64 | test('set BAP_SERVER', () => { 65 | const bap = new BAP(HDPrivateKey); 66 | const id = bap.newId(); 67 | expect(id.BAP_SERVER).toBe('https://bap.network/api/v1'); 68 | 69 | const newServer = 'https://bapdev.legallychained.com/'; 70 | id.BAP_SERVER = newServer; 71 | expect(id.BAP_SERVER).toBe(newServer); 72 | }); 73 | 74 | test('parseAttributes', () => { 75 | const bapId = bap.newId(); 76 | const parsed = bapId.parseAttributes(identityAttributes); 77 | expect(parsed).toStrictEqual(identityAttributes); 78 | 79 | const parsed2 = bapId.parseAttributes(identityAttributeStrings); 80 | expect(parsed2).toStrictEqual(identityAttributes); 81 | }); 82 | 83 | test('parseStringUrns', () => { 84 | const bapId = bap.newId(); 85 | const parsed = bapId.parseStringUrns(identityAttributeStrings); 86 | expect(parsed).toStrictEqual(identityAttributes); 87 | 88 | expect(() => { 89 | bapId.parseStringUrns({value: 'John Doe', nonce: ''}); 90 | }).toThrow(); 91 | }); 92 | 93 | test('attributes', () => { 94 | const bapId = bap.newId(false, identityAttributes); 95 | bapId.addAttribute('birthday', '1990-05-22'); // nonce will be automatically generated 96 | bapId.addAttribute('over18', '1', 'ca17ccaacd671b28dc811332525f2f2cd594d8e8e7825de515ce5d52d30e8'); 97 | 98 | expect(bapId.getAttribute('name').value).toBe('John Doe'); 99 | expect(bapId.getAttribute('name').nonce).toBe('e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa'); 100 | 101 | expect(bapId.getAttribute('birthday').value).toBe('1990-05-22'); 102 | expect(typeof bapId.getAttribute('birthday').nonce).toBe('string'); 103 | expect(bapId.getAttribute('birthday').nonce).toHaveLength(64); 104 | 105 | expect(bapId.getAttribute('over18').value).toBe('1'); 106 | expect(bapId.getAttribute('over18').nonce).toBe('ca17ccaacd671b28dc811332525f2f2cd594d8e8e7825de515ce5d52d30e8'); 107 | 108 | expect(bapId.getAttribute('over21')).toBe(null); 109 | }); 110 | 111 | test('getAttributeUrns', () => { 112 | const bapId = bap.newId(false, identityAttributes); 113 | 114 | expect(bapId.getAttributeUrn('name')).toBe('urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa'); 115 | expect(bapId.getAttributeUrn('over21')).toBe(null); 116 | 117 | const attributeStrings = bapId.getAttributeUrns(); 118 | expect(attributeStrings).toBe(`urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa 119 | urn:bap:id:email:john.doe@example.com:2864fd138ab1e9ddaaea763c77a45898dac64a26229f9f3d0f2280e4bfa915de 120 | `); 121 | }); 122 | 123 | test('incrementPath', () => { 124 | const randomHDPrivateKey = bsv.HDPrivateKey.fromRandom(); 125 | const bapId = new BAP_ID(randomHDPrivateKey); 126 | 127 | expect(bapId.currentPath).toBe(`${SIGNING_PATH_PREFIX}/0/0/1`); 128 | bapId.incrementPath(); 129 | expect(bapId.previousPath).toBe(`${SIGNING_PATH_PREFIX}/0/0/1`); 130 | expect(bapId.currentPath).toBe(`${SIGNING_PATH_PREFIX}/0/0/2`); 131 | bapId.incrementPath(); 132 | expect(bapId.previousPath).toBe(`${SIGNING_PATH_PREFIX}/0/0/2`); 133 | expect(bapId.currentPath).toBe(`${SIGNING_PATH_PREFIX}/0/0/3`); 134 | bapId.incrementPath(); 135 | expect(bapId.previousPath).toBe(`${SIGNING_PATH_PREFIX}/0/0/3`); 136 | expect(bapId.currentPath).toBe(`${SIGNING_PATH_PREFIX}/0/0/4`); 137 | }); 138 | 139 | test('signingPath', () => { 140 | const bapId = bap.newId(); 141 | expect(bapId.rootPath).toBe("m/424150'/0'/0'/0'/0'/0'"); 142 | expect(bapId.currentPath).toBe("m/424150'/0'/0'/0'/0'/1'"); 143 | 144 | bapId.currentPath = '/0/0/2'; 145 | expect(bapId.currentPath).toBe("m/424150'/0'/0'/0/0/2"); 146 | 147 | expect(() => { 148 | bapId.rootPath = 'test'; 149 | }).toThrow(); 150 | expect(() => { 151 | bapId.currentPath = 'test'; 152 | }).toThrow(); 153 | }); 154 | 155 | test('getAttestation / Hash', () => { 156 | const bapId = bap.newId(false, identityAttributes); 157 | const urn = bapId.getAttributeUrn('name'); 158 | expect(urn).toBe('urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa'); 159 | const attestation = bapId.getAttestation(urn); 160 | expect(attestation).toBe('bap:attest:b17c8e606afcf0d8dca65bdf8f33d275239438116557980203c82b0fae259838:GbPKb7tQpfZDut9mJnBm5BMtGqu'); 161 | 162 | const hash = bapId.getAttestationHash('name'); 163 | expect(hash).toBe('bc91964394e81cb0fc0a0cad53894456711e2f7e4626ce3977de0a92abdded70'); 164 | }); 165 | 166 | test('getInitialIdTransaction', () => { 167 | const bapId = bap.newId(false, identityAttributes); 168 | const tx = bapId.getInitialIdTransaction(); 169 | expect('0x' + tx[0]).toBe(BAP_BITCOM_ADDRESS_HEX); 170 | expect(tx[1]).toBe(Buffer.from('ID').toString('hex')); 171 | expect(tx[2]).toBe(Buffer.from(bapId.getIdentityKey()).toString('hex')); 172 | expect(tx[3]).toBe(Buffer.from(bapId.getAddress(bapId.currentPath)).toString('hex')); 173 | expect(tx[4]).toBe(Buffer.from('|').toString('hex')); 174 | expect('0x' + tx[5]).toBe(AIP_BITCOM_ADDRESS_HEX); 175 | expect(tx[6]).toBe(Buffer.from('BITCOIN_ECDSA').toString('hex')); 176 | expect(tx[7]).toBe(Buffer.from(bapId.getAddress(bapId.rootPath)).toString('hex')); 177 | expect(typeof tx[8]).toBe('string'); 178 | }); 179 | 180 | test('encryption public keys', () => { 181 | const bapId = bap.newId(false, identityAttributes); 182 | const pubKey = bapId.getEncryptionPublicKey(); 183 | expect(pubKey).toBe('02a257adfbba04a25a7c37600209a0926aa264428b2d3d2b17fa97cf9c31b87cdf'); 184 | 185 | const pubKeySeed = bapId.getEncryptionPublicKeyWithSeed('test-seed'); 186 | expect(pubKeySeed).toBe('0344786ed9e861b40b1157e841d6f0f7667548f03adff2709ebd74061068f8376a'); 187 | }); 188 | 189 | test('encryption', () => { 190 | const bapId = bap.newId(false, identityAttributes); 191 | const pubKey = bapId.getEncryptionPublicKey(); 192 | expect(pubKey).toBe('02a257adfbba04a25a7c37600209a0926aa264428b2d3d2b17fa97cf9c31b87cdf'); 193 | 194 | const testData = 'This is a test we are going to encrypt'; 195 | const ciphertext = bapId.encrypt(testData); 196 | expect(typeof ciphertext).toBe('string'); 197 | expect(testData === ciphertext).toBe(false); 198 | 199 | const decrypted = bapId.decrypt(ciphertext); 200 | expect(testData === decrypted).toBe(true); 201 | }); 202 | 203 | test('encryption with counterparty', () => { 204 | const bapId = bap.newId(false, identityAttributes); 205 | const pubKey = bapId.getEncryptionPublicKey(); 206 | expect(pubKey).toBe('02a257adfbba04a25a7c37600209a0926aa264428b2d3d2b17fa97cf9c31b87cdf'); 207 | 208 | const counterPartyKey = bsv.PrivateKey.fromRandom().publicKey; 209 | 210 | const testData = 'This is a test we are going to encrypt for the counterparty'; 211 | const ciphertext = bapId.encrypt(testData, counterPartyKey); 212 | expect(typeof ciphertext).toBe('string'); 213 | expect(testData === ciphertext).toBe(false); 214 | 215 | const decrypted = bapId.decrypt(ciphertext, counterPartyKey); 216 | expect(testData === decrypted).toBe(true); 217 | }); 218 | 219 | test('encryption with seed', () => { 220 | const bapId = bap.newId(false, identityAttributes); 221 | const pubKey = bapId.getEncryptionPublicKey(); 222 | expect(pubKey).toBe('02a257adfbba04a25a7c37600209a0926aa264428b2d3d2b17fa97cf9c31b87cdf'); 223 | 224 | const seed = 'test-seed'; 225 | 226 | const testData = 'This is a test we are going to encrypt'; 227 | const ciphertext = bapId.encryptWithSeed(testData, seed); 228 | expect(typeof ciphertext).toBe('string'); 229 | expect(testData === ciphertext).toBe(false); 230 | 231 | const decrypted = bapId.decryptWithSeed(ciphertext, seed); 232 | expect(testData === decrypted).toBe(true); 233 | }); 234 | 235 | test('encryption with seed with counterparty', () => { 236 | const bapId = bap.newId(false, identityAttributes); 237 | const pubKey = bapId.getEncryptionPublicKey(); 238 | expect(pubKey).toBe('02a257adfbba04a25a7c37600209a0926aa264428b2d3d2b17fa97cf9c31b87cdf'); 239 | 240 | const seed = 'test-seed'; 241 | const counterPartyKey = bsv.PrivateKey.fromRandom().publicKey; 242 | 243 | const testData = 'This is a test we are going to encrypt'; 244 | const ciphertext = bapId.encryptWithSeed(testData, seed, counterPartyKey); 245 | expect(typeof ciphertext).toBe('string'); 246 | expect(testData === ciphertext).toBe(false); 247 | 248 | const decrypted = bapId.decryptWithSeed(ciphertext, seed, counterPartyKey); 249 | expect(testData === decrypted).toBe(true); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import bsv from 'bsv'; 2 | import Message from 'bsv/message'; 3 | import { describe, expect, test } from '@jest/globals'; 4 | 5 | import { BAP } from '../src'; 6 | import { SIGNING_PATH_PREFIX, ENCRYPTION_PATH } from '../src/constants'; 7 | import { BAP_ID } from '../src/id'; 8 | 9 | import { HDPrivateKey, HDPublicKey } from './data/keys'; 10 | import fullId from './data/ids.json'; 11 | import oldFullId from './data/old-ids.json'; 12 | 13 | const testBAPInstance = function (bap) { 14 | // TODO .... 15 | expect(bap).toMatchObject({}); 16 | expect(bap.getHdPublicKey()).toMatch('xpub'); 17 | }; 18 | const identityKey = '4a59332b7d81c4c68a6edcb1160f4683037a97286b97cc500b5881632e921849z'; 19 | 20 | describe('bap', () => { 21 | test('init', () => { 22 | const randomHDPrivateKey = bsv.HDPrivateKey.fromRandom().toString(); 23 | const bap = new BAP(randomHDPrivateKey); 24 | testBAPInstance(bap); 25 | }); 26 | 27 | test('init without key', () => { 28 | expect(() => { 29 | const bap = new BAP(); 30 | }).toThrow(); 31 | }); 32 | 33 | test('with known key', () => { 34 | const bap = new BAP(HDPrivateKey); 35 | testBAPInstance(bap); 36 | 37 | expect(bap.getPublicKey()).toBe('02c23e9fc6a959bb5315159ac7438c5a6bff37c7197326d1060b176e3969d72af5'); 38 | expect(bap.getPublicKey(ENCRYPTION_PATH)).toBe('02fc759e24d922c2d47766710613910c0f40bab7439777af9ad45bff55ec622994'); 39 | 40 | expect(bap.getHdPublicKey()).toBe(HDPublicKey); 41 | expect(bap.getHdPublicKey(ENCRYPTION_PATH)).toBe('xpub6CXbFY2NumUP1dVRRbXiAdj6oRhqK3zQjt1vdzsTBfy3jDLjHMTCCE7AX6fz1KqAag9EPGf52KyCAT9iovKrXZ74BSryrDQ2XBHiawuFfsu'); 42 | }); 43 | 44 | test('set BAP_SERVER', () => { 45 | const bap = new BAP(HDPrivateKey); 46 | const id = bap.newId(); 47 | 48 | expect(bap.BAP_SERVER).toEqual('https://bap.network/api/v1'); 49 | expect(id.BAP_SERVER).toEqual('https://bap.network/api/v1'); 50 | 51 | const newServer = 'https://bapdev.legallychained.com/'; 52 | bap.BAP_SERVER = newServer; 53 | expect(bap.BAP_SERVER).toEqual(newServer); 54 | expect(id.BAP_SERVER).toEqual(newServer); 55 | }); 56 | 57 | test('set BAP_TOKEN', () => { 58 | const bap = new BAP(HDPrivateKey); 59 | const id = bap.newId(); 60 | 61 | expect(bap.BAP_TOKEN).toEqual(''); 62 | expect(id.BAP_TOKEN).toEqual(''); 63 | 64 | const newToken = 'token_string'; 65 | bap.BAP_TOKEN = newToken; 66 | expect(bap.BAP_TOKEN).toEqual(newToken); 67 | expect(id.BAP_TOKEN).toEqual(newToken); 68 | 69 | const tokenAtInit = 'test_token' 70 | const bap2 = new BAP(HDPrivateKey, tokenAtInit); 71 | const id2 = bap2.newId(); 72 | expect(bap2.BAP_TOKEN).toEqual(tokenAtInit); 73 | expect(id2.BAP_TOKEN).toEqual(tokenAtInit); 74 | 75 | const newToken2 = 'token_string2'; 76 | bap2.BAP_TOKEN = newToken2; 77 | expect(bap2.BAP_TOKEN).toEqual(newToken2); 78 | expect(id2.BAP_TOKEN).toEqual(newToken2); 79 | }); 80 | 81 | test('import full ID', () => { 82 | const bap = new BAP(HDPrivateKey); 83 | bap.importIds(fullId, false); 84 | testBAPInstance(bap); 85 | 86 | expect(bap.listIds()).toStrictEqual([identityKey]); 87 | 88 | const importedId = bap.getId(identityKey); 89 | expect(importedId).toBeInstanceOf(BAP_ID); 90 | expect(importedId.getIdentityKey()).toBe(identityKey); 91 | }); 92 | 93 | test('import OLD full ID', () => { 94 | const bap = new BAP(HDPrivateKey); 95 | bap.importIds(oldFullId, false); 96 | testBAPInstance(bap); 97 | 98 | expect(bap.listIds()).toStrictEqual([identityKey]); 99 | 100 | const importedId = bap.getId(identityKey); 101 | expect(importedId).toBeInstanceOf(BAP_ID); 102 | expect(importedId.getIdentityKey()).toBe(identityKey); 103 | }); 104 | 105 | test('export full ID', () => { 106 | const bap = new BAP(HDPrivateKey); 107 | bap.importIds(fullId, false); 108 | testBAPInstance(bap); 109 | 110 | const exportData = bap.exportIds(false); 111 | expect(exportData).toStrictEqual(fullId); 112 | }); 113 | 114 | test('export/import encrypted ID', () => { 115 | const bap = new BAP(HDPrivateKey); 116 | bap.importIds(fullId, false); 117 | testBAPInstance(bap); 118 | 119 | const encryptedExportData = bap.exportIds(); 120 | expect(typeof encryptedExportData).toBe('string'); 121 | 122 | const bap2 = new BAP(HDPrivateKey); 123 | bap2.importIds(encryptedExportData); 124 | expect(bap2.listIds()).toStrictEqual([identityKey]); 125 | 126 | const importedId = bap2.getId(identityKey); 127 | expect(importedId).toBeInstanceOf(BAP_ID); 128 | expect(importedId.getIdentityKey()).toBe(identityKey); 129 | }); 130 | 131 | test('checkIdBelongs', () => { 132 | const randomHDPrivateKey = bsv.HDPrivateKey.fromRandom().toString(); 133 | const bap1 = new BAP(randomHDPrivateKey); 134 | const bap2 = new BAP(HDPrivateKey); 135 | 136 | const id1 = bap1.newId(); 137 | const id2 = bap2.newId(); 138 | 139 | expect(bap1.checkIdBelongs(id1)).toBe(true); 140 | expect(() => { 141 | bap1.checkIdBelongs(id2); 142 | }).toThrow(); 143 | 144 | expect(bap2.checkIdBelongs(id2)).toBe(true); 145 | expect(() => { 146 | bap2.checkIdBelongs(id1); 147 | }).toThrow(); 148 | }); 149 | 150 | test('getId / setId', () => { 151 | const bap = new BAP(HDPrivateKey); 152 | const newId = bap.newId(); 153 | const idKey = newId.getIdentityKey(); 154 | bap.setId(newId); 155 | 156 | expect(bap.getId('test')).toEqual(null); 157 | expect(bap.getId(idKey).identityKey).toStrictEqual(idKey); 158 | 159 | expect(() => { 160 | bap.setId({}); 161 | }).toThrow(); 162 | }); 163 | 164 | test('listIds', () => { 165 | const randomHDPrivateKey = bsv.HDPrivateKey.fromRandom().toString(); 166 | const bap = new BAP(randomHDPrivateKey); 167 | expect(bap.listIds()).toStrictEqual([]); 168 | 169 | const newId = bap.newId(); 170 | const idKey = newId.getIdentityKey(); 171 | bap.setId(newId); 172 | expect(bap.listIds()).toStrictEqual([idKey]); 173 | }); 174 | 175 | test('newId', () => { 176 | const randomHDPrivateKey = bsv.HDPrivateKey.fromRandom().toString(); 177 | const bap = new BAP(randomHDPrivateKey); 178 | const newId = bap.newId(); 179 | expect(newId).toBeInstanceOf(BAP_ID); 180 | expect(bap.checkIdBelongs(newId)).toBe(true); 181 | expect(newId.rootPath).toBe(`${SIGNING_PATH_PREFIX}/0'/0'/0'`); 182 | expect(newId.currentPath).toBe(`${SIGNING_PATH_PREFIX}/0'/0'/1'`); 183 | 184 | const newId2 = bap.newId('/123/124/0'); 185 | expect(newId2).toBeInstanceOf(BAP_ID); 186 | expect(bap.checkIdBelongs(newId2)).toBe(true); 187 | expect(newId2.rootPath).toBe(`${SIGNING_PATH_PREFIX}/123/124/0`); 188 | expect(newId2.currentPath).toBe(`${SIGNING_PATH_PREFIX}/123/124/1`); 189 | 190 | // Hardened path given 191 | const newId3 = bap.newId(`/123'/124'/0`); 192 | expect(newId3).toBeInstanceOf(BAP_ID); 193 | expect(bap.checkIdBelongs(newId3)).toBe(true); 194 | expect(newId3.rootPath).toBe(`${SIGNING_PATH_PREFIX}/123'/124'/0`); 195 | expect(newId3.currentPath).toBe(`${SIGNING_PATH_PREFIX}/123'/124'/1`); 196 | 197 | // Hardened full path given 198 | const newId4 = bap.newId(`/123'/124'/0'`); 199 | expect(newId4).toBeInstanceOf(BAP_ID); 200 | expect(bap.checkIdBelongs(newId4)).toBe(true); 201 | expect(newId4.rootPath).toBe(`${SIGNING_PATH_PREFIX}/123'/124'/0'`); 202 | expect(newId4.currentPath).toBe(`${SIGNING_PATH_PREFIX}/123'/124'/1'`); 203 | 204 | expect(() => { 205 | const newId4 = bap.newId('/123erg/124ggg/0'); 206 | }).toThrow(); 207 | }); 208 | 209 | test('verifyAttestationWithAIP', () => { 210 | // test in id 211 | const bap = new BAP(HDPrivateKey); 212 | expect(() => { 213 | bap.verifyAttestationWithAIP([]); 214 | }).toThrow(); 215 | }); 216 | 217 | test('import full BAP doc', () => { 218 | const fullBap = new BAP(HDPrivateKey); 219 | fullBap.importIds(fullId, false); 220 | 221 | const bapId = fullBap.getId(identityKey); 222 | expect(bapId.getAttribute('name').value).toBe('John Doe'); 223 | expect(bapId.getAttribute('name').nonce).toBe('e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa'); 224 | expect(bapId.getAttributeUrn('name')).toBe('urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa'); 225 | }); 226 | 227 | test('sign attestation with AIP', () => { 228 | const bap = new BAP(HDPrivateKey); 229 | bap.importIds(fullId, false); 230 | 231 | const userId = new BAP_ID(bsv.HDPrivateKey(HDPrivateKey), { 232 | "name": { 233 | "value": "John Doe", 234 | "nonce": "e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa" 235 | }, 236 | }); 237 | const attestationHash = userId.getAttestationHash('name'); 238 | expect(attestationHash).toBe('d6cbf280ad7515e549c7b154a02555fff3eeb05c6b245039813d39d3c0397b4a'); 239 | 240 | // create a signing transaction of the user's hash with our own identity key 241 | const transaction = bap.signAttestationWithAIP(attestationHash, identityKey); 242 | expect(transaction.length).toBe(10); 243 | const verify = bap.verifyAttestationWithAIP(transaction); 244 | expect(verify.verified).toBe(true); 245 | }); 246 | 247 | test('sign attestation with AIP and data', () => { 248 | const bap = new BAP(HDPrivateKey); 249 | bap.importIds(fullId, false); 250 | 251 | const userId = new BAP_ID(bsv.HDPrivateKey(HDPrivateKey), { 252 | "name": { 253 | "value": "John Doe", 254 | "nonce": "e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa" 255 | }, 256 | }); 257 | const attestationHash = userId.getAttestationHash('name'); 258 | expect(attestationHash).toBe('d6cbf280ad7515e549c7b154a02555fff3eeb05c6b245039813d39d3c0397b4a'); 259 | 260 | // create a signing transaction of the user's hash with our own identity key 261 | const dataString = 'This is a test string to add to the attestation'; 262 | const transaction = bap.signAttestationWithAIP(attestationHash, identityKey, 0, dataString); 263 | expect(transaction.length).toBe(15); 264 | const verify = bap.verifyAttestationWithAIP(transaction); 265 | expect(verify.verified).toBe(true); 266 | }); 267 | 268 | test('lastIdPath', () => { 269 | const fullBap = new BAP(HDPrivateKey); 270 | fullBap.importIds(fullId, false); 271 | expect(fullBap.lastIdPath).toBe('/26562456/876543/345346'); 272 | 273 | const newId = fullBap.newId(); 274 | const idKey = newId.getIdentityKey(); 275 | expect(fullBap.lastIdPath).toBe('/26562456/876544/0'); 276 | 277 | fullBap.removeId(idKey); 278 | expect(fullBap.lastIdPath).toBe('/26562456/876544/0'); 279 | 280 | const newId2 = fullBap.newId(); 281 | const idKey2 = newId2.getIdentityKey(); 282 | expect(fullBap.lastIdPath).toBe('/26562456/876545/0'); 283 | 284 | fullBap.removeId(idKey2); 285 | expect(fullBap.lastIdPath).toBe('/26562456/876545/0'); 286 | 287 | const newId3 = fullBap.newId(); 288 | expect(fullBap.lastIdPath).toBe('/26562456/876546/0'); 289 | 290 | const idKeys = fullBap.listIds(); 291 | expect(idKeys.length).toBe(2); 292 | }); 293 | 294 | /* 295 | test('verifyChallengeSignature', () => { 296 | const privateKey = bsv.PrivateKey.fromRandom(); 297 | const address = privateKey.publicKey.toAddress().toString(); 298 | const message = 'test message'; 299 | const signature = Message(message).sign(privateKey); 300 | 301 | const bap = new BAP(HDPrivateKey); 302 | const result = bap.verifyChallengeSignature( 303 | identityKey, 304 | address, 305 | message, 306 | signature 307 | ) 308 | expect(result).toBe(true); 309 | }); 310 | 311 | */ 312 | }); 313 | -------------------------------------------------------------------------------- /tests/regression.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | describe, 3 | expect, 4 | beforeEach, 5 | afterEach, 6 | test, 7 | } from '@jest/globals'; 8 | import { BAP } from '../src'; 9 | 10 | import testVectors from './data/test-vectors.json'; 11 | 12 | describe('test-vectors', () => { 13 | it('regression', () => { 14 | testVectors.forEach((v) => { 15 | const bap = new BAP(v.HDPrivateKey); 16 | const id = bap.newId(); 17 | expect(id.getIdentityKey()).toBe(v.idKey); 18 | expect(id.rootPath).toBe(v.rootPath); 19 | expect(id.rootAddress).toBe(v.rootAddress); 20 | const tx = id.getInitialIdTransaction(); 21 | expect(typeof tx[8]).toBe('string') 22 | expect(typeof v.tx[8]).toBe('string') 23 | delete tx[8]; // remove the signature, will be different 24 | delete v.tx[8]; // remove the signature, will be different 25 | expect(tx).toStrictEqual(v.tx); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | describe, 3 | expect, 4 | beforeEach, 5 | afterEach, 6 | test, 7 | } from '@jest/globals'; 8 | import { Utils } from '../src/utils'; 9 | 10 | describe('random', () => { 11 | it('should generate random strings', () => { 12 | const randomString = Utils.getRandomString(32); 13 | expect(randomString.length).toEqual(64); 14 | 15 | const randomString2 = Utils.getRandomString(12); 16 | expect(randomString2.length).toEqual(24); 17 | }); 18 | 19 | test('getNextPath', () => { 20 | expect(Utils.getNextPath('/0/0/1')).toBe('/0/0/2'); 21 | expect(Utils.getNextPath('/0/2345/1')).toBe('/0/2345/2'); 22 | expect(Utils.getNextPath('/0\'/2345\'/1\'')).toBe('/0\'/2345\'/2\''); 23 | expect(Utils.getNextPath('/5765/2345/2342')).toBe('/5765/2345/2343'); 24 | expect(Utils.getNextPath('/5765\'/2345\'/2342\'')).toBe('/5765\'/2345\'/2343\''); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "target": "es6", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "declaration": true, 14 | "declarationDir": "dts/", 15 | "emitDeclarationOnly": true 16 | } 17 | } 18 | --------------------------------------------------------------------------------