Skip to content

Commit 37f0bd7

Browse files
sam-githubrvagg
authored andcommitted
tls: include elliptic curve X.509 public key info
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>
1 parent dadc2eb commit 37f0bd7

File tree

5 files changed

+130
-3
lines changed

5 files changed

+130
-3
lines changed

doc/api/tls.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,12 @@ If the full certificate chain was requested, each certificate will include an
649649
certificate.
650650

651651
#### Certificate Object
652+
<!-- YAML
653+
changes:
654+
- version: REPLACEME
655+
pr-url: https://github.com/nodejs/node/pull/24358
656+
description: Support Elliptic Curve public key info.
657+
-->
652658

653659
A certificate object has properties corresponding to the fields of the
654660
certificate.
@@ -688,7 +694,18 @@ For RSA keys, the following properties may be defined:
688694
`'B56CE45CB7...'`.
689695
* `pubkey` {Buffer} The public key.
690696

691-
697+
For EC keys, the following properties may be defined:
698+
* `pubkey` {Buffer} The public key.
699+
* `bits` {number} The key size in bits. Example: `256`.
700+
* `asn1Curve` {string} (Optional) The ASN.1 name of the OID of the elliptic
701+
curve. Well-known curves are identified by an OID. While it is unusual, it is
702+
possible that the curve is identified by its mathematical properties, in which
703+
case it will not have an OID. Example: `'prime256v1'`.
704+
* `nistCurve` {string} (Optional) The NIST name for the elliptic curve, if it
705+
has one (not all well-known curves have been assigned names by NIST). Example:
706+
`'P-256'`.
707+
708+
Example certificate:
692709
```text
693710
{ subject:
694711
{ OU: [ 'Domain Control Validated', 'PositiveSSL Wildcard' ],

src/env.h

+3
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
124124
V(address_string, "address") \
125125
V(aliases_string, "aliases") \
126126
V(args_string, "args") \
127+
V(asn1curve_string, "asn1Curve") \
127128
V(async_ids_stack_string, "async_ids_stack") \
129+
V(bits_string, "bits") \
128130
V(buffer_string, "buffer") \
129131
V(bytes_parsed_string, "bytesParsed") \
130132
V(bytes_read_string, "bytesRead") \
@@ -207,6 +209,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
207209
V(modulus_string, "modulus") \
208210
V(name_string, "name") \
209211
V(netmask_string, "netmask") \
212+
V(nistcurve_string, "nistCurve") \
210213
V(nsname_string, "nsname") \
211214
V(ocsp_request_string, "OCSPRequest") \
212215
V(onaltsvc_string, "onaltsvc") \

src/node_crypto.cc

+63-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ static const int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL
5252
| XN_FLAG_FN_SN;
5353

5454
namespace node {
55+
namespace Buffer {
56+
// OpenSSL uses `unsigned char*` for raw data, make this easier for us.
57+
v8::MaybeLocal<v8::Object> New(Environment* env, unsigned char* udata,
58+
size_t length) {
59+
char* data = reinterpret_cast<char*>(udata);
60+
return Buffer::New(env, data, length);
61+
}
62+
} // namespace Buffer
63+
5564
namespace crypto {
5665

5766
using v8::Array;
@@ -1629,8 +1638,17 @@ static Local<Object> X509ToObject(Environment* env, X509* cert) {
16291638

16301639
EVPKeyPointer pkey(X509_get_pubkey(cert));
16311640
RSAPointer rsa;
1632-
if (pkey)
1633-
rsa.reset(EVP_PKEY_get1_RSA(pkey.get()));
1641+
ECPointer ec;
1642+
if (pkey) {
1643+
switch (EVP_PKEY_id(pkey.get())) {
1644+
case EVP_PKEY_RSA:
1645+
rsa.reset(EVP_PKEY_get1_RSA(pkey.get()));
1646+
break;
1647+
case EVP_PKEY_EC:
1648+
ec.reset(EVP_PKEY_get1_EC_KEY(pkey.get()));
1649+
break;
1650+
}
1651+
}
16341652

16351653
if (rsa) {
16361654
const BIGNUM* n;
@@ -1666,10 +1684,53 @@ static Local<Object> X509ToObject(Environment* env, X509* cert) {
16661684
reinterpret_cast<unsigned char*>(Buffer::Data(pubbuff));
16671685
i2d_RSA_PUBKEY(rsa.get(), &pubserialized);
16681686
info->Set(env->context(), env->pubkey_string(), pubbuff).FromJust();
1687+
} else if (ec) {
1688+
const EC_GROUP* group = EC_KEY_get0_group(ec.get());
1689+
if (group != nullptr) {
1690+
int bits = EC_GROUP_order_bits(group);
1691+
if (bits > 0) {
1692+
info->Set(context, env->bits_string(),
1693+
Integer::New(env->isolate(), bits)).FromJust();
1694+
}
1695+
}
1696+
1697+
unsigned char* pub = nullptr;
1698+
size_t publen = EC_KEY_key2buf(ec.get(), EC_KEY_get_conv_form(ec.get()),
1699+
&pub, nullptr);
1700+
if (publen > 0) {
1701+
Local<Object> buf = Buffer::New(env, pub, publen).ToLocalChecked();
1702+
// Ownership of pub pointer accepted by Buffer.
1703+
pub = nullptr;
1704+
info->Set(context, env->pubkey_string(), buf).FromJust();
1705+
} else {
1706+
CHECK_NULL(pub);
1707+
}
1708+
1709+
if (EC_GROUP_get_asn1_flag(group) != 0) {
1710+
// Curve is well-known, get its OID and NIST nick-name (if it has one).
1711+
1712+
int nid = EC_GROUP_get_curve_name(group);
1713+
if (nid != 0) {
1714+
if (const char* sn = OBJ_nid2sn(nid)) {
1715+
info->Set(context, env->asn1curve_string(),
1716+
OneByteString(env->isolate(), sn)).FromJust();
1717+
}
1718+
}
1719+
if (nid != 0) {
1720+
if (const char* nist = EC_curve_nid2nist(nid)) {
1721+
info->Set(context, env->nistcurve_string(),
1722+
OneByteString(env->isolate(), nist)).FromJust();
1723+
}
1724+
}
1725+
} else {
1726+
// Unnamed curves can be described by their mathematical properties,
1727+
// but aren't used much (at all?) with X.509/TLS. Support later if needed.
1728+
}
16691729
}
16701730

16711731
pkey.reset();
16721732
rsa.reset();
1733+
ec.reset();
16731734

16741735
ASN1_TIME_print(bio.get(), X509_get_notBefore(cert));
16751736
BIO_get_mem_ptr(bio.get(), &mem);

src/node_crypto.h

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ using EVPKeyPointer = DeleteFnPtr<EVP_PKEY, EVP_PKEY_free>;
8181
using EVPKeyCtxPointer = DeleteFnPtr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
8282
using EVPMDPointer = DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free>;
8383
using RSAPointer = DeleteFnPtr<RSA, RSA_free>;
84+
using ECPointer = DeleteFnPtr<EC_KEY, EC_KEY_free>;
8485
using BignumPointer = DeleteFnPtr<BIGNUM, BN_free>;
8586
using NetscapeSPKIPointer = DeleteFnPtr<NETSCAPE_SPKI, NETSCAPE_SPKI_free>;
8687
using ECGroupPointer = DeleteFnPtr<EC_GROUP, EC_GROUP_free>;

test/parallel/test-tls-peer-certificate.js

+45
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,48 @@ connect({
8686

8787
return cleanup();
8888
});
89+
90+
connect({
91+
client: { rejectUnauthorized: false },
92+
server: keys.ec,
93+
}, function(err, pair, cleanup) {
94+
assert.ifError(err);
95+
const socket = pair.client.conn;
96+
let peerCert = socket.getPeerCertificate(true);
97+
assert.ok(peerCert.issuerCertificate);
98+
99+
peerCert = socket.getPeerCertificate(true);
100+
debug('peerCert:\n', peerCert);
101+
102+
assert.ok(peerCert.issuerCertificate);
103+
assert.strictEqual(peerCert.subject.emailAddress, 'ry@tinyclouds.org');
104+
assert.strictEqual(peerCert.serialNumber, 'C1EA7B03D5956D52');
105+
assert.strictEqual(peerCert.exponent, undefined);
106+
assert.strictEqual(peerCert.pubKey, undefined);
107+
assert.strictEqual(peerCert.modulus, undefined);
108+
assert.strictEqual(
109+
peerCert.fingerprint,
110+
'DF:F0:D3:6B:C3:E7:74:7C:C7:F3:FB:1E:33:12:AE:6C:8D:53:5F:74'
111+
);
112+
assert.strictEqual(
113+
peerCert.fingerprint256,
114+
'AB:08:3C:40:C7:07:D7:D1:79:32:92:3B:96:52:D0:38:4C:22:ED:CD:23:51:D0:A1:' +
115+
'67:AA:33:A0:D5:26:5C:41'
116+
);
117+
118+
assert.strictEqual(
119+
sha256(peerCert.pubkey).digest('hex'),
120+
'ec68fc7d5e32cd4e1da5a7b59c0a2229be6f82fcc9bf8c8691a2262aacb14f53'
121+
);
122+
assert.strictEqual(peerCert.asn1Curve, 'prime256v1');
123+
assert.strictEqual(peerCert.nistCurve, 'P-256');
124+
assert.strictEqual(peerCert.bits, 256);
125+
126+
assert.deepStrictEqual(peerCert.infoAccess, undefined);
127+
128+
const issuer = peerCert.issuerCertificate;
129+
assert.strictEqual(issuer.issuerCertificate, issuer);
130+
assert.strictEqual(issuer.serialNumber, 'C1EA7B03D5956D52');
131+
132+
return cleanup();
133+
});

0 commit comments

Comments
 (0)