Skip to content
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

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

Closed
Closed
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
```

Expand Down Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 2 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -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") \
Expand Down Expand Up @@ -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") \
Expand Down
55 changes: 37 additions & 18 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)*>(
Expand Down
28 changes: 27 additions & 1 deletion test/parallel/test-tls-peer-certificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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/' ]);

Expand Down