From ba19b7b3025d9483cd19b5d1cf57275f3d6411fd Mon Sep 17 00:00:00 2001 From: Hannes Magnusson Date: Thu, 14 Dec 2017 13:14:06 -0800 Subject: [PATCH 1/4] crypto: add cert.fingerprint256 as SHA256 fingerprint --- src/env.h | 1 + src/node_crypto.cc | 47 ++++++++++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/env.h b/src/env.h index dedf821b24f5c1..217cebc195bcad 100644 --- a/src/env.h +++ b/src/env.h @@ -170,6 +170,7 @@ struct PackageConfig { V(fd_string, "fd") \ V(file_string, "file") \ V(fingerprint_string, "fingerprint") \ + V(fingerprint256_string, "fingerprint256") \ V(flags_string, "flags") \ V(get_data_clone_error_string, "_getDataCloneError") \ V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index a0b21e7c39d76c..a4e187a5779167 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -1791,6 +1791,25 @@ static bool SafeX509ExtPrint(BIO* out, X509_EXTENSION* ext) { } +static void AddFingerprintDigest(const unsigned char* md, + unsigned int md_size, + char (*fingerprint)[3 * EVP_MAX_MD_SIZE + 1]) { + unsigned int i; + const char hex[] = "0123456789ABCDEF"; + + for (i = 0; i < md_size; i++) { + (*fingerprint)[3*i] = hex[(md[i] & 0xf0) >> 4]; + (*fingerprint)[(3*i)+1] = hex[(md[i] & 0x0f)]; + (*fingerprint)[(3*i)+2] = ':'; + } + + if (md_size > 0) { + (*fingerprint)[(3*(md_size-1))+2] = '\0'; + } else { + (*fingerprint)[0] = '\0'; + } +} + static Local X509ToObject(Environment* env, X509* cert) { EscapableHandleScope scope(env->isolate()); Local context = env->context(); @@ -1907,26 +1926,18 @@ static Local X509ToObject(Environment* env, X509* cert) { mem->length)).FromJust(); BIO_free_all(bio); - unsigned int md_size, i; unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int md_size; + char fingerprint[EVP_MAX_MD_SIZE * 3 + 1]; if (X509_digest(cert, EVP_sha1(), md, &md_size)) { - const char hex[] = "0123456789ABCDEF"; - char fingerprint[EVP_MAX_MD_SIZE * 3]; - - for (i = 0; i < md_size; i++) { - fingerprint[3*i] = hex[(md[i] & 0xf0) >> 4]; - fingerprint[(3*i)+1] = hex[(md[i] & 0x0f)]; - fingerprint[(3*i)+2] = ':'; - } - - if (md_size > 0) { - fingerprint[(3*(md_size-1))+2] = '\0'; - } else { - fingerprint[0] = '\0'; - } - - info->Set(context, env->fingerprint_string(), - OneByteString(env->isolate(), fingerprint)).FromJust(); + AddFingerprintDigest(md, md_size, &fingerprint); + info->Set(context, env->fingerprint_string(), + OneByteString(env->isolate(), fingerprint)).FromJust(); + } + if (X509_digest(cert, EVP_sha256(), md, &md_size)) { + AddFingerprintDigest(md, md_size, &fingerprint); + info->Set(context, env->fingerprint256_string(), + OneByteString(env->isolate(), fingerprint)).FromJust(); } STACK_OF(ASN1_OBJECT)* eku = static_cast( From 77b3abf7ed61b14d3a75bf7f5309e820c704b333 Mon Sep 17 00:00:00 2001 From: Hannes Magnusson Date: Tue, 6 Feb 2018 21:34:08 -0800 Subject: [PATCH 2/4] crypto: add cert.pubkey containing the raw pubkey of certificate --- src/env.h | 1 + src/node_crypto.cc | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/env.h b/src/env.h index 217cebc195bcad..6cc2cea2df73cb 100644 --- a/src/env.h +++ b/src/env.h @@ -242,6 +242,7 @@ struct PackageConfig { V(priority_string, "priority") \ V(produce_cached_data_string, "produceCachedData") \ V(promise_string, "promise") \ + V(pubkey_string, "pubkey") \ V(raw_string, "raw") \ V(read_host_object_string, "_readHostObject") \ V(readable_string, "readable") \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index a4e187a5779167..606f44479f2f4b 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -1899,6 +1899,14 @@ static Local X509ToObject(Environment* env, X509* cert) { String::kNormalString, mem->length)).FromJust(); USE(BIO_reset(bio)); + + int size = i2d_RSA_PUBKEY(rsa, nullptr); + CHECK_GE(size, 0); + Local pubbuff = Buffer::New(env, size).ToLocalChecked(); + unsigned char* pubserialized = + reinterpret_cast(Buffer::Data(pubbuff)); + i2d_RSA_PUBKEY(rsa, &pubserialized); + info->Set(env->pubkey_string(), pubbuff); } if (pkey != nullptr) { From 2987c0ebc4e932246096a15c71509ffa98db9737 Mon Sep 17 00:00:00 2001 From: Hannes Magnusson Date: Thu, 14 Dec 2017 15:45:44 -0800 Subject: [PATCH 3/4] crypto: provide full cert details to checkServerIdentity --- lib/_tls_wrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index fcd447bb5901fe..b3c48b950c6746 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -1055,7 +1055,7 @@ function onConnectSecure() { options.host || (options.socket && options.socket._host) || 'localhost'; - const cert = this.getPeerCertificate(); + const cert = this.getPeerCertificate(true); verifyError = options.checkServerIdentity(hostname, cert); } From 1be548b3936919bed1acf2ae59294049be4d84ca Mon Sep 17 00:00:00 2001 From: Hannes Magnusson Date: Wed, 14 Feb 2018 09:35:10 -0800 Subject: [PATCH 4/4] crypto: add docs & tests for cert.pubkey & cert.fingerprint256 Include example on how to pin certificate and/or public key --- doc/api/https.md | 92 ++++++++++++++++++++++ doc/api/tls.md | 6 +- test/parallel/test-tls-peer-certificate.js | 28 ++++++- 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/doc/api/https.md b/doc/api/https.md index 58e62ccced4728..da4dbb22abeedf 100644 --- a/doc/api/https.md +++ b/doc/api/https.md @@ -251,6 +251,98 @@ const req = https.request(options, (res) => { }); ``` +Example pinning on certificate fingerprint, or the public key (similar to `pin-sha256`): + +```js +const tls = require('tls'); +const https = require('https'); +const crypto = require('crypto'); + +function sha256(s) { + return crypto.createHash('sha256').update(s).digest('base64'); +} +const options = { + hostname: 'github.com', + port: 443, + path: '/', + method: 'GET', + checkServerIdentity: function(host, cert) { + // Make sure the certificate is issued to the host we are connected to + const err = tls.checkServerIdentity(host, cert); + if (err) { + return err; + } + + // Pin the public key, similar to HPKP pin-sha25 pinning + const pubkey256 = 'pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU='; + if (sha256(cert.pubkey) !== pubkey256) { + const msg = 'Certificate verification error: ' + + `The public key of '${cert.subject.CN}' ` + + 'does not match our pinned fingerprint'; + return new Error(msg); + } + + // Pin the exact certificate, rather then the pub key + const cert256 = '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'; + if (cert.fingerprint256 !== cert256) { + const msg = 'Certificate verification error: ' + + `The certificate of '${cert.subject.CN}' ` + + 'does not match our pinned fingerprint'; + return new Error(msg); + } + + // This loop is informational only. + // Print the certificate and public key fingerprints of all certs in the + // chain. Its common to pin the public key of the issuer on the public + // internet, while pinning the public key of the service in sensitive + // environments. + do { + console.log('Subject Common Name:', cert.subject.CN); + console.log(' Certificate SHA256 fingerprint:', cert.fingerprint256); + + hash = crypto.createHash('sha256'); + console.log(' Public key ping-sha256:', sha256(cert.pubkey)); + + lastprint256 = cert.fingerprint256; + cert = cert.issuerCertificate; + } while (cert.fingerprint256 !== lastprint256); + + }, +}; + +options.agent = new https.Agent(options); +const req = https.request(options, (res) => { + console.log('All OK. Server matched our pinned cert or public key'); + console.log('statusCode:', res.statusCode); + // Print the HPKP values + console.log('headers:', res.headers['public-key-pins']); + + res.on('data', (d) => {}); +}); + +req.on('error', (e) => { + console.error(e.message); +}); +req.end(); + +``` + Outputs for example: +```text +Subject Common Name: github.com + 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 + Public key ping-sha256: pL1+qb9HTMRZJmuC/bB/ZI9d302BYrrqiVuRyW+DGrU= +Subject Common Name: DigiCert SHA2 Extended Validation Server CA + 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 + Public key ping-sha256: RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= +Subject Common Name: DigiCert High Assurance EV Root CA + 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 + Public key ping-sha256: WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18= +All OK. Server matched our pinned cert or public key +statusCode: 200 +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 +``` + [`Agent`]: #https_class_https_agent [`URL`]: url.html#url_the_whatwg_url_api [`http.Agent`]: http.html#http_class_http_agent diff --git a/doc/api/tls.md b/doc/api/tls.md index 161ec5d9633fbf..5cdab4cbd357cb 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -618,9 +618,11 @@ For example: issuerCertificate: { ... another certificate, possibly with a .issuerCertificate ... }, raw: < RAW DER buffer >, + pubkey: < RAW DER buffer >, valid_from: 'Nov 11 09:52:22 2009 GMT', valid_to: 'Nov 6 09:52:22 2029 GMT', fingerprint: '2A:7A:C2:DD:E5:F9:CC:53:72:35:99:7A:02:5A:71:38:52:EC:8A:DF', + 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', serialNumber: 'B9B0D332A1AA5635' } ``` @@ -786,12 +788,14 @@ similar to: 'OCSP - URI': [ 'http://ocsp.comodoca.com' ] }, modulus: 'B56CE45CB740B09A13F64AC543B712FF9EE8E4C284B542A1708A27E82A8D151CA178153E12E6DDA15BF70FFD96CB8A88618641BDFCCA03527E665B70D779C8A349A6F88FD4EF6557180BD4C98192872BCFE3AF56E863C09DDD8BC1EC58DF9D94F914F0369102B2870BECFA1348A0838C9C49BD1C20124B442477572347047506B1FCD658A80D0C44BCC16BC5C5496CFE6E4A8428EF654CD3D8972BF6E5BFAD59C93006830B5EB1056BBB38B53D1464FA6E02BFDF2FF66CD949486F0775EC43034EC2602AEFBF1703AD221DAA2A88353C3B6A688EFE8387811F645CEED7B3FE46E1F8B9F59FAD028F349B9BC14211D5830994D055EEA3D547911E07A0ADDEB8A82B9188E58720D95CD478EEC9AF1F17BE8141BE80906F1A339445A7EB5B285F68039B0F294598A7D1C0005FC22B5271B0752F58CCDEF8C8FD856FB7AE21C80B8A2CE983AE94046E53EDE4CB89F42502D31B5360771C01C80155918637490550E3F555E2EE75CC8C636DDE3633CFEDD62E91BF0F7688273694EEEBA20C2FC9F14A2A435517BC1D7373922463409AB603295CEB0BB53787A334C9CA3CA8B30005C5A62FC0715083462E00719A8FA3ED0A9828C3871360A73F8B04A4FC1E71302844E9BB9940B77E745C9D91F226D71AFCAD4B113AAF68D92B24DDB4A2136B55A1CD1ADF39605B63CB639038ED0F4C987689866743A68769CC55847E4A06D6E2E3F1', exponent: '0x10001', + pubkey: , valid_from: 'Aug 14 00:00:00 2017 GMT', valid_to: 'Nov 20 23:59:59 2019 GMT', fingerprint: '01:02:59:D9:C3:D2:0D:08:F7:82:4E:44:A4:B4:53:C5:E2:3A:87:4D', + 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', ext_key_usage: [ '1.3.6.1.5.5.7.3.1', '1.3.6.1.5.5.7.3.2' ], serialNumber: '66593D57F20CBC573E433381B5FEC280', - raw: } + raw: } ``` ## tls.connect(options[, callback]) diff --git a/test/parallel/test-tls-peer-certificate.js b/test/parallel/test-tls-peer-certificate.js index 14c3d17cd24a76..e5de378675f29a 100644 --- a/test/parallel/test-tls-peer-certificate.js +++ b/test/parallel/test-tls-peer-certificate.js @@ -20,8 +20,12 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; -require('../common'); +const common = require('../common'); const fixtures = require('../common/fixtures'); +if (!common.hasCrypto) { + common.skip('missing crypto'); +} +const crypto = require('crypto'); // Verify that detailed getPeerCertificate() return value has all certs. @@ -29,6 +33,10 @@ const { assert, connect, debug, keys } = require(fixtures.path('tls-connect')); +function sha256(s) { + return crypto.createHash('sha256').update(s); +} + connect({ client: { rejectUnauthorized: false }, server: keys.agent1, @@ -49,6 +57,24 @@ connect({ peerCert.fingerprint, '8D:06:3A:B3:E5:8B:85:29:72:4F:7D:1B:54:CD:95:19:3C:EF:6F:AA' ); + assert.strictEqual( + peerCert.fingerprint256, + 'A1:DC:01:1A:EC:A3:7B:86:A8:C2:3E:26:9F:EB:EE:5C:A9:3B:BE:06' + + ':4C:A4:00:53:93:A9:66:07:A7:BC:13:32' + ); + + // SHA256 fingerprint of the public key + assert.strictEqual( + sha256(peerCert.pubkey).digest('hex'), + 'fa5152e4407bad1e7537ef5bfc3f19fa9a62ee04432fd75e109b1803704c31ba' + ); + + // HPKP / RFC7469 "pin-sha256" of the public key + assert.strictEqual( + sha256(peerCert.pubkey).digest('base64'), + '+lFS5EB7rR51N+9b/D8Z+ppi7gRDL9deEJsYA3BMMbo=' + ); + assert.deepStrictEqual(peerCert.infoAccess['OCSP - URI'], [ 'http://ocsp.nodejs.org/' ]);