From 37e440211083389c5cbc6dad5c22ebace5ab1bc0 Mon Sep 17 00:00:00 2001 From: cruz3rblade Date: Wed, 7 Aug 2024 18:51:16 +0300 Subject: [PATCH] feat: expose client's peer certificate der data (#1062) * expose getPeerCertificate function * add util method to extract certificate info * add peer certificate verification example * lint * return the x509cert in pem format * check for nullptr --- examples/PeerCertificate.js | 134 ++++++++++++++++++++++++++++++++++++ src/HttpResponseWrapper.h | 16 +++++ src/Utilities.h | 33 +++++++++ 3 files changed, 183 insertions(+) create mode 100644 examples/PeerCertificate.js diff --git a/examples/PeerCertificate.js b/examples/PeerCertificate.js new file mode 100644 index 00000000..27f85ba4 --- /dev/null +++ b/examples/PeerCertificate.js @@ -0,0 +1,134 @@ +const https = require('https'); +const forge = require('node-forge'); +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +// Generate CA certificate +const ca = generateCACertificate(); +const caCertPem = pemEncodeCert(ca.cert); + +// Generate server certificate signed by CA +const serverCert = generateCertificate(ca, 'localhost'); +const serverCertPem = pemEncodeCert(serverCert.cert); +const serverKeyPem = pemEncodeKey(serverCert.privateKey); + +// Generate client certificate signed by CA +const clientCert = generateCertificate(ca, 'client'); +const clientCertPem = pemEncodeCert(clientCert.cert); +const clientKeyPem = pemEncodeKey(clientCert.privateKey); + +fs.writeFileSync(path.join(__dirname, "server.ca"), caCertPem); +fs.writeFileSync(path.join(__dirname, "server.key"), serverKeyPem); +fs.writeFileSync(path.join(__dirname, "server.cert"), serverCertPem); + +const uWS = require('../dist/uws'); +const port = 8086; + +const app = uWS.SSLApp({ + cert_file_name: path.join(__dirname, "server.cert"), + key_file_name: path.join(__dirname, "server.key"), + ca_file_name: path.join(__dirname, "server.ca") +}).get('/*', (res, req) => { + const clientCert = res.getX509Certificate(); + const x509 = new crypto.X509Certificate(clientCert); + if (x509.verify(crypto.createPublicKey(caCertPem))) { + res.end(`Hello World! your certificate is valid!`); + } + else + res.end('Hello World!'); + +}).listen(port, (token) => { + if (token) { + console.log('Listening to port ' + port); + sendClientRequest(); + } else { + console.log('Failed to listen to port ' + port); + } +}); + +function generateCACertificate() { + const keys = forge.pki.rsa.generateKeyPair(2048); + const cert = forge.pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = '01'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); + const attrs = [ + { name: 'commonName', value: 'example.org' }, + { name: 'countryName', value: 'US' }, + { shortName: 'ST', value: 'California' }, + { name: 'localityName', value: 'San Francisco' }, + { name: 'organizationName', value: 'example.org' }, + { shortName: 'OU', value: 'Test' } + ]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.setExtensions([{ + name: 'basicConstraints', + cA: true + }]); + cert.sign(keys.privateKey, forge.md.sha256.create()); + return { + privateKey: keys.privateKey, + publicKey: keys.publicKey, + cert: cert + }; +} + +function generateCertificate(ca, commonName) { + const keys = forge.pki.rsa.generateKeyPair(2048); + const cert = forge.pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = '02'; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); + const attrs = [ + { name: 'commonName', value: commonName } + ]; + cert.setSubject(attrs); + cert.setIssuer(ca.cert.subject.attributes); + cert.sign(ca.privateKey, forge.md.sha256.create()); + return { + privateKey: keys.privateKey, + cert: cert + }; +} + +function pemEncodeCert(cert) { + return forge.pki.certificateToPem(cert); +} + +function pemEncodeKey(key) { + return forge.pki.privateKeyToPem(key); +} + +function sendClientRequest() { + const clientOptions = { + hostname: 'localhost', + port: port, + path: '/', + method: 'GET', + key: clientKeyPem, + cert: clientCertPem, + ca: [caCertPem], + rejectUnauthorized: false + }; + + const req = https.request(clientOptions, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + console.log('Response from server:', data); + }); + }); + + req.on('error', (e) => { + console.error('errp', e); + }); + + req.end(); +} diff --git a/src/HttpResponseWrapper.h b/src/HttpResponseWrapper.h index c333c87d..73c3a344 100644 --- a/src/HttpResponseWrapper.h +++ b/src/HttpResponseWrapper.h @@ -183,6 +183,18 @@ struct HttpResponseWrapper { } } + template + static void res_getX509Certificate(const FunctionCallbackInfo &args) { + Isolate *isolate = args.GetIsolate(); + auto *res = getHttpResponse(args); + if (res) { + void* sslHandle = res->getNativeHandle(); + SSL* ssl = static_cast(sslHandle); + std::string x509cert = extractX509PemCertificate(ssl); + args.GetReturnValue().Set(String::NewFromUtf8(isolate, x509cert.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + } + /* Returns the current write offset */ template static void res_getWriteOffset(const FunctionCallbackInfo &args) { @@ -466,6 +478,10 @@ struct HttpResponseWrapper { } } + if constexpr (SSL == 1) { + resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "getX509Certificate", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, res_getX509Certificate)); + } + /* Create our template */ Local resObjectLocal = resTemplateLocal->GetFunction(isolate->GetCurrentContext()).ToLocalChecked()->NewInstance(isolate->GetCurrentContext()).ToLocalChecked(); diff --git a/src/Utilities.h b/src/Utilities.h index a424beb2..3713f602 100644 --- a/src/Utilities.h +++ b/src/Utilities.h @@ -18,6 +18,8 @@ #ifndef ADDON_UTILITIES_H #define ADDON_UTILITIES_H +#include +#include #include using namespace v8; @@ -169,4 +171,35 @@ class NativeString { } }; +// Utility function to extract raw certificate data +std::string extractX509PemCertificate(SSL* ssl) { + std::string pemCertificate; + + if (!ssl) { + return pemCertificate; + } + + // Get the peer certificate + X509* peerCertificate = SSL_get_peer_certificate(ssl); + if (!peerCertificate) { + // No peer certificate available + return pemCertificate; + } + + // Convert X509 certificate to PEM format + BIO* bio = BIO_new(BIO_s_mem()); + if(bio) { + if (PEM_write_bio_X509(bio, peerCertificate)) { + char* buffer; + long length = BIO_get_mem_data(bio, &buffer); + pemCertificate.assign(buffer, length); + } + BIO_free(bio); + } + + // Free the peer certificate + X509_free(peerCertificate); + return pemCertificate; +} + #endif