Skip to content

crypto: Expose the public key of a certificate & cert sha256 #17690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions doc/api/https.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion doc/api/tls.md
Original file line number Diff line number Diff line change
@@ -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: <Buffer ... >,
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: <Buffer ....> }
raw: <Buffer ... > }
```

## tls.connect(options[, callback])
2 changes: 1 addition & 1 deletion lib/_tls_wrap.js
Original file line number Diff line number Diff line change
@@ -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);
}

2 changes: 2 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
@@ -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") \
@@ -241,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") \
55 changes: 37 additions & 18 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
@@ -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<Object> X509ToObject(Environment* env, X509* cert) {
EscapableHandleScope scope(env->isolate());
Local<Context> context = env->context();
@@ -1880,6 +1899,14 @@ static Local<Object> 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<Object> pubbuff = Buffer::New(env, size).ToLocalChecked();
unsigned char* pubserialized =
reinterpret_cast<unsigned char*>(Buffer::Data(pubbuff));
i2d_RSA_PUBKEY(rsa, &pubserialized);
info->Set(env->pubkey_string(), pubbuff);
}

if (pkey != nullptr) {
@@ -1907,26 +1934,18 @@ static Local<Object> 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<STACK_OF(ASN1_OBJECT)*>(
28 changes: 27 additions & 1 deletion test/parallel/test-tls-peer-certificate.js
Original file line number Diff line number Diff line change
@@ -20,15 +20,23 @@
// 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.

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/' ]);