tls: add ability to get cert/peer cert as X509Certificate object

Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: https://github.com/nodejs/node/pull/37070
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
This commit is contained in:
James M Snell 2021-01-25 16:38:51 -08:00
parent 84e41d27da
commit c973d503e0
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
9 changed files with 218 additions and 74 deletions

View File

@ -1804,6 +1804,16 @@ added: v15.6.0
The issuer identification included in this certificate.
### `x509.issuerCertificate`
<!-- YAML
added: REPLACEME
-->
* Type: {X509Certificate}
The issuer certificate or `undefined` if the issuer certificate is not
available.
### `x509.keyUsage`
<!-- YAML
added: v15.6.0

View File

@ -904,6 +904,41 @@ added: v0.11.4
Always returns `true`. This may be used to distinguish TLS sockets from regular
`net.Socket` instances.
### `tlsSocket.exportKeyingMaterial(length, label[, context])`
<!-- YAML
added:
- v13.10.0
- v12.17.0
-->
* `length` {number} number of bytes to retrieve from keying material
* `label` {string} an application specific label, typically this will be a
value from the
[IANA Exporter Label Registry](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#exporter-labels).
* `context` {Buffer} Optionally provide a context.
* Returns: {Buffer} requested bytes of the keying material
Keying material is used for validations to prevent different kind of attacks in
network protocols, for example in the specifications of IEEE 802.1X.
Example
```js
const keyingMaterial = tlsSocket.exportKeyingMaterial(
128,
'client finished');
/**
Example return value of keyingMaterial:
<Buffer 76 26 af 99 c5 56 8e 42 09 91 ef 9f 93 cb ad 6c 7b 65 f8 53 f1 d8 d9
12 5a 33 b8 b5 25 df 7b 37 9f e0 e2 4f b8 67 83 a3 2f cd 5d 41 42 4c 91
74 ef 2c ... 78 more bytes>
*/
```
See the OpenSSL [`SSL_export_keying_material`][] documentation for more
information.
### `tlsSocket.getCertificate()`
<!-- YAML
added: v11.2.0
@ -1113,6 +1148,18 @@ provided by SSL/TLS is not desired or is not enough.
Corresponds to the `SSL_get_peer_finished` routine in OpenSSL and may be used
to implement the `tls-unique` channel binding from [RFC 5929][].
### `tlsSocket.getPeerX509Certificate()`
<!-- YAML
added: REPLACEME
-->
* Returns: {X509Certificate}
Returns the peer certificate as an {X509Certificate} object.
If there is no peer certificate, or the socket has been destroyed,
`undefined` will be returned.
### `tlsSocket.getProtocol()`
<!-- YAML
added: v5.7.0
@ -1164,41 +1211,6 @@ See
[SSL_get_shared_sigalgs](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_shared_sigalgs.html)
for more information.
### `tlsSocket.exportKeyingMaterial(length, label[, context])`
<!-- YAML
added:
- v13.10.0
- v12.17.0
-->
* `length` {number} number of bytes to retrieve from keying material
* `label` {string} an application specific label, typically this will be a
value from the
[IANA Exporter Label Registry](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#exporter-labels).
* `context` {Buffer} Optionally provide a context.
* Returns: {Buffer} requested bytes of the keying material
Keying material is used for validations to prevent different kind of attacks in
network protocols, for example in the specifications of IEEE 802.1X.
Example
```js
const keyingMaterial = tlsSocket.exportKeyingMaterial(
128,
'client finished');
/**
Example return value of keyingMaterial:
<Buffer 76 26 af 99 c5 56 8e 42 09 91 ef 9f 93 cb ad 6c 7b 65 f8 53 f1 d8 d9
12 5a 33 b8 b5 25 df 7b 37 9f e0 e2 4f b8 67 83 a3 2f cd 5d 41 42 4c 91
74 ef 2c ... 78 more bytes>
*/
```
See the OpenSSL [`SSL_export_keying_material`][] documentation for more
information.
### `tlsSocket.getTLSTicket()`
<!-- YAML
added: v0.11.4
@ -1213,6 +1225,18 @@ It may be useful for debugging.
See [Session Resumption][] for more information.
### `tlsSocket.getX509Certificate()`
<!-- YAML
added: REPLACEME
-->
* Returns: {X509Certificate}
Returns the local certificate as an {X509Certificate} object.
If there is no local certificate, or the socket has been destroyed,
`undefined` will be returned.
### `tlsSocket.isSessionReused()`
<!-- YAML
added: v0.5.6

View File

@ -90,6 +90,9 @@ const {
validateString,
validateUint32
} = require('internal/validators');
const {
InternalX509Certificate
} = require('internal/crypto/x509');
const traceTls = getOptionValue('--trace-tls');
const tlsKeylog = getOptionValue('--tls-keylog');
const { appendFile } = require('fs');
@ -998,6 +1001,16 @@ TLSSocket.prototype.getCertificate = function() {
return null;
};
TLSSocket.prototype.getPeerX509Certificate = function(detailed) {
const cert = this._handle?.getPeerX509Certificate();
return cert ? new InternalX509Certificate(cert) : undefined;
};
TLSSocket.prototype.getX509Certificate = function() {
const cert = this._handle?.getX509Certificate();
return cert ? new InternalX509Certificate(cert) : undefined;
};
// Proxy TLSSocket handle methods
function makeSocketMethodProxy(name) {
return function socketMethodProxy(...args) {

View File

@ -90,6 +90,15 @@ function getFlags(options = {}) {
return flags;
}
class InternalX509Certificate extends JSTransferable {
[kInternalState] = new SafeMap();
constructor(handle) {
super();
this[kHandle] = handle;
}
}
class X509Certificate extends JSTransferable {
[kInternalState] = new SafeMap();
@ -168,6 +177,17 @@ class X509Certificate extends JSTransferable {
return value;
}
get issuerCertificate() {
let value = this[kInternalState].get('issuerCertificate');
if (value === undefined) {
const cert = this[kHandle].getIssuerCert();
if (cert)
value = new InternalX509Certificate(this[kHandle].getIssuerCert());
this[kInternalState].set('issuerCertificate', value);
}
return value;
}
get infoAccess() {
let value = this[kInternalState].get('infoAccess');
if (value === undefined) {
@ -313,15 +333,6 @@ class X509Certificate extends JSTransferable {
}
}
class InternalX509Certificate extends JSTransferable {
[kInternalState] = new SafeMap();
constructor(handle) {
super();
this[kHandle] = handle;
}
}
InternalX509Certificate.prototype.constructor = X509Certificate;
ObjectSetPrototypeOf(
InternalX509Certificate.prototype,

View File

@ -1591,6 +1591,20 @@ void TLSWrap::GetPeerCertificate(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(ret);
}
void TLSWrap::GetPeerX509Certificate(const FunctionCallbackInfo<Value>& args) {
TLSWrap* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder());
Environment* env = w->env();
X509Certificate::GetPeerCertificateFlag flag = w->is_server()
? X509Certificate::GetPeerCertificateFlag::SERVER
: X509Certificate::GetPeerCertificateFlag::NONE;
Local<Value> ret;
if (X509Certificate::GetPeerCert(env, w->ssl_, flag).ToLocal(&ret))
args.GetReturnValue().Set(ret);
}
void TLSWrap::GetCertificate(const FunctionCallbackInfo<Value>& args) {
TLSWrap* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder());
@ -1601,6 +1615,15 @@ void TLSWrap::GetCertificate(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(ret);
}
void TLSWrap::GetX509Certificate(const FunctionCallbackInfo<Value>& args) {
TLSWrap* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder());
Environment* env = w->env();
Local<Value> ret;
if (X509Certificate::GetCert(env, w->ssl_).ToLocal(&ret))
args.GetReturnValue().Set(ret);
}
void TLSWrap::GetFinished(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
@ -2051,11 +2074,14 @@ void TLSWrap::Initialize(
env->SetProtoMethodNoSideEffect(t, "getALPNNegotiatedProtocol",
GetALPNNegotiatedProto);
env->SetProtoMethodNoSideEffect(t, "getCertificate", GetCertificate);
env->SetProtoMethodNoSideEffect(t, "getX509Certificate", GetX509Certificate);
env->SetProtoMethodNoSideEffect(t, "getCipher", GetCipher);
env->SetProtoMethodNoSideEffect(t, "getEphemeralKeyInfo",
GetEphemeralKeyInfo);
env->SetProtoMethodNoSideEffect(t, "getFinished", GetFinished);
env->SetProtoMethodNoSideEffect(t, "getPeerCertificate", GetPeerCertificate);
env->SetProtoMethodNoSideEffect(t, "getPeerX509Certificate",
GetPeerX509Certificate);
env->SetProtoMethodNoSideEffect(t, "getPeerFinished", GetPeerFinished);
env->SetProtoMethodNoSideEffect(t, "getProtocol", GetProtocol);
env->SetProtoMethodNoSideEffect(t, "getSession", GetSession);

View File

@ -184,12 +184,16 @@ class TLSWrap : public AsyncWrap,
static void GetALPNNegotiatedProto(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetCertificate(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetX509Certificate(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetCipher(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetEphemeralKeyInfo(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetFinished(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetPeerCertificate(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetPeerX509Certificate(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetPeerFinished(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetProtocol(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetServername(const v8::FunctionCallbackInfo<v8::Value>& args);

View File

@ -15,7 +15,6 @@
namespace node {
using v8::Array;
using v8::ArrayBufferView;
using v8::Context;
using v8::EscapableHandleScope;
@ -82,6 +81,7 @@ Local<FunctionTemplate> X509Certificate::GetConstructorTemplate(
env->SetProtoMethod(tmpl, "checkPrivateKey", CheckPrivateKey);
env->SetProtoMethod(tmpl, "verify", Verify);
env->SetProtoMethod(tmpl, "toLegacy", ToLegacy);
env->SetProtoMethod(tmpl, "getIssuerCert", GetIssuerCert);
env->set_x509_constructor_template(tmpl);
}
return tmpl;
@ -93,14 +93,16 @@ bool X509Certificate::HasInstance(Environment* env, Local<Object> object) {
MaybeLocal<Object> X509Certificate::New(
Environment* env,
X509Pointer cert) {
X509Pointer cert,
STACK_OF(X509)* issuer_chain) {
std::shared_ptr<ManagedX509> mcert(new ManagedX509(std::move(cert)));
return New(env, std::move(mcert));
return New(env, std::move(mcert), issuer_chain);
}
MaybeLocal<Object> X509Certificate::New(
Environment* env,
std::shared_ptr<ManagedX509> cert) {
std::shared_ptr<ManagedX509> cert,
STACK_OF(X509)* issuer_chain) {
EscapableHandleScope scope(env->isolate());
Local<Function> ctor;
if (!GetConstructorTemplate(env)->GetFunction(env->context()).ToLocal(&ctor))
@ -110,7 +112,7 @@ MaybeLocal<Object> X509Certificate::New(
if (!ctor->NewInstance(env->context()).ToLocal(&obj))
return MaybeLocal<Object>();
new X509Certificate(env, obj, std::move(cert));
new X509Certificate(env, obj, std::move(cert), issuer_chain);
return scope.Escape(obj);
}
@ -122,7 +124,7 @@ MaybeLocal<Object> X509Certificate::GetCert(
if (cert == nullptr)
return MaybeLocal<Object>();
X509Pointer ptr(cert);
X509Pointer ptr(X509_dup(cert));
return New(env, std::move(ptr));
}
@ -130,16 +132,12 @@ MaybeLocal<Object> X509Certificate::GetPeerCert(
Environment* env,
const SSLPointer& ssl,
GetPeerCertificateFlag flag) {
EscapableHandleScope scope(env->isolate());
ClearErrorOnReturn clear_error_on_return;
Local<Object> obj;
MaybeLocal<Object> maybe_cert;
bool is_server =
static_cast<int>(flag) & static_cast<int>(GetPeerCertificateFlag::SERVER);
bool abbreviated =
static_cast<int>(flag)
& static_cast<int>(GetPeerCertificateFlag::ABBREVIATED);
X509Pointer cert(is_server ? SSL_get_peer_certificate(ssl.get()) : nullptr);
STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(ssl.get());
@ -148,23 +146,14 @@ MaybeLocal<Object> X509Certificate::GetPeerCert(
std::vector<Local<Value>> certs;
if (!cert) cert.reset(sk_X509_value(ssl_certs, 0));
if (!X509Certificate::New(env, std::move(cert)).ToLocal(&obj))
return MaybeLocal<Object>();
certs.push_back(obj);
int count = sk_X509_num(ssl_certs);
if (!abbreviated) {
for (int i = 0; i < count; i++) {
cert.reset(X509_dup(sk_X509_value(ssl_certs, i)));
if (!cert || !X509Certificate::New(env, std::move(cert)).ToLocal(&obj))
return MaybeLocal<Object>();
certs.push_back(obj);
}
if (!cert) {
cert.reset(sk_X509_value(ssl_certs, 0));
sk_X509_delete(ssl_certs, 0);
}
return scope.Escape(Array::New(env->isolate(), certs.data(), certs.size()));
return sk_X509_num(ssl_certs)
? New(env, std::move(cert), ssl_certs)
: New(env, std::move(cert));
}
void X509Certificate::Parse(const FunctionCallbackInfo<Value>& args) {
@ -475,13 +464,32 @@ void X509Certificate::ToLegacy(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(ret);
}
void X509Certificate::GetIssuerCert(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.Holder());
if (cert->issuer_cert_)
args.GetReturnValue().Set(cert->issuer_cert_->object());
}
X509Certificate::X509Certificate(
Environment* env,
Local<Object> object,
std::shared_ptr<ManagedX509> cert)
std::shared_ptr<ManagedX509> cert,
STACK_OF(X509)* issuer_chain)
: BaseObject(env, object),
cert_(std::move(cert)) {
MakeWeak();
if (issuer_chain != nullptr && sk_X509_num(issuer_chain)) {
X509Pointer cert(X509_dup(sk_X509_value(issuer_chain, 0)));
sk_X509_delete(issuer_chain, 0);
Local<Object> obj = sk_X509_num(issuer_chain)
? X509Certificate::New(env, std::move(cert), issuer_chain)
.ToLocalChecked()
: X509Certificate::New(env, std::move(cert))
.ToLocalChecked();
issuer_cert_.reset(Unwrap<X509Certificate>(obj));
}
}
void X509Certificate::MemoryInfo(MemoryTracker* tracker) const {

View File

@ -38,7 +38,7 @@ class ManagedX509 : public MemoryRetainer {
class X509Certificate : public BaseObject {
public:
enum class GetPeerCertificateFlag {
ABBREVIATED,
NONE,
SERVER
};
@ -49,11 +49,13 @@ class X509Certificate : public BaseObject {
static v8::MaybeLocal<v8::Object> New(
Environment* env,
X509Pointer cert);
X509Pointer cert,
STACK_OF(X509)* issuer_chain = nullptr);
static v8::MaybeLocal<v8::Object> New(
Environment* env,
std::shared_ptr<ManagedX509> cert);
std::shared_ptr<ManagedX509> cert,
STACK_OF(X509)* issuer_chain = nullptr);
static v8::MaybeLocal<v8::Object> GetCert(
Environment* env,
@ -91,6 +93,7 @@ class X509Certificate : public BaseObject {
static void CheckPrivateKey(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Verify(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ToLegacy(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetIssuerCert(const v8::FunctionCallbackInfo<v8::Value>& args);
X509* get() { return cert_->get(); }
@ -124,9 +127,11 @@ class X509Certificate : public BaseObject {
X509Certificate(
Environment* env,
v8::Local<v8::Object> object,
std::shared_ptr<ManagedX509> cert);
std::shared_ptr<ManagedX509> cert,
STACK_OF(X509)* issuer_chain = nullptr);
std::shared_ptr<ManagedX509> cert_;
BaseObjectPtr<X509Certificate> issuer_cert_;
};
} // namespace crypto

View File

@ -0,0 +1,43 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const tls = require('tls');
const fixtures = require('../common/fixtures');
const { X509Certificate } = require('crypto');
const options = {
key: fixtures.readKey('agent6-key.pem'),
cert: fixtures.readKey('agent6-cert.pem')
};
const server = tls.createServer(options, function(cleartext) {
cleartext.end('World');
});
server.once('secureConnection', common.mustCall(function(socket) {
const cert = socket.getX509Certificate();
assert(cert instanceof X509Certificate);
assert.strictEqual(
cert.serialNumber,
'D0082F458B6EFBE8');
}));
server.listen(0, common.mustCall(function() {
const socket = tls.connect({
port: this.address().port,
rejectUnauthorized: false
}, common.mustCall(function() {
const peerCert = socket.getPeerX509Certificate();
assert(peerCert.issuerCertificate instanceof X509Certificate);
assert.strictEqual(peerCert.issuerCertificate.issuerCertificate, undefined);
assert.strictEqual(
peerCert.issuerCertificate.serialNumber,
'ECC9B856270DA9A7'
);
server.close();
}));
socket.end('Hello');
}));