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

TLS certificate object documentation and support for EC certificates #24358

Closed
wants to merge 3 commits into from
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
146 changes: 87 additions & 59 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,44 +638,102 @@ added: v0.11.4

* `detailed` {boolean} Include the full certificate chain if `true`, otherwise
include just the peer's certificate.
* Returns: {Object}
* Returns: {Object} A certificate object.

Returns an object representing the peer's certificate. The returned object has
some properties corresponding to the fields of the certificate.
Returns an object representing the peer's certificate. If the peer does not
provide a certificate, an empty object will be returned. If the socket has been
destroyed, `null` will be returned.

If the full certificate chain was requested, each certificate will include an
`issuerCertificate` property containing an object representing its issuer's
certificate.

#### Certificate Object
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/24358
description: Support Elliptic Curve public key info.
-->

A certificate object has properties corresponding to the fields of the
certificate.

* `raw` {Buffer} The DER encoded X.509 certificate data.
* `subject` {Object} The certificate subject, described in terms of
Country (`C:`), StateOrProvince (`ST`), Locality (`L`), Organization (`O`),
OrganizationalUnit (`OU`), and CommonName (`CN`). The CommonName is typically
a DNS name with TLS certificates. Example:
`{C: 'UK', ST: 'BC', L: 'Metro', O: 'Node Fans', OU: 'Docs', CN: 'example.com'}`.
* `issuer` {Object} The certificate issuer, described in the same terms as the
`subject`.
* `valid_from` {string} The date-time the certificate is valid from.
* `valid_to` {string} The date-time the certificate is valid to.
* `serialNumber` {string} The certificate serial number, as a hex string.
Example: `'B9B0D332A1AA5635'`.
* `fingerprint` {string} The SHA-1 digest of the DER encoded certificate. It is
returned as a `:` separated hexadecimal string. Example: `'2A:7A:C2:DD:...'`.
* `fingerprint256` {string} The SHA-256 digest of the DER encoded certificate.
It is returned as a `:` separated hexadecimal string. Example:
`'2A:7A:C2:DD:...'`.
* `ext_key_usage` {Array} (Optional) The extended key usage, a set of OIDs.
* `subjectaltname` {Array} (Optional) An array of names for the subject, an
alternative to the `subject` names.
* `infoAccess` {Array} (Optional) An array describing the AuthorityInfoAccess,
used with OCSP.
* `issuerCert` {Object} (Optional) The issuer certificate object. For
self-signed certificates, this may be a circular reference.

The certificate may contain information about the public key, depending on
the key type.

For RSA keys, the following properties may be defined:
* `bits` {number} The RSA bit size. Example: `1024`.
* `exponent` {string} The RSA exponent, as a string in hexadecimal number
notation. Example: `'0x010001'`.
* `modulus` {string} The RSA modulus, as a hexadecimal string. Example:
`'B56CE45CB7...'`.
* `pubkey` {Buffer} The public key.

For EC keys, the following properties may be defined:
* `pubkey` {Buffer} The public key.
* `bits` {number} The key size in bits. Example: `256`.
* `asn1Curve` {string} (Optional) The ASN.1 name of the OID of the elliptic
curve. Well-known curves are identified by an OID. While it is unusual, it is
possible that the curve is identified by its mathematical properties, in which
case it will not have an OID. Example: `'prime256v1'`.
* `nistCurve` {string} (Optional) The NIST name for the elliptic curve, if it
has one (not all well-known curves have been assigned names by NIST). Example:
`'P-256'`.

Example certificate:
```text
{ subject:
{ C: 'UK',
ST: 'Acknack Ltd',
L: 'Rhys Jones',
O: 'node.js',
OU: 'Test TLS Certificate',
CN: 'localhost' },
{ OU: [ 'Domain Control Validated', 'PositiveSSL Wildcard' ],
CN: '*.nodejs.org' },
issuer:
{ C: 'UK',
ST: 'Acknack Ltd',
L: 'Rhys Jones',
O: 'node.js',
OU: 'Test TLS Certificate',
CN: 'localhost' },
issuerCertificate:
{ ... another certificate, possibly with an .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' }
{ C: 'GB',
ST: 'Greater Manchester',
L: 'Salford',
O: 'COMODO CA Limited',
CN: 'COMODO RSA Domain Validation Secure Server CA' },
subjectaltname: 'DNS:*.nodejs.org, DNS:nodejs.org',
infoAccess:
{ 'CA Issuers - URI':
[ 'http://crt.comodoca.com/COMODORSADomainValidationSecureServerCA.crt' ],
'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 ... > }
```

If the peer does not provide a certificate, an empty object will be returned.
If the socket has been destroyed, `null` will be returned.

### tlsSocket.getPeerFinished()
<!-- YAML
added: v9.9.0
Expand Down Expand Up @@ -830,8 +888,7 @@ added: v0.8.4

* `hostname` {string} The host name or IP address to verify the certificate
against.
* `cert` {Object} An object representing the peer's certificate. The returned
object has some properties corresponding to the fields of the certificate.
* `cert` {Object} A [certificate object][] representing the peer's certificate.
* Returns: {Error|undefined}

Verifies the certificate `cert` is issued to `hostname`.
Expand All @@ -847,36 +904,6 @@ the checks done with additional verification.
This function is only called if the certificate passed all other checks, such as
being issued by trusted CA (`options.ca`).

The cert object contains the parsed certificate and will have a structure
similar to:

```text
{ subject:
{ OU: [ 'Domain Control Validated', 'PositiveSSL Wildcard' ],
CN: '*.nodejs.org' },
issuer:
{ C: 'GB',
ST: 'Greater Manchester',
L: 'Salford',
O: 'COMODO CA Limited',
CN: 'COMODO RSA Domain Validation Secure Server CA' },
subjectaltname: 'DNS:*.nodejs.org, DNS:nodejs.org',
infoAccess:
{ 'CA Issuers - URI':
[ 'http://crt.comodoca.com/COMODORSADomainValidationSecureServerCA.crt' ],
'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 ... > }
```

## tls.connect(options[, callback])
<!-- YAML
added: v0.11.3
Expand Down Expand Up @@ -1402,5 +1429,6 @@ where `secureSocket` has the same API as `pair.cleartext`.
[TLS Session Tickets]: https://www.ietf.org/rfc/rfc5077.txt
[TLS recommendations]: https://wiki.mozilla.org/Security/Server_Side_TLS
[asn1.js]: https://www.npmjs.com/package/asn1.js
[certificate object]: #tls_certificate_object
[modifying the default cipher suite]: #tls_modifying_the_default_tls_cipher_suite
[specific attacks affecting larger AES key sizes]: https://www.schneier.com/blog/archives/2009/07/another_new_aes.html
3 changes: 3 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
V(address_string, "address") \
V(aliases_string, "aliases") \
V(args_string, "args") \
V(asn1curve_string, "asn1Curve") \
V(async_ids_stack_string, "async_ids_stack") \
V(bits_string, "bits") \
V(buffer_string, "buffer") \
V(bytes_parsed_string, "bytesParsed") \
V(bytes_read_string, "bytesRead") \
Expand Down Expand Up @@ -207,6 +209,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
V(modulus_string, "modulus") \
V(name_string, "name") \
V(netmask_string, "netmask") \
V(nistcurve_string, "nistCurve") \
V(nsname_string, "nsname") \
V(ocsp_request_string, "OCSPRequest") \
V(onaltsvc_string, "onaltsvc") \
Expand Down
69 changes: 67 additions & 2 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ static const int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL
| XN_FLAG_FN_SN;

namespace node {
namespace Buffer {
// OpenSSL uses `unsigned char*` for raw data, make this easier for us.
v8::MaybeLocal<v8::Object> New(Environment* env, unsigned char* udata,
size_t length) {
char* data = reinterpret_cast<char*>(udata);
return Buffer::New(env, data, length);
}
} // namespace Buffer

namespace crypto {

using v8::Array;
Expand Down Expand Up @@ -1652,8 +1661,17 @@ static Local<Object> X509ToObject(Environment* env, X509* cert) {

EVPKeyPointer pkey(X509_get_pubkey(cert));
RSAPointer rsa;
if (pkey)
rsa.reset(EVP_PKEY_get1_RSA(pkey.get()));
ECPointer ec;
if (pkey) {
switch (EVP_PKEY_id(pkey.get())) {
case EVP_PKEY_RSA:
rsa.reset(EVP_PKEY_get1_RSA(pkey.get()));
break;
case EVP_PKEY_EC:
ec.reset(EVP_PKEY_get1_EC_KEY(pkey.get()));
break;
}
}

if (rsa) {
const BIGNUM* n;
Expand All @@ -1667,6 +1685,10 @@ static Local<Object> X509ToObject(Environment* env, X509* cert) {
mem->length).ToLocalChecked()).FromJust();
USE(BIO_reset(bio.get()));

int bits = BN_num_bits(n);
info->Set(context, env->bits_string(),
Integer::New(env->isolate(), bits)).FromJust();

uint64_t exponent_word = static_cast<uint64_t>(BN_get_word(e));
uint32_t lo = static_cast<uint32_t>(exponent_word);
uint32_t hi = static_cast<uint32_t>(exponent_word >> 32);
Expand All @@ -1689,10 +1711,53 @@ static Local<Object> X509ToObject(Environment* env, X509* cert) {
reinterpret_cast<unsigned char*>(Buffer::Data(pubbuff));
i2d_RSA_PUBKEY(rsa.get(), &pubserialized);
info->Set(env->context(), env->pubkey_string(), pubbuff).FromJust();
} else if (ec) {
const EC_GROUP* group = EC_KEY_get0_group(ec.get());
if (group != nullptr) {
int bits = EC_GROUP_order_bits(group);
if (bits > 0) {
info->Set(context, env->bits_string(),
Integer::New(env->isolate(), bits)).FromJust();
}
}

unsigned char* pub = nullptr;
size_t publen = EC_KEY_key2buf(ec.get(), EC_KEY_get_conv_form(ec.get()),
&pub, nullptr);
if (publen > 0) {
Local<Object> buf = Buffer::New(env, pub, publen).ToLocalChecked();
// Ownership of pub pointer accepted by Buffer.
pub = nullptr;
info->Set(context, env->pubkey_string(), buf).FromJust();
} else {
CHECK_NULL(pub);
}

if (EC_GROUP_get_asn1_flag(group) != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check for asn1_flag is unnecessary? It should be enough to just check nid != 0—if the curve has a name, nid will be non-zero, and if it doesn't, nid will be zero. No need to also check the asn1_flag, right?

(The reason I'm asking is because EC_GROUP_get_asn1_flag doesn't exist in BoringSSL and I'm working on BoringSSL integration downstream in Electron.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code was directly copied from

if (EC_GROUP_get_asn1_flag(x)) {
/* the curve parameter are given by an asn1 OID */
int nid;
const char *nname;
if (!BIO_indent(bp, off, 128))
goto err;
nid = EC_GROUP_get_curve_name(x);
if (nid == 0)
goto err;
if (BIO_printf(bp, "ASN1 OID: %s", OBJ_nid2sn(nid)) <= 0)
goto err;
if (BIO_printf(bp, "\n") <= 0)
goto err;
nname = EC_curve_nid2nist(nid);
if (nname) {
if (!BIO_indent(bp, off, 128))
goto err;
if (BIO_printf(bp, "NIST CURVE: %s\n", nname) <= 0)
goto err;
}
} else {
but reading through the docs, yes, I suppose just trying to get the nid would work. I'll update this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see: #25345

// Curve is well-known, get its OID and NIST nick-name (if it has one).

int nid = EC_GROUP_get_curve_name(group);
if (nid != 0) {
if (const char* sn = OBJ_nid2sn(nid)) {
info->Set(context, env->asn1curve_string(),
OneByteString(env->isolate(), sn)).FromJust();
}
}
if (nid != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge these two conditionals?

if (const char* nist = EC_curve_nid2nist(nid)) {
info->Set(context, env->nistcurve_string(),
OneByteString(env->isolate(), nist)).FromJust();
}
}
} else {
// Unnamed curves can be described by their mathematical properties,
// but aren't used much (at all?) with X.509/TLS. Support later if needed.
}
}

pkey.reset();
rsa.reset();
ec.reset();

ASN1_TIME_print(bio.get(), X509_get_notBefore(cert));
BIO_get_mem_ptr(bio.get(), &mem);
Expand Down
1 change: 1 addition & 0 deletions src/node_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ using EVPKeyPointer = DeleteFnPtr<EVP_PKEY, EVP_PKEY_free>;
using EVPKeyCtxPointer = DeleteFnPtr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
using EVPMDPointer = DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free>;
using RSAPointer = DeleteFnPtr<RSA, RSA_free>;
using ECPointer = DeleteFnPtr<EC_KEY, EC_KEY_free>;
using BignumPointer = DeleteFnPtr<BIGNUM, BN_free>;
using NetscapeSPKIPointer = DeleteFnPtr<NETSCAPE_SPKI, NETSCAPE_SPKI_free>;
using ECGroupPointer = DeleteFnPtr<EC_GROUP, EC_GROUP_free>;
Expand Down
50 changes: 50 additions & 0 deletions test/parallel/test-tls-peer-certificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ connect({
assert.strictEqual(peerCert.subject.emailAddress, 'ry@tinyclouds.org');
assert.strictEqual(peerCert.serialNumber, 'ECC9B856270DA9A8');
assert.strictEqual(peerCert.exponent, '0x10001');
assert.strictEqual(peerCert.bits, 1024);
// The conversion to bits is odd because modulus isn't a buffer, its a hex
// string. There are two hex chars for every byte of modulus, and 8 bits per
// byte.
assert.strictEqual(peerCert.modulus.length / 2 * 8, peerCert.bits);
assert.strictEqual(
peerCert.fingerprint,
'D7:FD:F6:42:92:A8:83:51:8E:80:48:62:66:DA:85:C2:EE:A6:A1:CD'
Expand Down Expand Up @@ -86,3 +91,48 @@ connect({

return cleanup();
});

connect({
client: { rejectUnauthorized: false },
server: keys.ec,
}, function(err, pair, cleanup) {
assert.ifError(err);
const socket = pair.client.conn;
let peerCert = socket.getPeerCertificate(true);
assert.ok(peerCert.issuerCertificate);

peerCert = socket.getPeerCertificate(true);
debug('peerCert:\n', peerCert);

assert.ok(peerCert.issuerCertificate);
assert.strictEqual(peerCert.subject.emailAddress, 'ry@tinyclouds.org');
assert.strictEqual(peerCert.serialNumber, 'C1EA7B03D5956D52');
assert.strictEqual(peerCert.exponent, undefined);
assert.strictEqual(peerCert.pubKey, undefined);
assert.strictEqual(peerCert.modulus, undefined);
assert.strictEqual(
peerCert.fingerprint,
'DF:F0:D3:6B:C3:E7:74:7C:C7:F3:FB:1E:33:12:AE:6C:8D:53:5F:74'
);
assert.strictEqual(
peerCert.fingerprint256,
'AB:08:3C:40:C7:07:D7:D1:79:32:92:3B:96:52:D0:38:4C:22:ED:CD:23:51:D0:A1:' +
'67:AA:33:A0:D5:26:5C:41'
);

assert.strictEqual(
sha256(peerCert.pubkey).digest('hex'),
'ec68fc7d5e32cd4e1da5a7b59c0a2229be6f82fcc9bf8c8691a2262aacb14f53'
);
assert.strictEqual(peerCert.asn1Curve, 'prime256v1');
assert.strictEqual(peerCert.nistCurve, 'P-256');
assert.strictEqual(peerCert.bits, 256);

assert.deepStrictEqual(peerCert.infoAccess, undefined);

const issuer = peerCert.issuerCertificate;
assert.strictEqual(issuer.issuerCertificate, issuer);
assert.strictEqual(issuer.serialNumber, 'C1EA7B03D5956D52');

return cleanup();
});