Skip to content

Commit

Permalink
feat: add support for enumerating all certificates COMPASS-4105
Browse files Browse the repository at this point in the history
This is conceptually very similar to the functionality
exposed by https://github.com/ukoloff/win-ca, but this
variant doesn't crash, leak memory, break when used by
multiple threads, etc.

Since this package already accesses the Windows system
CA stores, it feels like a natural place to add this here.
  • Loading branch information
addaleax committed Feb 15, 2022
1 parent 11261e0 commit c94d0a4
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 82 deletions.
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
# win-export-certificate-and-key

Export a certificate and its corresponding private key from the Windows CA store.
Access the Windows system certificate and key store.
This module is a native addon. It will only successfully work on Windows.
No prebuilt binaries are currently provided.

This module returns a single certificate (and by default its private key)
## API

### `exportCertificateAndPrivateKey(opts?)`

Export a certificate and its corresponding private key from the Windows CA store.

Valid options are:

- `subject`: Subject line of the certificate/key as a string.
- `thumbprint`: Thumbprint of the certificate/key as a Uint8Array.
Either `subject` or `thumbprint` must be provided.
- `store`: Optional Windows certificate store name. Default: `MY`.
- `storeTypeList`: Optional array of Windows certificate store types
to try. Default: `['CERT_SYSTEM_STORE_LOCAL_MACHINE', 'CERT_SYSTEM_STORE_CURRENT_USER']`.
- `requirePrivKey`: Boolean. If set, the
`REPORT_NO_PRIVATE_KEY | REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY` flags will
be passed to the wincrypt API.

This function returns a single certificate (and by default its private key)
combination as a .pfx file, along with a random passphrase that has been
used for encrypting the file.
It will throw an exception if no relevant certificate could be found.
The certificate in question can be specified either through its subject line
string or its thumbprint.

### `exportSystemCertificates(opts?)`

Export all system certificates (no private keys) matching the given
options.

Valid options are:

- `store`: Optional Windows certificate store name. Default: `ROOT`.
- `storeTypeList`: Optional array of Windows certificate store types
to try. Default: `['CERT_SYSTEM_STORE_LOCAL_MACHINE', 'CERT_SYSTEM_STORE_CURRENT_USER']`.

This functions returns an array of PEM-formatted certificates.
(Note that this can be a fairly slow operation.)

## Testing

You need to import `testkeys\certificate.pfx` manually into your local
You need to import `testkeys\certificate.pfx` manually into your local
CA store in order for the tests to pass. Make sure to import that certificate
with the "exportable private key" option. The password for the file is `pass`.
39 changes: 35 additions & 4 deletions binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,36 @@ Buffer<BYTE> CertToBuffer(Env env, PCCERT_CONTEXT cert, LPCWSTR password, DWORD
class CertStoreHandle {
public:
CertStoreHandle(HCERTSTORE store) : store_(store) {}
~CertStoreHandle() { CertCloseStore(store_, 0); }
~CertStoreHandle() {
CertCloseStore(store_, 0);
if (current_cert_) {
CertFreeCertificateContext(current_cert_);
}
}
HCERTSTORE get() const { return store_; }
operator boolean() const { return !!get(); }

CertStoreHandle(CertStoreHandle&& other) : store_(other.store_) { other.store_ = nullptr; }
CertStoreHandle(CertStoreHandle&& other)
: store_(other.store_), current_cert_(other.current_cert_) {
other.store_ = nullptr;
other.current_cert_ = nullptr;
}
CertStoreHandle& operator=(CertStoreHandle&& other) {
this->~CertStoreHandle();
return *new(this)CertStoreHandle(std::move(other));
}

PCCERT_CONTEXT next() {
current_cert_ = CertEnumCertificatesInStore(store_, current_cert_);
return current_cert_;
}

private:
CertStoreHandle(const CertStoreHandle&) = delete;
CertStoreHandle& operator=(const CertStoreHandle&) = delete;

HCERTSTORE store_;
PCCERT_CONTEXT current_cert_ = nullptr;
};

CertStoreHandle CertOpenStore(Env env, const std::wstring& name, DWORD type) {
Expand All @@ -112,9 +127,24 @@ CertStoreHandle CertOpenStore(Env env, const std::wstring& name, DWORD type) {
return sys_cs;
}

Value ExportAllCertificates(const CallbackInfo& args) {
std::wstring sys_store_name = MultiByteToWideChar(args[0].ToString());
DWORD store_type = args[1].ToNumber().Uint32Value();
CertStoreHandle sys_cs = CertOpenStore(args.Env(), sys_store_name, store_type);

PCCERT_CONTEXT cert;
Array result = Array::New(args.Env());
size_t index = 0;
while (cert = sys_cs.next()) {
Buffer<BYTE> buf = CertToBuffer(args.Env(), cert, L"", 0);
result[index++] = buf;
}
return result;
}

// Export a given certificate from a system certificate store,
// identified either by its thumbprint or its subject line.
Value ExportCertificate(const CallbackInfo& args) {
Value ExportCertificateAndKey(const CallbackInfo& args) {
std::wstring password_buf = MultiByteToWideChar(args[0].ToString());
LPCWSTR password = password_buf.data();
std::wstring sys_store_name = MultiByteToWideChar(args[1].ToString());
Expand Down Expand Up @@ -163,7 +193,8 @@ Value ExportCertificate(const CallbackInfo& args) {
}

static Object InitWinExportCertAndKey(Env env, Object exports) {
exports["exportCertificate"] = Function::New(env, ExportCertificate);
exports["exportCertificateAndKey"] = Function::New(env, ExportCertificateAndKey);
exports["exportAllCertificates"] = Function::New(env, ExportAllCertificates);
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
28 changes: 23 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,32 @@ declare type StoreType =
'CERT_SYSTEM_STORE_SERVICES' |
'CERT_SYSTEM_STORE_USERS';

declare function exportCertificateAndPrivateKey(input: {
subject: string;
declare interface StoreOptions {
store?: string;
storeTypeList?: Array<StoreType | number>;
};

declare type LookupOptions = StoreOptions & {
requirePrivKey?: boolean;
} & ({
subject: string;
thumbprint?: never;
} | {
subject?: never;
thumbprint: Uint8Array;
store?: string;
requirePrivKey?: boolean;
}): { passphrase: string; pfx: Uint8Array; };
});

declare interface PfxResult {
passphrase: string;
pfx: Uint8Array;
};

declare function exportCertificateAndPrivateKey(input: LookupOptions): PfxResult;

declare namespace exportCertificateAndPrivateKey {
function exportCertificateAndPrivateKey(input: LookupOptions): PfxResult;

function exportSystemCertificates(input: StoreOptions): string[];
}

export = exportCertificateAndPrivateKey;
62 changes: 52 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
const { exportCertificate, storeTypes } = require('bindings')('win_export_cert');
const {
exportCertificateAndKey,
exportAllCertificates,
storeTypes
} = require('bindings')('win_export_cert');
const { randomBytes } = require('crypto');
const forge = require('node-forge');
const util = require('util');

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

function validateStoreTypeList(storeTypeList) {
storeTypeList = storeTypeList || DEFAULT_STORE_TYPE_LIST;
if (!Array.isArray(storeTypeList) ||
storeTypeList.length < 1 ||
!storeTypeList.every(st => typeof st === 'number' || Object.keys(storeTypes).includes(st))) {
throw new Error(`storeTypeList needs to be an array of valid store types`);
}
return storeTypeList.map(st => typeof st === 'number' ? st : storeTypes[st]);
}

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

const result = new Set();
for (const storeType of storeTypeList) {
const certs = exportAllCertificates(store || 'ROOT', storeType);
for (const cert of certs) {
// Convert PKCS#12 (aka .pfx) to PEM
const asn1 = forge.asn1.fromDer(cert.toString('latin1'));
const pkcs12 = forge.pkcs12.pkcs12FromAsn1(asn1, '');
const certBags = pkcs12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag];
for (const bag of certBags) {
if (bag.cert) {
const pem = forge.pki.certificateToPem(bag.cert);
result.add(pem);
}
}
}
}

return [...result];
}

function exportCertificateAndPrivateKey(opts = {}) {
let {
subject,
Expand All @@ -10,12 +54,8 @@ function exportCertificateAndPrivateKey(opts = {}) {
storeTypeList,
requirePrivKey
} = opts;
storeTypeList = storeTypeList || ['CERT_SYSTEM_STORE_LOCAL_MACHINE', 'CERT_SYSTEM_STORE_CURRENT_USER'];
if (!Array.isArray(storeTypeList) ||
storeTypeList.length < 1 ||
!storeTypeList.every(st => typeof st === 'number' || Object.keys(storeTypes).includes(st))) {
throw new Error(`storeTypeList needs to be an array of valid store types`);
}
storeTypeList = validateStoreTypeList(storeTypeList);

if (storeTypeList.length !== 1) {
let err;
for (const storeType of storeTypeList) {
Expand All @@ -42,13 +82,15 @@ function exportCertificateAndPrivateKey(opts = {}) {
}
requirePrivKey = requirePrivKey !== false;
const passphrase = randomBytes(12).toString('hex');
const pfx = exportCertificate(
const pfx = exportCertificateAndKey(
passphrase,
store || 'MY',
typeof storeTypeList[0] === 'number' ? storeTypeList[0] : storeTypes[storeTypeList[0]],
storeTypeList[0],
subject ? { subject } : { thumbprint },
requirePrivKey);
return { passphrase, pfx };
}

module.exports = exportCertificateAndPrivateKey;
module.exports = exportCertificateAndPrivateKey;
module.exports.exportCertificateAndPrivateKey = exportCertificateAndPrivateKey;
module.exports.exportSystemCertificates = exportSystemCertificates;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"gypfile": true,
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^3.1.0"
"node-addon-api": "^3.1.0",
"node-forge": "^1.2.1"
},
"license": "Apache-2.0",
"exports": {
Expand Down
Loading

0 comments on commit c94d0a4

Please sign in to comment.