Skip to content

Commit

Permalink
feat: create async version of API
Browse files Browse the repository at this point in the history
  • Loading branch information
addaleax committed Aug 8, 2024
1 parent 234200b commit aeea37d
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 58 deletions.
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ declare function exportCertificateAndPrivateKey(input: LookupOptions): PfxResult

declare namespace exportCertificateAndPrivateKey {
function exportCertificateAndPrivateKey(input: LookupOptions): PfxResult;
function exportCertificateAndPrivateKeyAsync(input: LookupOptions): Promise<PfxResult>;

function exportSystemCertificates(input: StoreOptions): string[];
function exportSystemCertificatesAsync(input: StoreOptions): Promise<string[]>;
}

export = exportCertificateAndPrivateKey;
104 changes: 82 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
'use strict';
const {
exportCertificateAndKey,
exportCertificateAndKeyAsync,
exportAllCertificates,
exportAllCertificatesAsync,
storeTypes
} = require('bindings')('win_export_cert');
const { randomBytes, X509Certificate } = require('crypto');
const util = require('util');
const { promisify } = util;

const DEFAULT_STORE_TYPE_LIST = ['CERT_SYSTEM_STORE_LOCAL_MACHINE', 'CERT_SYSTEM_STORE_CURRENT_USER'];

Expand All @@ -19,24 +22,56 @@ function validateStoreTypeList(storeTypeList) {
return storeTypeList.map(st => typeof st === 'number' ? st : storeTypes[st]);
}

function exportSystemCertificates(opts = {}) {
let {
store,
storeTypeList
} = opts;
function addExportedCertificatesToSet(set, list) {
for (const cert of list) {
// X509Certificate was added in Node.js 15 and accepts DER as input, but .toString() returns PEM
set.add(new X509Certificate(cert).toString());
}
}

function exportSystemCertificates({
store,
storeTypeList
} = {}) {
storeTypeList = validateStoreTypeList(storeTypeList);

const result = new Set();
for (const storeType of storeTypeList) {
for (const cert of exportAllCertificates(store || 'ROOT', storeType)) {
// X509Certificate was added in Node.js 15 and accepts DER as input, but .toString() returns PEM
result.add(new X509Certificate(cert).toString());
}
addExportedCertificatesToSet(result, exportAllCertificates(store || 'ROOT', storeType));
}

return [...result];
}

async function exportSystemCertificatesAsync({
store,
storeTypeList
} = {}) {
storeTypeList = validateStoreTypeList(storeTypeList);

const result = new Set();
for (const storeType of storeTypeList) {
addExportedCertificatesToSet(result, await promisify(exportAllCertificatesAsync)(store || 'ROOT', storeType));
}

return [...result];
}

function validateSubjectAndThumbprint(subject, thumbprint) {
if (!subject && !thumbprint) {
throw new Error('Need to specify either `subject` or `thumbprint`');
}
if (subject && thumbprint) {
throw new Error('Cannot specify both `subject` and `thumbprint`');
}
if (subject && typeof subject !== 'string') {
throw new Error('`subject` needs to be a string');
}
if (thumbprint && !util.types.isUint8Array(thumbprint)) {
throw new Error('`thumbprint` needs to be a Uint8Array');
}
}

function exportCertificateAndPrivateKey(opts = {}) {
let {
subject,
Expand All @@ -59,18 +94,7 @@ function exportCertificateAndPrivateKey(opts = {}) {
throw err;
}

if (!subject && !thumbprint) {
throw new Error('Need to specify either `subject` or `thumbprint`');
}
if (subject && thumbprint) {
throw new Error('Cannot specify both `subject` and `thumbprint`');
}
if (subject && typeof subject !== 'string') {
throw new Error('`subject` needs to be a string');
}
if (thumbprint && !util.types.isUint8Array(thumbprint)) {
throw new Error('`thumbprint` needs to be a Uint8Array');
}
validateSubjectAndThumbprint(subject, thumbprint);
requirePrivKey = requirePrivKey !== false;
const passphrase = randomBytes(12).toString('hex');
const pfx = exportCertificateAndKey(
Expand All @@ -82,6 +106,42 @@ function exportCertificateAndPrivateKey(opts = {}) {
return { passphrase, pfx };
}

async function exportCertificateAndPrivateKeyAsync(opts = {}) {
let {
subject,
thumbprint,
store,
storeTypeList,
requirePrivKey
} = opts;
storeTypeList = validateStoreTypeList(storeTypeList);

if (storeTypeList.length !== 1) {
let err;
for (const storeType of storeTypeList) {
try {
return await exportCertificateAndPrivateKeyAsync({ ...opts, storeTypeList: [storeType] });
} catch(err_) {
err = err_;
}
}
throw err;
}

validateSubjectAndThumbprint(subject, thumbprint);
requirePrivKey = requirePrivKey !== false;
const passphrase = (await promisify(randomBytes)(12)).toString('hex');
const pfx = await promisify(exportCertificateAndKeyAsync)(
passphrase,
store || 'MY',
storeTypeList[0],
subject ? { subject } : { thumbprint },
requirePrivKey);
return { passphrase, pfx };
}

module.exports = exportCertificateAndPrivateKey;
module.exports.exportCertificateAndPrivateKey = exportCertificateAndPrivateKey;
module.exports.exportSystemCertificates = exportSystemCertificates;
module.exports.exportCertificateAndPrivateKeyAsync = exportCertificateAndPrivateKeyAsync;
module.exports.exportSystemCertificates = exportSystemCertificates;
module.exports.exportSystemCertificatesAsync = exportSystemCertificatesAsync;
83 changes: 80 additions & 3 deletions src/binding.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "certs.h"
#include <napi.h>

#define PACKAGE "win-export-certificate-and-key"

namespace {
using namespace Napi;
using namespace WinExportCertificateAndKey;
Expand Down Expand Up @@ -36,9 +38,7 @@ Value ExportAllCertificatesSync(const CallbackInfo& args) {
}
}

// Export a given certificate from a system certificate store,
// identified either by its thumbprint or its subject line.
Value ExportCertificateAndKeySync(const CallbackInfo& args) {
ExportCertificateAndKeyArgs GatherExportCertificateAndKeyArgs(const CallbackInfo& args) {
ExportCertificateAndKeyArgs exp_args;
exp_args.password_buf = MultiByteToWideChar(args[0].ToString());
exp_args.sys_store_name = MultiByteToWideChar(args[1].ToString());
Expand All @@ -56,6 +56,13 @@ Value ExportCertificateAndKeySync(const CallbackInfo& args) {
} else {
throw Error::New(args.Env(), "Need to specify either `thumbprint` or `subject`");
}
return exp_args;
}

// Export a given certificate from a system certificate store,
// identified either by its thumbprint or its subject line.
Value ExportCertificateAndKeySync(const CallbackInfo& args) {
ExportCertificateAndKeyArgs exp_args = GatherExportCertificateAndKeyArgs(args);
try {
auto result = ExportCertificateAndKey(exp_args);
return Buffer<BYTE>::Copy(args.Env(), result.data(), result.size());
Expand All @@ -64,11 +71,81 @@ Value ExportCertificateAndKeySync(const CallbackInfo& args) {
}
}

Value ExportAllCertificatesAsync(const CallbackInfo& args) {
class Worker final : public AsyncWorker {
public:
Worker(Function callback, std::wstring&& sys_store_name, DWORD store_type)
: AsyncWorker(callback, PACKAGE ":ExportAllCertificates"),
sys_store_name(std::move(sys_store_name)), store_type(store_type) {}
~Worker() {}

void Execute() override {
results = ExportAllCertificates(sys_store_name, store_type);
}

void OnOK() override {
try {
Callback().Call({Env().Null(), BufferListToArray(Env(), results)});
} catch (const std::exception& e) {
throw Error::New(Env(), e.what());
}
}

private:
std::vector<std::vector<BYTE>> results;
std::wstring sys_store_name;
DWORD store_type;
};

Worker* worker = new Worker(
args[2].As<Function>(),
MultiByteToWideChar(args[0].ToString()),
args[1].ToNumber().Uint32Value());
worker->Queue();
return args.Env().Undefined();
}

Value ExportCertificateAndKeyAsync(const CallbackInfo& args) {
ExportCertificateAndKeyArgs exp_args = GatherExportCertificateAndKeyArgs(args);

class Worker final : public AsyncWorker {
public:
Worker(Function callback, ExportCertificateAndKeyArgs&& exp_args)
: AsyncWorker(callback, PACKAGE ":ExportCertificateAndKey"),
exp_args(std::move(exp_args)) {}
~Worker() {}

void Execute() override {
result = ExportCertificateAndKey(exp_args);
}

void OnOK() override {
try {
Callback().Call({Env().Null(), Buffer<BYTE>::Copy(Env(), result.data(), result.size())});
} catch (const std::exception& e) {
throw Error::New(Env(), e.what());
}
}

private:
std::vector<BYTE> result;
ExportCertificateAndKeyArgs exp_args;
};

Worker* worker = new Worker(
args[5].As<Function>(),
std::move(exp_args));
worker->Queue();
return args.Env().Undefined();
}

}

static Object InitWinExportCertAndKey(Env env, Object exports) {
exports["exportCertificateAndKey"] = Function::New(env, ExportCertificateAndKeySync);
exports["exportAllCertificates"] = Function::New(env, ExportAllCertificatesSync);
exports["exportCertificateAndKeyAsync"] = Function::New(env, ExportCertificateAndKeyAsync);
exports["exportAllCertificatesAsync"] = Function::New(env, ExportAllCertificatesAsync);
Object storeTypes = Object::New(env);
storeTypes["CERT_SYSTEM_STORE_CURRENT_SERVICE"] = Number::New(env, CERT_SYSTEM_STORE_CURRENT_SERVICE);
storeTypes["CERT_SYSTEM_STORE_CURRENT_USER"] = Number::New(env, CERT_SYSTEM_STORE_CURRENT_USER);
Expand Down
84 changes: 51 additions & 33 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ const fs = require('fs');
const assert = require('assert');
const {
exportCertificateAndPrivateKey,
exportSystemCertificates
exportCertificateAndPrivateKeyAsync,
exportSystemCertificates,
exportSystemCertificatesAsync,
} = require('./');

describe('exportCertificateAndPrivateKey', () => {
Expand Down Expand Up @@ -39,43 +41,59 @@ describe('exportCertificateAndPrivateKey', () => {
tlsServer.close();
});

it('throws when no cert can be found', () => {
assert.throws(() => {
exportCertificateAndPrivateKey({ subject: 'Banana Corp '});
}, /CertFindCertificateInStore\(\) failed with: Cannot find object or property. \(0x80092004\)/);
});
for (const method of ['sync', 'async']) {
const fn = {
sync: exportCertificateAndPrivateKey,
async: exportCertificateAndPrivateKeyAsync
}[method];
context(method, () => {
it('throws when no cert can be found', async() => {
await assert.rejects(async() => {
await fn({ subject: 'Banana Corp '});
}, /CertFindCertificateInStore\(\) failed with: Cannot find object or property. \(0x80092004\)/);
});

it('loads a certificate based on its thumbprint', async() => {
const { passphrase, pfx } = exportCertificateAndPrivateKey({
thumbprint: Buffer.from('d755afda2bbad2509d39eca5968553b9103305af', 'hex')
});
tls.connect({ ...tlsServerConnectOptions, passphrase, pfx });
assert.strictEqual(await authorized, true);
});
it('loads a certificate based on its thumbprint', async() => {
const { passphrase, pfx } = await fn({
thumbprint: Buffer.from('d755afda2bbad2509d39eca5968553b9103305af', 'hex')
});
tls.connect({ ...tlsServerConnectOptions, passphrase, pfx });
assert.strictEqual(await authorized, true);
});

it('loads a certificate based on its subject', async() => {
const { passphrase, pfx } = exportCertificateAndPrivateKey({
subject: 'Internet Widgits Pty Ltd'
it('loads a certificate based on its subject', async() => {
const { passphrase, pfx } = await fn({
subject: 'Internet Widgits Pty Ltd'
});
tls.connect({ ...tlsServerConnectOptions, passphrase, pfx });
assert.strictEqual(await authorized, true);
});
});
tls.connect({ ...tlsServerConnectOptions, passphrase, pfx });
assert.strictEqual(await authorized, true);
});
}
});

describe('exportSystemCertificates', () => {
it('exports certificates from the ROOT store as .pem', () => {
const certs = exportSystemCertificates({ store: 'ROOT' });
assert.notStrictEqual(certs.length, 0);
for (const cert of certs) {
assert.match(cert.trim(), /^-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----$/);
}
});
for (const method of ['sync', 'async']) {
const fn = {
sync: exportSystemCertificates,
async: exportSystemCertificatesAsync
}[method];
context(method, () => {
it('exports certificates from the ROOT store as .pem', async() => {
const certs = await fn({ store: 'ROOT' });
assert.notStrictEqual(certs.length, 0);
for (const cert of certs) {
assert.match(cert.trim(), /^-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----$/);
}
});

it('exports certificates from the CA store as .pem', () => {
const certs = exportSystemCertificates({ store: 'CA' });
assert.notStrictEqual(certs.length, 0);
for (const cert of certs) {
assert.match(cert.trim(), /^-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----$/);
}
});
it('exports certificates from the CA store as .pem', async() => {
const certs = await fn({ store: 'CA' });
assert.notStrictEqual(certs.length, 0);
for (const cert of certs) {
assert.match(cert.trim(), /^-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----$/);
}
});
});
}
});

0 comments on commit aeea37d

Please sign in to comment.