Skip to content

Commit 6800688

Browse files
committed
Add example for fetching ECC and PGP keys, verifying signatures, and creating a key map for use in the bitcoinRpc example
1 parent 88a3550 commit 6800688

File tree

3 files changed

+513
-108
lines changed

3 files changed

+513
-108
lines changed

examples/fetchEccKeysAndVerify.js

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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

Comments
 (0)