Skip to content

Commit

Permalink
tls: include elliptic curve X.509 public key info
Browse files Browse the repository at this point in the history
X.509 certs are provided to the user in a parsed object form by a number
of TLS APIs. Include public key info for elliptic curves as well, not
just RSA.
- pubkey: the public key
- bits: the strength of the curve
- asn1Curve: the ASN.1 OID for the curve
- nistCurve: the NIST nickname for the curve, if it has one

PR-URL: #24358
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
  • Loading branch information
sam-github authored and rvagg committed Nov 28, 2018
1 parent dadc2eb commit 37f0bd7
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 3 deletions.
19 changes: 18 additions & 1 deletion doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,12 @@ If the full certificate chain was requested, each certificate will include an
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.
Expand Down Expand Up @@ -688,7 +694,18 @@ For RSA keys, the following properties may be defined:
`'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:
{ OU: [ 'Domain Control Validated', 'PositiveSSL Wildcard' ],
Expand Down
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
65 changes: 63 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 @@ -1629,8 +1638,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 Down Expand Up @@ -1666,10 +1684,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) {
// 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) {
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
45 changes: 45 additions & 0 deletions test/parallel/test-tls-peer-certificate.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,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();
});

0 comments on commit 37f0bd7

Please sign in to comment.