Using node.js to verify a X509 certificate with CA cert - node.js

I am looking for a node.js way to verify a client certificate in X509 format with a CA certificate which was given to me (none of those are created/managed by me, my software only has to verify what is beeing sent to it).
I have found several modules for this job, however I am having issues with each of them:
X509 is able to do it using x509.verify(cert, CABundlePath, cb), however it needs to read the certificates from FS, and I am having them in memory already. This is cumbersome as it will be done with each web request which reaches my app.
It seems like PKI.js is able to do it, however their examples don't work for me but complain about missing files, so I can't even try it out.
I tried node-forge, but while I am unsure if I use it correctly (they don't have any API documentation) its throwing a forge.pki.BadCertificate error from forge.pki.verifyCertificateChain(caStore, [ cer ], cb).
When trying pem, using a simple pem.verifySigningChain(cer, [ ca ], cb) would throw some error complaining about loading a file from /var/.... Even if it would work, I would avoid using this lib as its relying on the openssl command line tool, which I would like to avoid
Now I feel pretty stupid because I failed to get this simple task done with any of the above modules. Could someone point me to a simple solution which will allow me to verify the signature/validity of a X509 certificate using a given CA certificate? :s
[edit] Basically I would need openssl verify -verbose -CAfile ca-crt.pem client1-crt.pem in Node.js but without dependencies to the openssl command line tool and without temporarily saving the certs to disk.
[edit2] Would it be possible to just use https://nodejs.org/api/crypto.html#crypto_verify_verify_object_signature_signatureformat?

I finally managed to do it using node-forge. Heres a working code example:
let pki = require('node-forge').pki;
let fs = require('fs');
let caCert;
let caStore;
try {
caCert = fs.readFileSync('path/to/ca-cert.pem').toString();
caStore = pki.createCaStore([ caCert ]);
} catch (e) {
log.error('Failed to load CA certificate (' + e + ')');
return....;
}
try {
pki.verifyCertificateChain(caStore, [ cert ]);
} catch (e) {
return handleResponse(new Error('Failed to verify certificate (' + e.message || e + ')'));
}
Both certificates shall be given in base64 encoded PEM format/js string.
verifyCertificateChain checks the certifitate validity (notBefore/notAfter) as well as verifies the given CA chain.
I am not 100% sure if this is the best approach, or if this library is doing a good job, since their source code of verifyCertificateChain is full of #TODOs, so maybe this is not ready for production?
But at least I have a somewhat working solution. Probably it would be better to create a node module which wraps the libssl c calls, but thats just a lot of effort for this small task.

You can also do like this if you want to check the using the client certificates from the http request directly :
// retrieve certificates from the request ( in der format )
clientCert = req.connection.getPeerCertificate(true).raw.toString('base64'))
Method to convert the der certificate to pem and verify against the castore.
const caCert = fs....
const ca = pki.certificateFromPem(caCert)
const caStore = pki.createCaStore([ ca ])
const verify = (clientCert, next) => {
try {
const derKey = forge.util.decode64(clientCert)
const asnObj = forge.asn1.fromDer(derKey)
const asn1Cert = pki.certificateFromAsn1(asnObj)
const pemCert = pki.certificateToPem(asn1Cert)
const client = pki.certificateFromPem(pemCert)
return pki.verifyCertificateChain(caStore, [ client ], cb)
} catch (err) {
next(new Error(err))
}
}
I did not find a better way to verify the client "der" certificate from the request.
fas3r

This works for me:
const fs = require('fs'), pki = require('node-forge').pki
var ca = pki.certificateFromPem(fs.readFileSync('ca.pem', 'ascii'))
var client = pki.certificateFromPem(fs.readFileSync('client.pem', 'ascii'))
try {
if (!ca.verify(client)) throw 'verify failed'
} catch (err) {
console.log(err)
}
try/catch was required, because .verify threw an error (instead of returning false) in my case.

Related

openssl: ca key too small but key size is 4096

I am getting a weird error when try to upload a zip to an external API. The error is "ca key too small".
I have already verified and my p12(key + certificate) is generated with SHA256 signature and the key size is 4096 bytes.
In order to verify this, I added my p12 file to OSX keychain.
I checked the certificate it says Signature Algorithm as SHA-256 with RSA Encryption and Public key size as 4.096 bits. I checked the private key it says RSA, 4096-bit. I already tried generating a new p12 key pair but the issue persists.
I am facing this issue when deploying a zipped package through bitbucket pipelines. We recently switched our node version from 14.XX to 16.XX and because of this we started facing this issue.
Upon deeper investigation it seems that the "openssl.cnf" packaged as part of node 16.XX docker image has SECLEVEL specified with value 2. If I downgrade the node version to 14.XX and inspect the "openssl.cnf", it is missing the SECLEVEL entry altogether. So, for node image 16.XX, if I modify the SECLEVEL to 1 by manipulating the "openssl.cnf" everything works fine.
# node 16.XX docker image /etc/ssl/openssl.cnf
[system_default_sect]
MinProtocol = TLSv1.2
CipherString = DEFAULT:#SECLEVEL=2
I am also able to replicate the same issue locally by add the SECLEVEL in my local "openssl.cnf" file and then making the API call to the server. Below is the stripped down version of the nodejs code.
I do not want to use this hack of downgrading the SECLEVEL as I would like to understand what exactly the issue is. As per my understanding, my key size(4096) is not small.
var fs = require('fs');
var request = require('request');
var endpoint = 'SOME_ENDPOINT";
var instance = "SOME_INSTANCE"
var file = './test.zip';
const options = {
pfx: '/path/to/certificate.p12',
passphrase: 'SOME_PASSPHRASE',
}
var token = 'SOME_TOKEN';
var opts = {
baseUrl: 'https://' + instance,
uri: endpoint,
auth: {
bearer: ( token ? token : null )
},
strictSSL: true,
method: 'PUT'
};
if (options && options.pfx && fs.existsSync(options.pfx)) {
var stat = fs.statSync(options.pfx);
if (stat.isFile()) {
opts.agentOptions = {
pfx: fs.readFileSync(options.pfx),
passphrase: options.passphrase // as passphrase is optional, it can be undefined here
};
}
}
var req = request(opts, function(err) {
console.log(err);
});
fs.createReadStream(file).pipe(req);

Trying to symmetrically encrypt a value for storage in the client (httpOnly cookie) and having an issue decrypting

I am trying to encrypt a value on my server with a private key to store it on the client within an httpOnly cookie.
I am having trouble with the encryption/decryption lifecycle
function encrypt(input) {
const encryptedData = crypto.privateEncrypt(
privateKey,
Buffer.from(input)
)
return encryptedData.toString('base64')
}
function decrypt(input) {
const decryptedData = crypto.privateDecrypt(
{ key: privateKey },
Buffer.from(input, 'base64'),
)
return decryptedData.toString()
}
const enc = encrypt('something moderately secret')
const dec = decrypt(enc)
console.log(dec) // 'something moderately secret'
However the crypto.privateDecrypt function is throwing with
Error: error:04099079:rsa routines:RSA_padding_check_PKCS1_OAEP_mgf1:oaep decoding error
Side question, is it safe to reuse the same private key the server uses to sign JWTs. It's an rsa key generated using ssh-keygen -t rsa -b 4096 -m PEM -f RS256.key
So, you don't use crypto.privateEncrypt() with crypto.privateDecrypt(). That's not how they work. Those functions are for asymmetric encryption, not for symmetric encryption. You use either of these two pairs:
crypto.publicEncrypt() ==> crypto.privateDescrypt()
crypto.privateEncrypt() ==> crypto.publicDecrypt()
So, that's why you're getting the error you're getting. The nodejs doc for crypto.privateDecript() says this:
Decrypts buffer with privateKey. buffer was previously encrypted using the corresponding public key, for example using crypto.publicEncrypt().
If what you really want is symmetric encryption, there are a bunch of options in the crypto module for that. There are some examples shown here: https://www.section.io/engineering-education/data-encryption-and-decryption-in-node-js-using-crypto/ and https://fireship.io/lessons/node-crypto-examples/#symmetric-encryption-in-nodejs.

PDFNet Digital Signature in Node JS using Google KMS

I've seen example of signing https://www.pdftron.com/documentation/nodejs/guides/features/signature/sign-pdf
signOnNextSave uses PKCS #12 certificate, but I use Google KMS for asymmetric signing to keep private keys safe.
Here is example of signing and verifying by Google Cloud KMS
I tried to implement custom SignatureHandler but Node.JS API is different from Java or .NET
https://www.pdftron.com/api/pdfnet-node/PDFNet.SignatureHandler.html
How can I implement custom signing and verifying logic?
const data = Buffer.from('pdf data')
// We have 2048 Bit RSA - PSS Padding - SHA256 Digest key in Google Cloud KMS
const signAsymmetric = async () => {
const hash = crypto.createHash('sha256')
hash.update(data)
const digest = hash.digest()
const digestCrc32c = crc32c.calculate(digest)
// Sign the data with Cloud KMS
const [signResponse] = await client.asymmetricSign({
name: locationName,
digest: {
sha256: digest
},
digestCrc32c: {
value: digestCrc32c
}
})
if (signResponse.name !== locationName) {
throw new Error('AsymmetricSign: request corrupted in-transit')
}
if (!signResponse.verifiedDigestCrc32c) {
throw new Error('AsymmetricSign: request corrupted in-transit')
}
if (
crc32c.calculate(signResponse.signature) !==
Number(signResponse.signatureCrc32c.value)
) {
throw new Error('AsymmetricSign: response corrupted in-transit')
}
// Returns signature which is buffer
const encoded = signResponse.signature.toString('base64')
console.log(`Signature: ${encoded}`)
return signResponse.signature
}
// Verify data with public key
const verifyAsymmetricSignatureRsa = async () => {
const signatureBuffer = await signAsymmetric()
const publicKeyPem = await getPublicKey()
const verify = crypto.createVerify('sha256')
verify.update(data)
verify.end()
const key = {
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING
}
// Verify the signature using the public key
const verified = verify.verify(key, signatureBuffer)
return verified
}
At this time, the PDFTron SDK only supports custom handlers on C++, Java, and C# (there are more plans to include additional languages in the future).
On a different platform like C++, you would extend the custom handler functions by putting hash.update(data) into SignatureHandler::AppendData, and the rest of signAsymmetric would go into SignatureHandler::CreateSignature. A name would be given to the custom handler for interoperability like Adobe.PPKLite (we do not yet support custom handler SubFilter entries, only Filter -- see PDF standard for the difference -- but this won't matter so long as you use a verification tool that supports Filter Adobe.PPKLite). Please see the following link for a concrete example:
https://www.pdftron.com/documentation/samples/cpp/DigitalSignaturesTest
As for verification, our code can already do this for you if your signatures fulfill the following conditions:
they use a standard digest algorithm
they use RSA to sign
they use the correct data formats according to the PDF standard (i.e. detached CMS, digital signature dictionary)
If you have more questions or require more details, please feel free to reach out to PDFTron support at support#pdftron.com

Using a Microsoft Windows Certificate Store in node.js

I am trying to create a node https server. The idea is that the server will receive the thumbprint of a certificate in the store, pull it out of the store and use it for SSL/TLS connection. I have done examples for self-signed certificates and it has worked fine.
I have the code to look through the certificate store and find the correct certificate based on its thumbprint
The code I have so far is below.
const ca = require('win-ca');
const forge = require('node-forge');
const pki = forge.pki;
const ssh = forge.ssh;
const thumbprint = process.env.THUMBPRINT;
...
for (let cert of ca.all()) {
console.log(forge.pki.certificateToPem(cert));
console.log(' ----------- \r');
const md = forge.md.sha1.create();
md.update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes());
const hex = md.digest().toHex();
console.log(hex);
console.log('\r');
if (thumbprint === hex) {
foundCert = cert;
console.log('found\r');
break;
}
}
console.log('**********************');
console.log('\r');
//}
cnt++;
}
if (foundCert) {
console.log(forge.pki.certificateToPem(foundCert));
const pm = forge.pki.certificateToPem(foundCert);
console.log(foundCert);
}
In windows ASP.NET Core, one only needs the the certificate for https. For node.js, all the examples use a private key. I don't have the private key, but I have access to the machine's certificate store. Can I use the certificate from the store without the private key? There seems to be a piece I am missing.
Thanks

APN-Node: Error when loading PEM file

I am trying to get apn-node to push to my devices. The server is hosted on Heroku, so I do not want to commit the file. Also, I do not want to fetch it from a remote server but instead put it in an environment variable.
I already tried the following (source):
I created and downloaded the certificate from Apple and now have it in my Keychain. I exported it as a *.p12 file and converted it with openssl pkcs12 -in dev.p12 -out dev.pem -nodes into a *.pem file.
To set the environment variable, I did export APN_CERT="$(cat dev.pem)". When I print it out in my application it shows the certificate perfectly fine.
However, when I actually send a notification (and node-apn opens the connection) it throws an [Error: wrong tag].
This error is emitted by the crypto module:
apn Raising error: +4ms [Error: wrong tag] undefined undefined
apn Error occurred with trace: +1ms Error: wrong tag
at Object.exports.createCredentials (crypto.js:176:17)
at Object.exports.connect (tls.js:1344:27)
at apnSocketLegacy
The module also throws a APN transmission error: moduleInitialisationFailed (Code: 513).
I was unable to find any useful information other than that this could be related to the crypto module itself of node itself. That's why I suspect I did something wrong when creating the certificate but thankful for any guiding advice.
I found this guide for apns-sharp which actually described how to generate a valid .p12 file.
Still, writing it into an environment variable did not work. My code for reading it is: new Buffer(certString, 'binary') but I thought it still was not supplied in a correct format.
The solution for me was to actually read the buffer directly from a file via fs.readFileSync.
To get the env variable to work you could encode the file via cat cert.p12 | base64 and load it as such with new Buffer(certString, 'base64'). This finally worked for me.
The preference here would be to use an encrypted p12 stored alongside the applciation and specify a passphrase which you set via an environment variable.
I was unable to replicate your problem using the following script
var apn = require("apn");
var token = "<token>; // iPad
var service = new apn.connection({
cert: process.env.APN_CERT, key: process.env.APN_KEY
});
service.on("connected", function() {
console.log("Connected");
});
service.on("error", function(err) {
console.log("Standard error", err);
});
function pushNotification() {
var note = new apn.notification().setAlertText("Hello");
service.pushNotification(note, token);
service.shutdown();
}
pushNotification();
Run with:
$ export APN_CERT=$(cat certificates/cert.pem)
$ export APN_KEY=$(cat certificates/key.pem)
$ node apn-env.js
The error you are seeing "wrong tag" is from OpenSSL and suggests a parsing error of the data contained within the certificate data itself rather than the data being loaded incorrectly from the env. Loading PEM files from environment variables works correctly
var apn = require("apn");
var deviceToken = "device token";
var service = new apn.Provider({
cert: '.path to /cert.pem', key:'pat to ./key.pem'
});
var note = new apn.Notification();
note.expiry = Math.floor(Date.now() / 1000) + 60; // Expires 1 minute from now.
note.badge = 3;
note.sound = "ping.aiff";
note.alert = " You have a new message";
note.payload = {'messageFrom': 'Rahul test apn'};
note.topic = "(Bundle_id).voip";
note.priority = 10;
note.pushType = "alert";
service.send(note, deviceToken).then( (err,result) => {
if(err) return console.log(JSON.stringify(err));
return console.log(JSON.stringify(result))
});
Load the pem files and run with the tokens

Resources