Open dhensby opened 3 years ago
Hello @dhensby , I have a question, is 'md' hash of the file in the following section ?: key: { sign: async (md) => { const signature = await getSignatureSomehow(md); return signature; } and if yes, how to get base64 string from md?
@DavAslanyan If you're looking at integrating what I've done like-for-like, the message digest is created by forge from the content for you automatically, so you don't really need to worry about what it is, exactly...
But yes, I believe it's the digest for the content you want to sign (ie: the file content, a string, whatever).
The md here is a forge md object. You have to digest it and then you can extract the digested hash in you preferred format:
const digest = md.digest(); // this returns a forge buffer
console.log(digest.getBytes()); // binary string, best used for converting to native buffer like so: `Buffer.from(digest.getBytes(), 'binary');`
console.log(digest.toHex()); // hex string
For base64 you need to do a bit more, personally I like using native buffers so I create a native buffer from the bytes then encode it as a string, but you can also use the util.encode64()
function from this lib:
// native buffer approach
const base64 = Buffer.from(digest.getBytes(), 'binary').toString('base64');
// util approach
const base64 = forge.util.encode64(digest.getBytes());
Oh, and the md object is important here because it's how you know what algorithm you're using to sign the data with (md.algoritm
, I think), so if you're using an HSM or similar to sign the data, then you need more than just the digested data.
@dhensby thank you for your answer. I would like to sign a pdf using external service returning signature (Base64 signature of The hash).
sign: async (md) => { const signature = await getSignatureSomehow(md); return signature; }, The question is what kind of format should be returned signature?
And every time when I get this base64 string for the same file it is different, is it ok? const base64 = Buffer.from(digest.getBytes(), 'binary').toString('base64');
Here is some rough code for how to do it with Azure:
const { DefaultAzureCredential } = require("@azure/identity");
const { CertificateClient } = require("@azure/keyvault-certificates");
const { KeyClient, CryptographyClient } = require("@azure/keyvault-keys");
const forge = require('node-forge');
const credential = new DefaultAzureCredential();
const vaultName = "<YOUR VAULT NAME>";
const url = `https://${vaultName}.vault.azure.net`;
const certClient = new CertificateClient(url, credential);
const keyClient = new KeyClient(url, credential);
const certificateName = "<YOUR CERTIFICATE NAME>";
/**
* Convert the forge md and signing scheme to the Azure equivalent
*
* @param md
* @param scheme
* @returns {string}
*/
function forgeMdToAzureAlg(md, scheme) {
const encoding = scheme === 'RSASSA-PKCS1-V1_5' ? 'RS' : 'PS';
switch(md.algorithm) {
case 'sha256':
return `${encoding}256`;
case 'sha384':
return `${encoding}384`;
case 'sha512':
return `${encoding}512`;
default:
throw new Error('Unsupported algorithm');
}
}
async function main() {
// Fetch the certificate data from Key Vault
const azCert = await certClient.getCertificate(certificateName);
// fetch the certificate's key from the vault
const keyId = new URL(azCert.keyId);
const [, , keyName, keyVersion] = keyId.pathname.split('/');
const key = await keyClient.getKey(keyName, { version: keyVersion });
// construct a certificate PEM for Forge to work with
const certPem = ['-----BEGIN CERTIFICATE-----', azCert.cer.toString('base64'), '-----END CERTIFICATE-----'].join('\n');
// create the certificate and p7 signed data object
const certificate = forge.pki.certificateFromPem(certPem);
const p7 = forge.pkcs7.createSignedData();
p7.content = forge.util.createBuffer('data to sign');
p7.addCertificate(certificate);
// add as many signers as you like
p7.addSigner({
key: {
// an async signer, which uses the certificates key to sign the data
sign: async (md, scheme) => {
const cryptoClient = new CryptographyClient(key, credential);
// convert the message digest object to the correct algorithm name for Azure and supply the digest as a buffer
const signature = await cryptoClient.sign(forgeMdToAzureAlg(md, scheme), Buffer.from(md.digest().getBytes(), 'binary'));
// return the binary string of the signature
return signature.toString('binary');
},
},
certificate,
// this bit is important, you must choose a supported algorithm by the key vault
// sha1 is not supported, for example
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [
{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data,
}, {
type: forge.pki.oids.messageDigest,
// value will be auto-populated at signing time
}, {
type: forge.pki.oids.signingTime,
// value can also be auto-populated at signing time
// We may also support passing this as an option to sign().
// Would be useful to match the creation time of the document for example.
value: new Date(),
},
],
});
// perform the signing - ideally this would be async and we could just await this
p7.sign({ detached: true });
// walk through all the internally assigned promises we now need to wait to resolve
p7.signerInfos = await Promise.all(p7.signerInfos.map(async (signerInfo) => {
signerInfo.value = await Promise.all(signerInfo.value.map(async (value) => {
value.value = await value.value;
return value;
}));
return signerInfo;
}));
// just for completeness, assign resolved values to signer's signature
p7.signers = await Promise.all(p7.signers.map(async (p7Signer) => {
p7Signer.signature = await p7Signer.signature;
return p7Signer;
}));
// construct the raw signature
const raw = forge.asn1.toDer(p7.toAsn1()).getBytes();
// logs the signature as hex to the console - job done
console.log(Buffer.from(raw, 'binary').toString('hex'));
}
main().catch(console.error);
Oh, and @DavAslanyan - if you're looking to sign PDFs and you want to hit some kind of PAdES compliance, you're in for a world of pain, as forge just doesn't construct the signatures in a way that is compliant with PAdES.
You need to start fetching and integrating OCSP responses (not supported by this library) among other things that I struggled with.
In the end I had to throw out the forge library for signing PDFs and I still haven't managed to sign the PDF to be PAdES-LTV compliant (I can't seem to find a PDF library that can do this in node)
Thanks for your response, it really seems challenging.
Hello @dhensby. Did you find a way to sign a PDF document with LTV enabled?
@funfuncode yes and no. I'm able to construct an LTV compliant CMS signature and attach it to a PDF, but the PDF standard then calls for the PDF to be signed again for it to show up in Adobe Acrobat as signed properly. As such, I've got PDFs with LTV signatures, but not ones that are validated by acrobat. I'm still looking for some help to do that 😓
Here is some rough code for how to do it with Azure:
const { DefaultAzureCredential } = require("@azure/identity"); const { CertificateClient } = require("@azure/keyvault-certificates"); const { KeyClient, CryptographyClient } = require("@azure/keyvault-keys"); const forge = require('node-forge'); const credential = new DefaultAzureCredential(); const vaultName = "<YOUR VAULT NAME>"; const url = `https://${vaultName}.vault.azure.net`; const certClient = new CertificateClient(url, credential); const keyClient = new KeyClient(url, credential); const certificateName = "<YOUR CERTIFICATE NAME>"; /** * Convert the forge md and signing scheme to the Azure equivalent * * @param md * @param scheme * @returns {string} */ function forgeMdToAzureAlg(md, scheme) { const encoding = scheme === 'RSASSA-PKCS1-V1_5' ? 'RS' : 'PS'; switch(md.algorithm) { case 'sha256': return `${encoding}256`; case 'sha384': return `${encoding}384`; case 'sha512': return `${encoding}512`; default: throw new Error('Unsupported algorithm'); } } async function main() { // Fetch the certificate data from Key Vault const azCert = await certClient.getCertificate(certificateName); // fetch the certificate's key from the vault const keyId = new URL(azCert.keyId); const [, , keyName, keyVersion] = keyId.pathname.split('/'); const key = await keyClient.getKey(keyName, { version: keyVersion }); // construct a certificate PEM for Forge to work with const certPem = ['-----BEGIN CERTIFICATE-----', azCert.cer.toString('base64'), '-----END CERTIFICATE-----'].join('\n'); // create the certificate and p7 signed data object const certificate = forge.pki.certificateFromPem(certPem); const p7 = forge.pkcs7.createSignedData(); p7.content = forge.util.createBuffer('data to sign'); p7.addCertificate(certificate); // add as many signers as you like p7.addSigner({ key: { // an async signer, which uses the certificates key to sign the data sign: async (md, scheme) => { const cryptoClient = new CryptographyClient(key, credential); // convert the message digest object to the correct algorithm name for Azure and supply the digest as a buffer const signature = await cryptoClient.sign(forgeMdToAzureAlg(md, scheme), Buffer.from(md.digest().getBytes(), 'binary')); // return the binary string of the signature return signature.toString('binary'); }, }, certificate, // this bit is important, you must choose a supported algorithm by the key vault // sha1 is not supported, for example digestAlgorithm: forge.pki.oids.sha256, authenticatedAttributes: [ { type: forge.pki.oids.contentType, value: forge.pki.oids.data, }, { type: forge.pki.oids.messageDigest, // value will be auto-populated at signing time }, { type: forge.pki.oids.signingTime, // value can also be auto-populated at signing time // We may also support passing this as an option to sign(). // Would be useful to match the creation time of the document for example. value: new Date(), }, ], }); // perform the signing - ideally this would be async and we could just await this p7.sign({ detached: true }); // walk through all the internally assigned promises we now need to wait to resolve p7.signerInfos = await Promise.all(p7.signerInfos.map(async (signerInfo) => { signerInfo.value = await Promise.all(signerInfo.value.map(async (value) => { value.value = await value.value; return value; })); return signerInfo; })); // just for completeness, assign resolved values to signer's signature p7.signers = await Promise.all(p7.signers.map(async (p7Signer) => { p7Signer.signature = await p7Signer.signature; return p7Signer; })); // construct the raw signature const raw = forge.asn1.toDer(p7.toAsn1()).getBytes(); // logs the signature as hex to the console - job done console.log(Buffer.from(raw, 'binary').toString('hex')); } main().catch(console.error);
This was really helpful, @dhensby. The ideas here - especially the async signer and resolving signerInfos - got me past the roadblocks I had with Azure Key Vault signing. Put together a project with the learnings here: https://github.com/othnielee/pdf-sign. Thanks!!
I have some requirements to use external hosted Key Management Services; for example to produce a p7 signature of a JSON payload but with a hosted certificate.
At the moment, it's possible to pull down the certificate and construct the request, and with a bit of monkey-patching/hackery it's possible to actually perform an async key signing (see example below) - but it would be much nicer if this was officially supported so that keys could be more decoupled and externally hosted.