Skip to content

Commit 3afabc9

Browse files
crypto: add docs & tests for cert.pubkey & cert.fingerprint256
Include example on how to pin certificate and/or public key
1 parent 01eb7f7 commit 3afabc9

File tree

3 files changed

+128
-5
lines changed

3 files changed

+128
-5
lines changed

doc/api/https.md

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,10 @@ changes:
168168

169169
Makes a request to a secure web server.
170170

171-
The following additional `options` from [`tls.connect()`][] are also accepted
172-
when using a custom [`Agent`][]: `ca`, `cert`, `ciphers`, `clientCertEngine`,
173-
`key`, `passphrase`, `pfx`, `rejectUnauthorized`, `secureProtocol`, `servername`
171+
Additionally, the `options` from [`tls.connect()`][] are also accepted when
172+
using a custom [`Agent`][], such as: `ca`, `cert`, `ciphers`,
173+
`clientCertEngine`, `key`, `passphrase`, `pfx`, `rejectUnauthorized`,
174+
`secureProtocol`, `servername`.
174175

175176
`options` can be an object, a string, or a [`URL`][] object. If `options` is a
176177
string, it is automatically parsed with [`url.parse()`][]. If it is a [`URL`][]
@@ -250,6 +251,98 @@ const req = https.request(options, (res) => {
250251
});
251252
```
252253

254+
Example pinning on certificate fingerprint, or the public key (similar to `pin-sha256`):
255+
256+
```js
257+
const tls = require('tls');
258+
const https = require('https');
259+
const crypto = require('crypto');
260+
261+
function sha256(s) {
262+
return crypto.createHash('sha256').update(s).digest('base64');
263+
}
264+
const options = {
265+
hostname: 'github.com',
266+
port: 443,
267+
path: '/',
268+
method: 'GET',
269+
checkServerIdentity: function(host, cert) {
270+
// Make sure the certificate is issued to the host we are connected to
271+
const err = tls.checkServerIdentity(host, cert);
272+
if (err) {
273+
return err;
274+
}
275+
276+
// Pin the public key, similar to HPKP pin-sha25 pinning
277+
const pubkey256 = 'pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU=';
278+
if (sha256(cert.pubkey) !== pubkey256) {
279+
const msg = 'Certificate verification error: ' +
280+
`The public key of '${cert.subject.CN}' ` +
281+
'does not match our pinned fingerprint';
282+
return new Error(msg);
283+
}
284+
285+
// Pin the exact certificate, rather then the pub key
286+
const cert256 = '25:FE:39:32:D9:63:8C:8A:FC:A1:9A:29:87:' +
287+
'D8:3E:4C:1D:98:DB:71:E4:1A:48:03:98:EA:22:6A:BD:8B:93:16';
288+
if (cert.fingerprint256 !== cert256) {
289+
const msg = 'Certificate verification error: ' +
290+
`The certificate of '${cert.subject.CN}' ` +
291+
'does not match our pinned fingerprint';
292+
return new Error(msg);
293+
}
294+
295+
// This loop is informational only.
296+
// Print the certificate and public key fingerprints of all certs in the
297+
// chain. Its common to pin the public key of the issuer on the public
298+
// internet, while pinning the public key of the service in sensitive
299+
// environments.
300+
do {
301+
console.log('Subject Common Name:', cert.subject.CN);
302+
console.log(' Certificate SHA256 fingerprint:', cert.fingerprint256);
303+
304+
hash = crypto.createHash('sha256');
305+
console.log(' Public key ping-sha256:', sha256(cert.pubkey));
306+
307+
lastprint256 = cert.fingerprint256;
308+
cert = cert.issuerCertificate;
309+
} while (cert.fingerprint256 !== lastprint256);
310+
311+
},
312+
};
313+
314+
options.agent = new https.Agent(options);
315+
const req = https.request(options, (res) => {
316+
console.log('All OK. Server matched our pinned cert or public key');
317+
console.log('statusCode:', res.statusCode);
318+
// Print the HPKP values
319+
console.log('headers:', res.headers['public-key-pins']);
320+
321+
res.on('data', (d) => {});
322+
});
323+
324+
req.on('error', (e) => {
325+
console.error(e.message);
326+
});
327+
req.end();
328+
329+
```
330+
Outputs for example:
331+
```text
332+
Subject Common Name: github.com
333+
Certificate SHA256 fingerprint: 25:FE:39:32:D9:63:8C:8A:FC:A1:9A:29:87:D8:3E:4C:1D:98:DB:71:E4:1A:48:03:98:EA:22:6A:BD:8B:93:16
334+
Public key ping-sha256: pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU=
335+
Subject Common Name: DigiCert SHA2 Extended Validation Server CA
336+
Certificate SHA256 fingerprint: 40:3E:06:2A:26:53:05:91:13:28:5B:AF:80:A0:D4:AE:42:2C:84:8C:9F:78:FA:D0:1F:C9:4B:C5:B8:7F:EF:1A
337+
Public key ping-sha256: RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=
338+
Subject Common Name: DigiCert High Assurance EV Root CA
339+
Certificate SHA256 fingerprint: 74:31:E5:F4:C3:C1:CE:46:90:77:4F:0B:61:E0:54:40:88:3B:A9:A0:1E:D0:0B:A6:AB:D7:80:6E:D3:B1:18:CF
340+
Public key ping-sha256: WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=
341+
All OK. Server matched our pinned cert or public key
342+
statusCode: 200
343+
headers: max-age=0; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; pin-sha256="RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho="; pin-sha256="k2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K+59sNQws="; pin-sha256="K87oWBWM9UZfyddvDfoxL+8lpNyoUB2ptGtn0fv6G2Q="; pin-sha256="IQBnNBEiFuhj+8x6X8XLgh01V9Ic5/V3IRQLNFFc7v4="; pin-sha256="iie1VXtL7HzAMF+/PVPR9xzT80kQxdZeJ+zduCB3uj0="; pin-sha256="LvRiGEjRqfzurezaWuj8Wie2gyHMrW5Q06LspMnox7A="; includeSubDomains
344+
```
345+
253346
[`Agent`]: #https_class_https_agent
254347
[`URL`]: url.html#url_the_whatwg_url_api
255348
[`http.Agent`]: http.html#http_class_http_agent

doc/api/tls.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,11 @@ For example:
619619
issuerCertificate:
620620
{ ... another certificate, possibly with a .issuerCertificate ... },
621621
raw: < RAW DER buffer >,
622+
pubkey: < RAW DER buffer >,
622623
valid_from: 'Nov 11 09:52:22 2009 GMT',
623624
valid_to: 'Nov 6 09:52:22 2029 GMT',
624625
fingerprint: '2A:7A:C2:DD:E5:F9:CC:53:72:35:99:7A:02:5A:71:38:52:EC:8A:DF',
626+
fingerprint256: '2A:7A:C2:DD:E5:F9:CC:53:72:35:99:7A:02:5A:71:38:52:EC:8A:DF:00:11:22:33:44:55:66:77:88:99:AA:BB',
625627
serialNumber: 'B9B0D332A1AA5635' }
626628
```
627629

@@ -786,12 +788,14 @@ The cert object contains the parsed certificate and will have a structure simila
786788
'OCSP - URI': [ 'http://ocsp.comodoca.com' ] },
787789
modulus: 'B56CE45CB740B09A13F64AC543B712FF9EE8E4C284B542A1708A27E82A8D151CA178153E12E6DDA15BF70FFD96CB8A88618641BDFCCA03527E665B70D779C8A349A6F88FD4EF6557180BD4C98192872BCFE3AF56E863C09DDD8BC1EC58DF9D94F914F0369102B2870BECFA1348A0838C9C49BD1C20124B442477572347047506B1FCD658A80D0C44BCC16BC5C5496CFE6E4A8428EF654CD3D8972BF6E5BFAD59C93006830B5EB1056BBB38B53D1464FA6E02BFDF2FF66CD949486F0775EC43034EC2602AEFBF1703AD221DAA2A88353C3B6A688EFE8387811F645CEED7B3FE46E1F8B9F59FAD028F349B9BC14211D5830994D055EEA3D547911E07A0ADDEB8A82B9188E58720D95CD478EEC9AF1F17BE8141BE80906F1A339445A7EB5B285F68039B0F294598A7D1C0005FC22B5271B0752F58CCDEF8C8FD856FB7AE21C80B8A2CE983AE94046E53EDE4CB89F42502D31B5360771C01C80155918637490550E3F555E2EE75CC8C636DDE3633CFEDD62E91BF0F7688273694EEEBA20C2FC9F14A2A435517BC1D7373922463409AB603295CEB0BB53787A334C9CA3CA8B30005C5A62FC0715083462E00719A8FA3ED0A9828C3871360A73F8B04A4FC1E71302844E9BB9940B77E745C9D91F226D71AFCAD4B113AAF68D92B24DDB4A2136B55A1CD1ADF39605B63CB639038ED0F4C987689866743A68769CC55847E4A06D6E2E3F1',
788790
exponent: '0x10001',
791+
pubkey: <Buffer ... >,
789792
valid_from: 'Aug 14 00:00:00 2017 GMT',
790793
valid_to: 'Nov 20 23:59:59 2019 GMT',
791794
fingerprint: '01:02:59:D9:C3:D2:0D:08:F7:82:4E:44:A4:B4:53:C5:E2:3A:87:4D',
795+
fingerprint256: '69:AE:1A:6A:D4:3D:C6:C1:1B:EA:C6:23:DE:BA:2A:14:62:62:93:5C:7A:EA:06:41:9B:0B:BC:87:CE:48:4E:02',
792796
ext_key_usage: [ '1.3.6.1.5.5.7.3.1', '1.3.6.1.5.5.7.3.2' ],
793797
serialNumber: '66593D57F20CBC573E433381B5FEC280',
794-
raw: <Buffer ....> }
798+
raw: <Buffer ... > }
795799
```
796800

797801
## tls.connect(options[, callback])

test/parallel/test-tls-peer-certificate.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,23 @@
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

2222
'use strict';
23-
require('../common');
23+
const common = require('../common');
2424
const fixtures = require('../common/fixtures');
25+
if (!common.hasCrypto) {
26+
common.skip('missing crypto');
27+
}
28+
const crypto = require('crypto');
2529

2630
// Verify that detailed getPeerCertificate() return value has all certs.
2731

2832
const {
2933
assert, connect, debug, keys
3034
} = require(fixtures.path('tls-connect'));
3135

36+
function sha256(s) {
37+
return crypto.createHash('sha256').update(s);
38+
}
39+
3240
connect({
3341
client: { rejectUnauthorized: false },
3442
server: keys.agent1,
@@ -49,6 +57,24 @@ connect({
4957
peerCert.fingerprint,
5058
'8D:06:3A:B3:E5:8B:85:29:72:4F:7D:1B:54:CD:95:19:3C:EF:6F:AA'
5159
);
60+
assert.strictEqual(
61+
peerCert.fingerprint256,
62+
'A1:DC:01:1A:EC:A3:7B:86:A8:C2:3E:26:9F:EB:EE:5C:A9:3B:BE:06' +
63+
':4C:A4:00:53:93:A9:66:07:A7:BC:13:32'
64+
);
65+
66+
// SHA256 fingerprint of the public key
67+
assert.strictEqual(
68+
sha256(peerCert.pubkey).digest('hex'),
69+
'fa5152e4407bad1e7537ef5bfc3f19fa9a62ee04432fd75e109b1803704c31ba'
70+
);
71+
72+
// HPKP / RFC7469 "pin-sha256" of the public key
73+
assert.strictEqual(
74+
sha256(peerCert.pubkey).digest('base64'),
75+
'+lFS5EB7rR51N+9b/D8Z+ppi7gRDL9deEJsYA3BMMbo='
76+
);
77+
5278
assert.deepStrictEqual(peerCert.infoAccess['OCSP - URI'],
5379
[ 'http://ocsp.nodejs.org/' ]);
5480

0 commit comments

Comments
 (0)