PDFNet Digital Signature in Node JS using Google KMS - node.js

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

Related

Implementing JWE encryption for a JWS signed token in Node.JS with Jose 4.11

I have difficulty manipulating the Jose Node.JS documentation to chain the creation of a JWS and JWE. I cannot find the proper constructor for encryption. It looks like I can only encrypt a basic payload not a signed JWS.
Here is the code sample I try to fix to get something that would look like
const jws = await createJWS("myUserId");
const jwe = await encryptAsJWE(jws);
with the following methods
export const createJWS = async (userId) => {
const payload = {
}
payload['urn:userId'] = userId
// importing key from base64 encrypted secret key for signing...
const secretPkcs8Base64 = process.env.SMART_PRIVATE_KEY
const key = new NodeRSA()
key.importKey(Buffer.from(secretPkcs8Base64, 'base64'), 'pkcs8-private-der')
const privateKey = key.exportKey('pkcs8')
const ecPrivateKey = await jose.importPKCS8(privateKey, 'ES256')
const assertion = await new jose.SignJWT(payload)
.setProtectedHeader({ alg: 'RS256' })
.setIssuer('demolive')
.setExpirationTime('5m')
.sign(ecPrivateKey)
return assertion
}
export const encryptAsJWE = async (jws) => {
// importing key similar to createJWS key import
const idzPublicKey = process.env.IDZ_PUBLIC_KEY //my public key for encryption
...
const pkcs8PublicKey = await jose.importSPKI(..., 'ES256')
// how to pass a signed JWS as parameter?
const jwe = await new jose.CompactEncrypt(jws)
.encrypt(pkcs8PublicKey)
return jwe
}
The input to the CompactEncrypt constructor needs to be a Uint8Array, so just wrapping the jws like so (new TextEncoder().encode(jws)) will allow you to move forward.
Moving forward then:
You are also missing the JWE protected header, given you likely use an EC key (based on the rest of your code) you should a) choose an appropriate EC-based JWE Key Management Algorithm (e.g. ECDH-ES) and put that as the public key import algorithm, then proceed to call .setProtectedHeader({ alg: 'ECDH-ES', enc: 'A128CBC-HS256' }) on the constructed object before calling encrypt.
Here's a full working example https://github.com/panva/jose/issues/112#issue-746919790 using a different combination of algorithms but it out to help you get the gist of it.

Azure "JsonWebTokenError: invalid algorithm"

Azure Static Web App (SWA) with integrated API. One of the step at backend API is to validate the Bearer Token with public key submitted in request headers:
const jwt = require("jsonwebtoken"); // v 8.5.1
async function getMSPublicKey(misc) // misc contains kid and tenantId, confirmed in F12 request header
{
var vurl = "https://login.microsoftonline.com/" + misc.tenantId + "/v2.0/.well-known/openid-configuration";
const x1 = await fetch(vurl);
const x2 = await x1.json();
const x3 = await fetch(x2.jwks_uri);
const k = await x3.json();
return pkey = k.keys.find( k => k.kid === misc.kid).x5c[0]; // public key in the entry matching kid
}
var vmisc = JSON.parse(ac.req.headers["misc"]);
var publickey = "-----BEGIN CERTIFICATE-----\n" + await getMSPublicKey(vmisc) + "\n-----END CERTIFICATE-----";
// next line is reported in AppTraces, Message = JsonWebTokenError: invalid algorithm
var payload = jwt.verify(theToken, publickey, { algorithms: ['RS256'] });
// theToken is validated ok at jwt.io
It only occurs when deployed to Azure cloud, local Azure Static Web Apps emulator is all ok.
Update, Guess this is something about Azure cloud, particularly security. similar result on another package Jose, error only on Azure cloud.
Update: found culprit My original code was sending the token in under Authorization name. Azure log shows its read-in length is always 372 vs. 1239 tested in local emulator. After renaming it to something else like mytoken, all good! This is undocumented, reminder to everyone: avoid sensitive/reserved words.
This ought to be painless and work the same with less code on your end, it handles rotation, re-fetching of the public keys, as well as implements a complete applicable JWK selection algorithm for all known JWS algorithms. Also does not depend on a brittle x5c[0] JWK parameter.
const jose = require('jose')
const JWKS = jose.createRemoteJWKSet(new URL(`https://login.microsoftonline.com/${misc.tenantId}/discovery/v2.0/keys`))
// JWKS you keep around for subsequent verifications.
const { payload, protectedHeader } = await jose.jwtVerify(jwt, JWKS)
Please check if the below steps help to work around:
Replace the CERTIFICATE keyword with PUBLIC KEY if you're using the public key or PRIVATE KEY if you're using the Private Key or RSA PRIVATE KEY if you are using RSA Private Key.
Also, the problem again occurs in the way we format the Public Key which requires begin and end lines, and line breaks at every 64 characters.
Refer here for more information.

Can't verify webhook Node.js

I'm attempting to verify a webhook signature from Patreon using Node.js. Here is my code:
const crypto = require("crypto");
...
function validateJsonWebhook(request) {
const secret = SECRET_KEY_GIVEN_BY_PATREON;
const hash = crypto.createHmac("md5", secret)
.update(JSON.stringify(request.body))
.digest("hex");
if (request.header("x-patreon-signature") === hash) {
return true;
} else {
return false;
}
}
Patreon webhooks use MD5 - see https://docs.patreon.com/#webhooks.
I've verified the secret key multiple times so I know that's not the issue.
Both "request.header("x-patreon-signature")" and "hash" are returning the correct format (i.e. they're both a 32 digit letter-number combination) but they're just not matching.
Any idea about what's going on?
So #gaiazov's comment led me to do some Googling which led me to the first two comments on Stripe verify web-hook signature HMAC sha254 HAPI.js by Karl Reid which led me to https://github.com/stripe/stripe-node/issues/331#issuecomment-314917167.
For anyone who finds this in the future: DON'T use JSON.stringify(request.body) - use request.rawBody instead, as the signature is calculated based on the raw JSON. I feel like this should be emphasized in Patreon's documentation, as all the examples I found used the code I originally posted. My new, working code is as follows (I cleaned up the "if (request.header("x-patreon-signature") === hash)" part at the end):
const crypto = require("crypto");
...
function validateJsonWebhook(request) {
// Secret key given by Patreon.
const secret = patreonSecret;
const hash = crypto.createHmac("md5", secret)
.update(request.rawBody)
.digest("hex");
return (request.header("x-patreon-signature") === hash);
}

How am i doing this wrong: SubtleCrypto (Node.js) encryption to RSACryptoServiceProvider (.net) decryption with OAEP

I've been trying to crack this for a while with no success.
The server-side decryption uses RSACryptoServiceProvider RSA-OAEP. I can't change this
public void SetEncryptedPassword(string password) {
using (RSACryptoServiceProvider decrypter = new RSACryptoServiceProvider()) {
decrypter.FromXmlString(Resources.PrivateKey);
var decryptedBytes = decrypter.Decrypt(Convert.FromBase64String(password), true);
_password = Encoding.UTF8.GetString(decryptedBytes).ToSecureString();
}
}
I am trying to implement a web client that can access this service but I can't get the encryption right. I have tried loads of libraries but found the most help with SubtleCrypto, which at least can accept the public key provided by the server. I had to add the kty, alg and ext properties and encode the key as URL Base64, but it appears to import fine. Encryption does come back with something so I guess it's working?
const encrypt = async (msg)=>{
let msgBytes = stringToBytes(msg);
let publicKey2 = await window.crypto.subtle.importKey("jwk",publicKey, {name:"RSA-OAEP", hash:"SHA-256"}, true, ["encrypt"]).catch((issue)=>console.log(issue));
var result = await window.crypto.subtle.encrypt({name: "RSA-OAEP"}, publicKey2, msgBytes );
var toBase64 = _arrayBufferToBase64(result);
return toBase64;
}
I had a few issues getting a valid base64 string so now I'm using this
function _arrayBufferToBase64( buffer ) {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa( binary );
}
The result looks a little shorter than the outputs produced by the iPad and .net services, but I have no idea if that means anything.
The decryption always fails with the error "Error occurred while decoding OAEP padding.", which tells me that it fails at the first step.
Am I doing something wrong? Any advice would be helpful. I'll be watching comments and replies for most of the day so I can supply extra information if you ask for it.
Thanks in advance
CodeSandbox.io demo
The problem arises because in the C#-code SHA-1 is (implicitly) used for OAEP and in the JavaScript/Node.js-code SHA-256.
RSACryptoServiceProvider only supports PKCS#1 v1.5-padding and OAEP with SHA-1. The support of OAEP with SHA-2 is only
implemented for the newer RSA implementation, RSACng (available since .NET 4.6), which belongs to the new Cryptography API (Next Generation).
Since you can't change the C#-code according to your own statement, there is only the possibility to change the hash in the JavaScript/Node.js-code from SHA-256 to SHA-1.

how to generate encrypted JWE with node-jose

I'm using node-jose v0.11.0 (https://www.npmjs.com/package/node-jose) for JWK and JWE operations. I have an RSA key in JWK format that I can load into a JWK key store and also extract again. However, when I try to encrypt anything, I get into the "error2", "unsupported algorithm". How is it possible that RSA is an unsupported algorithm?
import * as jose from "node-jose";
const webkey = {
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "a024254d-0321-459f-9530-93020ce9d54a",
"key_ops": [
"encrypt"
],
"n": "jkHgYN98dlR2w7NX-gekCWaCdbxs7X4XXh52DVQrK--krwUYqRbBIUEw1bV8KX0ox6TLt-e6wpYsYYFUItSd5ySqohHRMq1IhyE2zpEC95BA9V7VrFUYnczf1bd5c-aR079aoz5JPXfqx01TzNfxWBb04SlRjsmJeY1v6JrDUI5U0FSOmnJTb3tSS6Szrvi_qOyViYp4v9V2_OVYy45kF_LQQy-pr-kP4gapXL235cieeTW6UvkhzaPT2D-JKyzVjjjgnfRXr8Ox9I9c4wpef2-5nPPeafB5EnOMpJE11KzO_8xxiTGUywPPLQagBvY35gkhQbYS2dv3NGIVSLZHFw"
}
]
};
console.log("webkey", webkey);
//generate key store from public JWK
jose.JWK.asKeyStore(webkey)
.then((result) => {
console.log("Key Store", JSON.stringify(result.toJSON()));
let keyStore = result;
//get the key to encrypt
const encryptionKey: jose.JWK.Key = keyStore.get(webkey.keys[0].kid);
const output = jose.util.base64url.encode("Hello World");
const output2 = jose.util.asBuffer(output);
//encrypting content
jose.JWE.createEncrypt(encryptionKey)
.update(output2)
.final()
.then((jweInGeneralSerialization) => {
console.log("Encryption result", JSON.stringify(jweInGeneralSerialization));
}, (error) => {
console.log("error2", error.message);
});
}, (error) => {
console.log("error1", error.message);
})
The output is as follows:
'webkey', Object{keys: [Object{kty: ..., e: ..., kid: ..., key_ops: ..., n: ...}]}
'Key Store', '{"keys":[{"kty":"RSA","kid":"a024254d-0321-459f-9530-93020ce9d54a","key_ops":["encrypt"],"e":"AQAB","n":"jkHgYN98dlR2w7NX-gekCWaCdbxs7X4XXh52DVQrK--krwUYqRbBIUEw1bV8KX0ox6TLt-e6wpYsYYFUItSd5ySqohHRMq1IhyE2zpEC95BA9V7VrFUYnczf1bd5c-aR079aoz5JPXfqx01TzNfxWBb04SlRjsmJeY1v6JrDUI5U0FSOmnJTb3tSS6Szrvi_qOyViYp4v9V2_OVYy45kF_LQQy-pr-kP4gapXL235cieeTW6UvkhzaPT2D-JKyzVjjjgnfRXr8Ox9I9c4wpef2-5nPPeafB5EnOMpJE11KzO_8xxiTGUywPPLQagBvY35gkhQbYS2dv3NGIVSLZHFw"}]}'
'error2', 'unsupported algorithm'
Update
I digged around a bit in the actual code and found in "basekey.js" that the error is thrown because the algorithms of the library are empty.
Object.defineProperty(this, "encrypt", {
value: function(alg, data, props) {
// validate appropriateness
if (this.algorithms("encrypt").indexOf(alg) === -1) {
console.log("Algorithm USED", alg
);
console.log("All algorithms", this.algorithms("encrypt"))
return Promise.reject(new Error("unsupported algorithm"));
}
The output here is:
'Algorithm USED', 'A128CBC-HS256'
'All algorithms', []
I have an example that I added to another question:
node-jose explanation / example?
I used node-jose in a research proof, for a reflection of my c# code, I only created signed and Encrypted tokens for decryption and verification, on my server ( written in c#).
I need to use symetric secret key or asymetric public private key pair
?
I used RSA keys for Asymmetric signatures and key wrapping the Symmetric encryption details of the content. The Encryption algorithm for content encryption is a Symmetric one. The node-jose package generated the Symmetric key. The Key Wrap algorithm encrypted the Symmetric key.
The C# code I have decrypts and validates the token signature. Please note: I used the functions of the package to do all the work.
Here are my runkit notebooks for my workups:
for signing (JWS) https://runkit.com/archeon2/5bd66a8e7ee3b70012ec2e39
for encrypting (JWE) https://runkit.com/archeon2/5bd6736ff36b39001313262a
In my final, I combined the two, creating a signed token, then used the output as the payload for the encrypted one (JWS + JWE). I was successful using the c# server code in decrypting, and validating the created tokens.
JWS + JWE : https://runkit.com/archeon2/jws-jwe-integration
How i need to generate and where i need to store keys in my server
node app to then allow me to sign and verify my tokens ?
var store = jose.JWK.createKeyStore();
await store.generate("RSA",2048,{alg:"RS256", key_ops:["sign", "decrypt", "unwrap"]});
lkey = (await store.get());
var key = lkey.toJSON(); //get public key to exchange
key.use = "sig";
key.key_ops=["encrypt","verify", "wrap"];
var pubKey = await jose.JWK.asKey(key);
key = null;
The Keystore can be serialized to JSON, so my concept would be to store this in Session Storage, or Local storage in a browser. Then retrieve the JSON representation and read in the Keystore.
var store= await jose.JWK.asKeyStore({"keys":[{"kty":"RSA","kid":"h9VHWShTfENF6xwjF3FR_b-9k1MvBvl3gnWnthV0Slk","alg":"RS256","key_ops":["sign","decrypt","unwrap"],"e":"AQAB","n":"l61fUp2hM3QxbFKk182yI5wTtiVS-g4ZxB4SXiY70sn23TalKT_01bgFElICexBXYVBwEndp6Gq60fCbaBeqTEyRvVbIlPlelCIhtYtL32iHvkkh2cXUgrQOscLGBm-8aWVtZE3HrtO-lu23qAoV7cGDU0UkX9z2QgQVmvT0JYxFsxHEYuWBOiWSGcBCgH10GWj40QBryhCPVtkqxBE3CCi9qjMFRaDqUg6kLqY8f0jtpY9ebgYWOmc1m_ujh7K6EDdsdn3D_QHfwtXtPi0ydEWu7pj1vq5AqacOd7AQzs4sWaTmMrpD9Ux43SVHbXK0UUkN5z3hcy6utysiBjqOwQ","d":"AVCHWvfyxbdkFkRBGX225Ygcw59fMLuejYyVLCu4qQMHGLO4irr7LD8EDDyZuOdTWoyP7BkM2e7S367uKeDKoQ6o1LND2cavgykokaI7bhxB0OxhVrnYNanJ1tCRVszxHRi78fqamHFNXZGB3fr4Za8frEEVJ5-KotfWOBmXZBvnoXbYbFXsKuaGo121AUCcEzFCGwuft75kPawzNjcdKhItfFrYh45OQLIO08W0fr_ByhxzWMU7yFUCELHSX5-4GT8ssq1dtvVgY2G14PbT67aYWJ2V571aSxM8DTwHrnB9tI8btbkXWt9JyVoQq13wDdo5fVN-c_5t07HBIaPoAQ","p":"8nLGa9_bRnke1w4paNCMjpdJ--eOUpZYbqEa8jnbsiaSWFwxZiOzUakIcpJ3iO0Bl28JEcdVbo7DE7mZ4M3BkOtm577cNuuK8243L7-k1a71X_ko2mQ3yF4rG2PzWAH_5P4wca1uk0Jj3PmhbkXDI6f_btm1X7Vw_U1K6jRhNbE","q":"oCe94Bed1Wzh-xgNq0hz52Z6WLf9eQlNxLzBbYkpLc_bGj9vMeGNO10qdxhWPi8ClkW9h5gBiFEk2s6aEWYRvIoZjrMYXD7xzyTNC5zcsikjNhM3FVj-kVdqUJy25o9uqgn2IwTvQr5WSKuxz37ZSnItEqK5SEgpCpjwEju_XhE","dp":"jAe2ir-0ijOSmGtZh2xMgl7nIFNRZGnpkZwDUDwSpAabJ-W3smKUQ2n5sxLdb3xUGv7KojYbJcvW6CGeurScQ_NycA9QaXgJvSe_QBjUP4bZuiDSc7DGdzfMdfl4pzAgeEZH_KBK6UrDGvIjRumMF6AEbCXaF_lX1TU7O6IdM0E","dq":"fDU2OjS2sQ5n2IAYIc3oLf-5RVM0nwlLKhil_xiQOjppF9s4lrvx96dSxti2EjYNUJQ34JBQJ_OenJ_8tx-tA8cq-RQHAYvDp75H1AjM1NO4vjh60PCbRgdAqdJQu1FkJzXgkdpC4UWSz3txRJaBWQ5hzIEtJ1Tnl5NzJQD3crE","qi":"3EoKqhKh5mwVGldSjwUGX7xnfQIfkQ4IETsQZh9jcfOFlf9f8rT2qnJ7eeJoXWlm5jwMnsTZAMg4l3rUlbYmCdg10zGA5PDadnRoCnSgMBF87d0mVYXxM1p2C-JmLJjqKhJObr3wndhvBXUImo_jV6aHismwkUjc1gSx_b3ajyU"},{"kty":"RSA","kid":"h9VHWShTfENF6xwjF3FR_b-9k1MvBvl3gnWnthV0Slk","use":"verify","alg":"RS256","key_ops":["encrypt","verify","wrap"],"e":"AQAB","n":"l61fUp2hM3QxbFKk182yI5wTtiVS-g4ZxB4SXiY70sn23TalKT_01bgFElICexBXYVBwEndp6Gq60fCbaBeqTEyRvVbIlPlelCIhtYtL32iHvkkh2cXUgrQOscLGBm-8aWVtZE3HrtO-lu23qAoV7cGDU0UkX9z2QgQVmvT0JYxFsxHEYuWBOiWSGcBCgH10GWj40QBryhCPVtkqxBE3CCi9qjMFRaDqUg6kLqY8f0jtpY9ebgYWOmc1m_ujh7K6EDdsdn3D_QHfwtXtPi0ydEWu7pj1vq5AqacOd7AQzs4sWaTmMrpD9Ux43SVHbXK0UUkN5z3hcy6utysiBjqOwQ","use":"sig"}]});
How can i know which one use between OCT, EC, RSA etc ?
For this, the need your token serves may dictate this. I needed the receiver to be the one who could see the contents, so I chose RSA, for Asymmetric keys. Forgery is a bit harder.
These notebooks are somewhat a work in progress. Please review with care, as this is my interpretation and how I worked out what I needed. My hope is that they give some guidance.

Resources