|
| 1 | +const crypto = require('crypto'); |
| 2 | + |
| 3 | +const bs58 = require('bs58'); |
| 4 | +const kbpgp = require('kbpgp'); |
| 5 | +const request = require('request-promise'); |
| 6 | + |
| 7 | +let bitpayPgpKeys = {}; |
| 8 | +let githubPgpKeys = {}; |
| 9 | +let importedPgpKeys = {}; |
| 10 | +let signatureCount = 0; |
| 11 | + |
| 12 | +let eccPayload; |
| 13 | +let parsedEccPayload; |
| 14 | +let eccKeysHash; |
| 15 | + |
| 16 | +let keyRequests = []; |
| 17 | + |
| 18 | +keyRequests.push((() => { |
| 19 | + console.log('Fetching keys from github.com/bitpay/pgp-keys...'); |
| 20 | + return request({ |
| 21 | + method: 'GET', |
| 22 | + url: 'https://api.github.com/repos/bitpay/pgp-keys/contents/keys', |
| 23 | + headers: { |
| 24 | + 'user-agent': 'BitPay Key-Check Utility' |
| 25 | + }, |
| 26 | + json: true |
| 27 | + }).then((pgpKeyFiles) => { |
| 28 | + let fileDataPromises = []; |
| 29 | + pgpKeyFiles.forEach((file) => { |
| 30 | + fileDataPromises.push((() => { |
| 31 | + return request({ |
| 32 | + method: 'GET', |
| 33 | + url: file.download_url, |
| 34 | + headers: { |
| 35 | + 'user-agent': 'BitPay Key-Check Utility' |
| 36 | + } |
| 37 | + }).then((body) => { |
| 38 | + let hash = crypto.createHash('sha256').update(body).digest('hex'); |
| 39 | + githubPgpKeys[hash] = body; |
| 40 | + return Promise.resolve(); |
| 41 | + }); |
| 42 | + })()); |
| 43 | + }); |
| 44 | + return Promise.all(fileDataPromises); |
| 45 | + }); |
| 46 | +})()); |
| 47 | + |
| 48 | +keyRequests.push((() => { |
| 49 | + console.log('Fetching keys from bitpay.com/pgp-keys...'); |
| 50 | + return request({ |
| 51 | + method: 'GET', |
| 52 | + url: 'https://bitpay.com/pgp-keys.json', |
| 53 | + headers: { |
| 54 | + 'user-agent': 'BitPay Key-Check Utility' |
| 55 | + }, |
| 56 | + json: true |
| 57 | + }).then((body) => { |
| 58 | + body.pgpKeys.forEach(function(key) { |
| 59 | + let hash = crypto.createHash('sha256').update(key.publicKey).digest('hex'); |
| 60 | + bitpayPgpKeys[hash] = key.publicKey; |
| 61 | + }); |
| 62 | + return Promise.resolve(); |
| 63 | + }); |
| 64 | +})()); |
| 65 | + |
| 66 | +Promise.all(keyRequests).then(() => { |
| 67 | + if (Object.keys(githubPgpKeys).length !== Object.keys(bitpayPgpKeys).length) { |
| 68 | + console.log('Warning: Different number of keys returned by key lists'); |
| 69 | + } |
| 70 | + |
| 71 | + let bitpayOnlyKeys = Object.keys(bitpayPgpKeys).filter((keyHash) => { |
| 72 | + return !githubPgpKeys[keyHash]; |
| 73 | + }); |
| 74 | + |
| 75 | + let githubOnlyKeys = Object.keys(githubPgpKeys).filter((keyHash) => { |
| 76 | + return !bitpayPgpKeys[keyHash]; |
| 77 | + }); |
| 78 | + |
| 79 | + if (bitpayOnlyKeys.length) { |
| 80 | + console.log('BitPay returned some keys which are not present in github'); |
| 81 | + Object.keys(bitpayOnlyKeys).forEach((keyHash) => { |
| 82 | + console.log(`Hash ${keyHash} Key: ${bitpayOnlyKeys[keyHash]}`); |
| 83 | + }); |
| 84 | + } |
| 85 | + |
| 86 | + if (githubOnlyKeys.length) { |
| 87 | + console.log('GitHub returned some keys which are not present in BitPay'); |
| 88 | + Object.keys(githubOnlyKeys).forEach((keyHash) => { |
| 89 | + console.log(`Hash ${keyHash} Key: ${githubOnlyKeys[keyHash]}`); |
| 90 | + }); |
| 91 | + } |
| 92 | + |
| 93 | + if (!githubOnlyKeys.length && !bitpayOnlyKeys.length) { |
| 94 | + console.log(`Both sites returned ${Object.keys(githubPgpKeys).length} keys. Key lists from both are identical.`); |
| 95 | + return Promise.resolve(); |
| 96 | + } else { |
| 97 | + return Promise.reject('Aborting signature checks due to key mismatch'); |
| 98 | + } |
| 99 | +}).then(() => { |
| 100 | + console.log('Importing PGP keys for later use...'); |
| 101 | + return Promise.all(Object.values(bitpayPgpKeys).map((pgpKeyString) => { |
| 102 | + return new Promise((resolve, reject) => { |
| 103 | + kbpgp.KeyManager.import_from_armored_pgp({armored: pgpKeyString}, (err, km) => { |
| 104 | + if (err) { |
| 105 | + return reject(err); |
| 106 | + } |
| 107 | + // console.log(km.pgp.key(km.pgp.primary).get_fingerprint().toString('hex')); |
| 108 | + importedPgpKeys[km.pgp.key(km.pgp.primary).get_fingerprint().toString('hex')] = km; |
| 109 | + return resolve(); |
| 110 | + }); |
| 111 | + }); |
| 112 | + })); |
| 113 | +}).then(() => { |
| 114 | + console.log('Fetching current ECC keys from bitpay.com/signingKeys/paymentProtocol.json'); |
| 115 | + return request({ |
| 116 | + method: 'GET', |
| 117 | + url: 'https://bitpay.com/signingKeys/paymentProtocol.json', |
| 118 | + headers: { |
| 119 | + 'user-agent': 'BitPay Key-Check Utility' |
| 120 | + } |
| 121 | + }).then((rawEccPayload) => { |
| 122 | + if (rawEccPayload.indexOf('rate limit') !== -1) { |
| 123 | + return Promise.reject('Rate limited by BitPay'); |
| 124 | + } |
| 125 | + eccPayload = rawEccPayload; |
| 126 | + parsedEccPayload = JSON.parse(rawEccPayload); |
| 127 | + if (new Date(parsedEccPayload.expirationDate) < Date.now()) { |
| 128 | + return console.log('The currently published ECC keys are expired'); |
| 129 | + } |
| 130 | + eccKeysHash = crypto.createHash('sha256').update(rawEccPayload).digest('hex'); |
| 131 | + return Promise.resolve(); |
| 132 | + }); |
| 133 | +}).then(() => { |
| 134 | + console.log(`Fetching signatures for ECC payload with hash ${eccKeysHash}`); |
| 135 | + return request({ |
| 136 | + method: 'GET', |
| 137 | + url: `https://bitpay.com/signatures/${eccKeysHash}.json`, |
| 138 | + headers: { |
| 139 | + 'user-agent': 'BitPay Key-Check Utility' |
| 140 | + }, |
| 141 | + json: true |
| 142 | + }).then((signatureData) => { |
| 143 | + console.log('Verifying each signature is valid and comes from the set of PGP keys retrieved earlier'); |
| 144 | + Promise.all(signatureData.signatures.map((signature) => { |
| 145 | + return new Promise((resolve, reject) => { |
| 146 | + let pgpKey = importedPgpKeys[signature.identifier]; |
| 147 | + if (!pgpKey) { |
| 148 | + return reject(`PGP key ${signature.identifier} missing for signature`); |
| 149 | + } |
| 150 | + let armoredSignature = Buffer.from(signature.signature, 'hex').toString(); |
| 151 | + |
| 152 | + kbpgp.unbox({armored: armoredSignature, data: Buffer.from(eccPayload), keyfetch: pgpKey}, (err, result) => { |
| 153 | + if (err) { |
| 154 | + return reject(`Unable to verify signature from ${signature.identifier} ${err}`); |
| 155 | + } |
| 156 | + signatureCount++; |
| 157 | + console.log(`Good signature from ${signature.identifier} (${pgpKey.get_userids()[0].get_username()})`); |
| 158 | + return Promise.resolve(); |
| 159 | + }); |
| 160 | + }); |
| 161 | + })); |
| 162 | + }); |
| 163 | +}).then(() => { |
| 164 | + if (signatureCount >= (Object.keys(bitpayPgpKeys).length / 2) ) { |
| 165 | + console.log(`----\nThe following ECC key set has been verified against signatures from ${signatureCount} of the ${Object.keys(bitpayPgpKeys).length} published BitPay PGP keys.`); |
| 166 | + console.log(eccPayload); |
| 167 | + |
| 168 | + let keyMap = {}; |
| 169 | + |
| 170 | + console.log('----\nValid keymap for use in bitcoinRpc example:'); |
| 171 | + |
| 172 | + parsedEccPayload.publicKeys.forEach((pubkey) => { |
| 173 | + // Here we are just generating the pubkey hash (btc address) of the |
| 174 | + let a = crypto.createHash('sha256').update(pubkey, 'hex').digest(); |
| 175 | + let b = crypto.createHash('rmd160').update(a).digest('hex'); |
| 176 | + let c = '00' + b; // This is assuming livenet |
| 177 | + let d = crypto.createHash('sha256').update(c, 'hex').digest(); |
| 178 | + let e = crypto.createHash('sha256').update(d).digest('hex'); |
| 179 | + |
| 180 | + let pubKeyHash = bs58.encode(Buffer.from(c + e.substr(0, 8), 'hex')); |
| 181 | + |
| 182 | + |
| 183 | + keyMap[pubKeyHash] = { |
| 184 | + owner: parsedEccPayload.owner, |
| 185 | + networks: ['main'], |
| 186 | + domains: parsedEccPayload.domains, |
| 187 | + publicKey: pubkey |
| 188 | + } |
| 189 | + }); |
| 190 | + |
| 191 | + console.log(keyMap); |
| 192 | + } else { |
| 193 | + return Promise.reject(`Insufficient good signatures ${signatureCount} for a proper validity check`); |
| 194 | + } |
| 195 | +}).catch((err) => { |
| 196 | + console.log(`Error encountered ${err}`); |
| 197 | +}); |
| 198 | + |
| 199 | +process.on('unhandledRejection', console.log); |
0 commit comments