NodeJS cross-platform module to code-sign windows executables with Authenticode signatures.
authenticode-sign
is a simple NodeJS module written in TypeScript that can be used to create, nest and replace authenticode signatures for Windows Portable Executable files (.exe). It can be used to programatically sign code with your own crypto tools. As far as my testing goes, this one is the only pure javascript module that creates working authenticode signatures and can use your own signing tools.
I took a lot of inspiration from the Jsign, osslsigncode and resedit projects.
The library currently only supports PE files (EXE, SYS, DLL, etc), but in the future I would like to extend it for MSI, CAB and CAT files as well.
The library is bundled as an ES Module. After installing it with npm install authenticode-sign
you have to create a SignerObject
which will handle the actual crypto operations (hashing, signing and sending the timestamping request).
The SignerObject
has to implement this interface:
interface SignerObject {
getDigestAlgorithmOid: () => OID;
getSignatureAlgorithmOid: () => OID;
getCertificate: () => Buffer;
getIntermediateCertificates?: () => Buffer[];
digest: DigestFn;
sign: SignFn;
timestamp?: TimestampFn;
}
Where:
getDigestAlgorithmOid()
returns the Object ID of the digest algorithm. For example2.16.840.1.101.3.4.2.1
for SHA256. You can get the list of hash algorithm OIDs from oidref hashAlgsgetSignatureAlgorithmOid()
return the Object ID of the signature algorithm. For example1.2.840.10045.4.3.2
for SHA256Ecdsa. For these you don't have a nice list like the one for the hashing algorithms, but you can still use oidref.com to find the required OIDsgetCertificate()
returns the X.509 certificate in a DER encoded (binary) format as a NodeJS buffergetIntermediateCertificates()
can optionally return an array of DER encoded X.509 certificates that will be included in the certificate chain of the signature. It is recommended to include the intermediate certificates and even the root certificate in the chain. Omitting the intermediate certificate might cause the validation to faildigest(data: Iterator<Buffer>)
hashes the supplied data and returns it as a buffer (can be async)sign(data: Iterator<Buffer>)
signes the supplied data and returns it as a buffer (can be async)timestamp(data: Buffer)
sends the timestamp request to your TSA and returns the response as a buffer (can be async). This method is optional, if you don't implement it, the signature will not be timestamped.
You can see the whole example (including test files) in the test directory.
import fsp from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { AuthenticodeSigner, PEFile } from 'authenticode-sign';
// we will use nodeJS's crypto module for the crypto operations
// but you can use any crypto engine that supports hashing and signing
import crypto from 'crypto';
const main = async () => {
// create work directory
const dir = path.dirname(fileURLToPath(import.meta.url));
const workdir = path.join(dir, 'work')
await fsp.mkdir(path.join(dir, 'work'), {recursive: true});
// read the EXE file to a Buffer
const file = await fsp.readFile(path.join(dir, 'test.exe'));
// create the PEFile that will be signed using that buffer
const exe = new PEFile(file);
// you can output the checksum if you would like
console.log('Checksum:', exe.calculateChecksum().toString(16));
// read the certificate already encoded in DER format
const certDer = await fsp.readFile(path.join(dir, 'signer.cer'))
// read the intermediate certificate already encoded in DER format
const intermediateCertDer = await fsp.readFile(path.join(dir, 'intermediate.cer'))
// read the private key encoded in PEM format
const key = (await fsp.readFile(path.join(dir, 'signer.key'))).toString('utf8')
// create the AuthenticodeSigner
const signer = new AuthenticodeSigner({
getDigestAlgorithmOid: () => '2.16.840.1.101.3.4.2.1', // return OID for sha256
getSignatureAlgorithmOid: () => '1.2.840.10045.4.3.2', // return OID for ecdsa with sha256
getCertificate: () => certDer, // return the binary certificate
getIntermediateCertificates: () => [intermediateCertDer], // return the intermediate certificates
digest: async (dataIterator) => {
// create a SHA256 hash using NodeJS Crypto module
const hash = crypto.createHash('sha256')
// consume the whole iterator
while (true) {
const it = dataIterator.next();
if(it.done){
break;
}
// update the hash with the current value
await hash.update(it.value)
}
// return the digest in binary format as a buffer
return hash.digest()
},
sign: async (dataIterator) => {
// create a signature using SHA256 digest
const signature = crypto.createSign('sha256')
// consume the whole iterator
while (true) {
const it = dataIterator.next();
if(it.done) {
break;
}
// update the signature with the current value
await signature.update(it.value)
}
// sign it with your private key and return it as a buffer
return signature.sign(key)
},
timestamp: async data => {
// send the timestamp request to one of the public timestamping servers
// see the list of free TSAs: https://gist.github.com/Manouchehri/fd754e402d98430243455713efada710
const resp = await fetch('http://timestamp.digicert.com', {
method: 'POST',
headers: {
'Content-type': 'application/timestamp-query',
'Content-length': data.byteLength.toString()
},
body: data
});
return Buffer.from(await resp.arrayBuffer());
}
})
// do the actual signing of the executable
// this method will return the signed executable as a buffer
const result = await signer.sign(exe);
console.log('Saving result file...')
// save the signed file
await fsp.writeFile(path.join(workdir, 'test.signed.exe'), result);
console.log('Done')
}
main();
You can pass additional options to the signer.sing()
function:
replace
(boolean): if true, replace the existing signature with the newly created one. It can be safely set even if no existing signature is presentnest
(boolean): if true, nest the newly created signature into the existing one. It will throw an error if no existing signature is present
If the file already has a signature, you MUST specify either replace
or nest
.
To maximize compatibility, you should sign your exectuable using SHA1 (for legacy systems) and SHA256 digests as well. You can use signature nesting to achieve this. The first signature should be SHA1 and the nested one should be SHA256.
This basically means that you're going to sign the executable two times and the second time you specify the {nest: true}
option.
Note: You MUST timestamp the SHA1 signatures, otherwise Windows will not accept them.
Example usage of nesting:
// refer to the simple example for the rest
const sha1Signer = new AuthenticodeSigner({
getDigestAlgorithmOid: () => '1.3.14.3.2.26', // SHA1
getSignatureAlgorithmOid: () => '1.2.840.10045.4.1', // ecdsa with SHA1
getCertificate: () => certDer,
digest: async (dataIterator) => {
const hash = crypto.createHash('sha1')
while (true) {
const it = dataIterator.next();
if(it.done){
break;
}
await hash.update(it.value)
}
return hash.digest()
},
sign: async (dataIterator) => {
const signature = crypto.createSign('sha1')
while (true) {
const it = dataIterator.next();
if(it.done) {
break;
}
await signature.update(it.value)
}
return signature.sign(key)
},
timestamp: async data => {
const resp = await fetch('http://timestamp.digicert.com', {
method: 'POST',
headers: {
'Content-type': 'application/timestamp-query',
'Content-length': data.byteLength.toString()
},
body: data
});
return Buffer.from(await resp.arrayBuffer());
}
});
const sha256Signer = new AuthenticodeSigner({
getDigestAlgorithmOid: () => '2.16.840.1.101.3.4.2.1', // SHA256
getSignatureAlgorithmOid: () => '1.2.840.10045.4.3.2', // ecdsa with SHA256
getCertificate: () => certDer,
digest: async (dataIterator) => {
const hash = crypto.createHash('sha256')
while (true) {
const it = dataIterator.next();
if(it.done){
break;
}
await hash.update(it.value)
}
return hash.digest()
},
sign: async (dataIterator) => {
const signature = crypto.createSign('sha256')
while (true) {
const it = dataIterator.next();
if(it.done) {
break;
}
await signature.update(it.value)
}
return signature.sign(key)
},
timestamp: async data => {
const resp = await fetch('http://timestamp.digicert.com', {
method: 'POST',
headers: {
'Content-type': 'application/timestamp-query',
'Content-length': data.byteLength.toString()
},
body: data
});
return Buffer.from(await resp.arrayBuffer());
}
});
const exe = new PEFile(file);
const sha1SignedExeFile = sha1Signer.sign(exe);
const sha1SignedExe = new PEFile(sha1SignedExeFile);
const result = sha256Signer.sign(sha1SignedExe, { nest: true });
// result now contains the double-signed exe that can be writte to disk
You can use npm test
and it will sign an empty EXE with a pregenerated ECDSA256 key and SHA256 hashing algorithm. The result is saved to test/work/test.signed.exe
. To verify the signature it's the easiest to use Windows's built in signature verification tool (right click -> properties -> digital signatures), but the next best thing is to use the osslsigncode verify -CAfile test/ca.crt -verbose test/work/test.signed.exe
command.
If you would like to sign your executables for Windows from any OS you can check out my other open source project: Signo. It's still in a development stage, but that tool can be used to sign executables using any PKCS#11 hardware (or software) token from anywhere, which is pretty useful, since all newly created codesigning certificates have to be stored on a HSM. With Signo and a cheap Yubikey FIPS token you can get some benefits of a networked HSM while not breaking the bank.