vbuch / node-signpdf

Simple signing of PDFs in node.
MIT License
700 stars 175 forks source link

Sign PDF with external service returning signature + certificate from hash (no private key access) #46

Closed joelviel closed 4 years ago

joelviel commented 4 years ago

Hi,

The application is great but in my case I don't have access to a p12 certificate containing a private key to build the CMS. I only have access to an external provider that takes a hash I give him as input and return user certificate as well as signature of the hash in response (never access to the private key from my point of view). Would it be possible to achieve the construction of the signature to be inserted in the PDF with such external provider?

Thank you for your help

vbuch commented 4 years ago

Take a look at those issues that are related: https://github.com/vbuch/node-signpdf/issues/40, https://github.com/vbuch/node-signpdf/issues/34, https://github.com/vbuch/node-signpdf/issues/15

You should be able to achieve what you want with a slight modification of the code so that instead of using forge something else is used for generating the signature. @andres-blanco I think has done something in this direction in the past. Also, there are other people aronud this repo that could help. As I said, it should be possible with a small modification.

andres-blanco commented 4 years ago

Hey! If you want to use an external signature you can do it by passing a custom function as a key to forge when building the p7 message. You can check how it's done for pkcs11 here:

You may need to do some byte prepending, check this: https://stackoverflow.com/a/47106124

joelviel commented 4 years ago

I tried to hook your code using pcks 11 but I notice md.digest() value changes each time I run the code despite inputs are the same. My idea was to call the external service provider who will sign this value and return it. Is it normal md.digest() value change with same PDF input?

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had activity in the past 90 days. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] commented 4 years ago

This issue has been automatically because it was stale.

tafelito commented 4 years ago

@joelviel have you found a workaround for your requirement. I have do something similar to that

joelviel commented 4 years ago

Hi @tafelito,

Yes I found the solution, I built the CMS with the lib jsrsasign (not forge, it does not support ECDSA) ans then overwrite the signature value computed by the external provider.

const jsrsasign = require('jsrsasign')
const fs = require('fs')
let {pdf} = require('./pdf')

const certBuf = fs.readFileSync('./signer-certificate.crt')
const certPem = '-----BEGIN CERTIFICATE-----\n'+ certBuf.toString('base64') + '\n-----END CERTIFICATE-----'

pdf.start(fs.readFileSync('./sample.pdf'))
pdf.addPlaceholder({})

const signedData = jsrsasign.KJUR.asn1.cms.CMSUtil.newSignedData({
    content: {hex: pdf.HexToBeSigned},
    certs: [certPem],
    signerInfos: [{
        hashAlg: 'sha256',
        sAttr: {
            SigningTime: {}
        }, 
        signerCert: certPem,
        sigAlg: "SHA256withECDSA",
        signerPrvKey: {
            d: null,
            curve: 'secp384r1'
        }
    }]
})

signedData.dEncapContentInfo.isDetached = true
const dSignedAttrs = signedData.signerInfoList[0].dSignedAttrs.getEncodedHex()
console.log('dSignedAttrs to be signed by external provider:', dSignedAttrs)

// Request here external service to get the signature of dSignedAttrs, and get the output externalSignatureAsn1 (e.g. "30650231008506d2...")

signedData.signerInfoList[0].dSig.hV = externalSignatureAsn1
const cmsHex = signedData.getContentInfoEncodedHex()

pdf.signature = cmsHex
pdf.saveFiles()

console.log(pdf.signature)

pdf object definition is attached pdf.txt

tafelito commented 4 years ago

thanks @joelviel for showing your solution

In my case, I don't have a certificate, my external provider expects a hash and returns the signed signature

I have a similar solution made in Java using PDFBox where it only does this

ExternalSigningSupport externalSigning = pdDocument.saveIncrementalForExternalSigning(pdfFOS);

and then it creates the hash I send the external provider like this

byte[] content = IOUtils.toByteArray(externalSigning.getContent());
MessageDigest md = MessageDigest.getInstance("SHA256", new BouncyCastleProvider());
digestA = md.digest(content);

// this saves the file with a 0 signature
externalSigning.setSignature(new byte[0]);

// remember the offset (add 1 because of "<")
int offset = signature.getByteRange()[1] + 1;

String hashToBeSigned = new String(Base64.encode(digestA))

Also, how did you come up with the signatureLength number? is that a random length?

I'm trying to figure out how would this translate to the examples used with node-signpdf

joelviel commented 4 years ago

You will need to change from that line https://github.com/vbuch/node-signpdf/blob/develop/src/signpdf.js#L104 and use jsrsasign instead of forge to get the attributes to be signed (dSignedAttrs in my example, it is asn1 structure containing the hash of the PDF with placeholder (hash of https://github.com/vbuch/node-signpdf/blob/develop/src/signpdf.js#L106) and eventually other data like signing time etc., but it does not contain the signer certificate) and then produce the CMS (also called pkcs7). The CMS must contain the signer certificate (required to confirm the signature is valid by the PDF reader), the provider has to give you this certificate as output along the signature. Finally you set this CMS at that line https://github.com/vbuch/node-signpdf/blob/develop/src/signpdf.js#L168 as the PDF signature

tafelito commented 4 years ago

but it does not contain the signer certificate

What do you mean by that? because you are creating the signedData with your certPem

How would you use jsrsasign without a certificate?

And how did you come up with the signatureLength number? is that a random length?

Thanks for the help by te way

tafelito commented 4 years ago

One more thing

In your example you assume that the response from the external service is right after hashing the document. In my case that could happen in some other time, how would you load the signature coming from the service later on?

joelviel commented 4 years ago

The value you need to sign is dSignedAttrs not the whole signedData object. You can use a dummy certificate as a first step, dSignedAttrs wont be affected. But you need to update the certificate when you will have it to produce a valid signedData (i.e. CMS). I suppose with such method signedData.signerInfoList[0].setSignerIdentifier(certPEM).

signatureLength will depend on what you use for the signature algo, in my case, SHA256withECDSA. Many other options are available (SHA512withECDSA, SHA512withRSA etc.). If your provider expects only a hash to be signed, you need to send hash(dSignedAttrs), hash function being sha256 in my example.

For your last question: use a dummy certificate to generate the value to be signed (dSignedAttrs), and update signedData attributes (signerInfoList[0].setSignerIdentifier(certPEM) and signerInfoList[0].dSig.hV) when you have the response of your provider

vbuch commented 4 years ago

@tafelito read the note regarding the signature length in our readme.

tafelito commented 4 years ago

For your last question: use a dummy certificate to generate the value to be signed (dSignedAttrs), and update signedData attributes (signerInfoList[0].setSignerIdentifier(certPEM) and signerInfoList[0].dSig.hV) when you have the response of your provider

So If I create the signedData in a step 1, then step 2 (apply the response from the provider) I won't have access to that variable since this happens in a different time. If i create the signedData object again in step 2, will it be the same? Otherwise, how do I get access to the signedData?

thanks again @joelviel and @vbuch for all the help

vbuch commented 4 years ago

TL:DR It will be the same.

Everyone in the process (signer and verifier) is relying on the signedData not being modified. This is exactly what you prove with applying a signature: document was not altered since it was signed. This is exactly what the verifier will check. They will construct the signed data, generate a hash, check the signature and also check that the hash that was signed is the same they have constructed.

jchapelle commented 3 years ago

Hi @tafelito,

Yes I found the solution, I built the CMS with the lib jsrsasign (not forge, it does not support ECDSA) ans then overwrite the signature value computed by the external provider.

const jsrsasign = require('jsrsasign')
const fs = require('fs')
let {pdf} = require('./pdf')

const certBuf = fs.readFileSync('./signer-certificate.crt')
const certPem = '-----BEGIN CERTIFICATE-----\n'+ certBuf.toString('base64') + '\n-----END CERTIFICATE-----'

pdf.start(fs.readFileSync('./sample.pdf'))
pdf.addPlaceholder({})

const signedData = jsrsasign.KJUR.asn1.cms.CMSUtil.newSignedData({
  content: {hex: pdf.HexToBeSigned},
  certs: [certPem],
  signerInfos: [{
      hashAlg: 'sha256',
      sAttr: {
          SigningTime: {}
      }, 
      signerCert: certPem,
      sigAlg: "SHA256withECDSA",
      signerPrvKey: {
          d: null,
          curve: 'secp384r1'
      }
  }]
})

signedData.dEncapContentInfo.isDetached = true
const dSignedAttrs = signedData.signerInfoList[0].dSignedAttrs.getEncodedHex()
console.log('dSignedAttrs to be signed by external provider:', dSignedAttrs)

// Request here external service to get the signature of dSignedAttrs, and get the output externalSignatureAsn1 (e.g. "30650231008506d2...")

signedData.signerInfoList[0].dSig.hV = externalSignatureAsn1
const cmsHex = signedData.getContentInfoEncodedHex()

pdf.signature = cmsHex
pdf.saveFiles()

console.log(pdf.signature)

pdf object definition is attached pdf.txt

Hey joelviel,

I'm trying to do the same kind of thing but I can't get a valid signature to be inserted in the pdf. The signature is not valid.

Can you explain what format the dSignedAttrs is and what format the signedData.signerInfoList[0].dSig.hV is supposed to be ? When setting the content to be sign of jsrsasign.KJUR.asn1.cms.CMSUtil.newSignedData, the generated dSignedAttrs is not a hash256 of the content. Do you knw how is generated dSignedAttrs ?

Cheers

joelviel commented 3 years ago

Hi @jchapelle , example of value of dSignedAttrs: 3169301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3231303732333038313833315a302f06092a864886f70d010904312204202c2c0a3e069410fb2f747d0225ec453e90f8c8c82847999e088baa589ca80a0b It is asn1 value that can be parsed on https://lapo.it/asn1js/ to see its content (signingTime, hash of the PDF with placeholder, ...)

example of signature by external service (smartcard in my case): 30650231008506d2bc16fda88c71477fa98864e7cd6e2b29a2b23cceac241b20060d08d265b1c526d8095145ec0715e2038e8341480230140d57364f5f3090a6be90677dcfc4843007cde102c8431e8b4d7cdb818285f4fa59971c522432b825350fe0b4479f35 It is asn1 value that can be parsed on https://lapo.it/asn1js/ to see its content (ecdsa signature i.e. 2 big integers). External service may return a concatSignature, this function KJUR.crypto.ECDSA.concatSigToASN1Sig(concatSig) can help to convert to ASN1 format

jchapelle commented 3 years ago

Great, thanks for the feedback.

What is the format of 3169301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3231303732333038313833315a302f06092a864886f70d010904312204202c2c0a3e069410fb2f747d0225ec453e90f8c8c82847999e088baa589ca80a0b ?

Is it an hex ? Or a Sha256->hex ? Or an hex->sha256 ? Or an sha256 ?

On Fri, Jul 23, 2021 at 10:37 AM Joel Viellepeau @.***> wrote:

Hi @jchapelle https://github.com/jchapelle , example of value of dSignedAttrs:

3169301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3231303732333038313833315a302f06092a864886f70d010904312204202c2c0a3e069410fb2f747d0225ec453e90f8c8c82847999e088baa589ca80a0b It is asn1 value that can be parsed on https://lapo.it/asn1js/ to see its content (signingTime, hash of the PDF with placeholder, ...)

example of signature by external service (smartcard in my case):

30650231008506d2bc16fda88c71477fa98864e7cd6e2b29a2b23cceac241b20060d08d265b1c526d8095145ec0715e2038e8341480230140d57364f5f3090a6be90677dcfc4843007cde102c8431e8b4d7cdb818285f4fa59971c522432b825350fe0b4479f35 It is asn1 value that can be parsed on https://lapo.it/asn1js/ to see its content (ecdsa signature i.e. 2 big integers). External service may return a concatSignature, this function KJUR.crypto.ECDSA.concatSigToASN1Sig(concatSig) can help to convert to ASN1 format

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/vbuch/node-signpdf/issues/46#issuecomment-885488685, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABPAMXJVA3UEN6LILF7RJ7TTZES3LANCNFSM4JWPPDEA .

-- Jean Chapelle

sbsatter commented 2 years ago

Hi @tafelito,

Yes I found the solution, I built the CMS with the lib jsrsasign (not forge, it does not support ECDSA) ans then overwrite the signature value computed by the external provider.

const jsrsasign = require('jsrsasign')
const fs = require('fs')
let {pdf} = require('./pdf')

const certBuf = fs.readFileSync('./signer-certificate.crt')
const certPem = '-----BEGIN CERTIFICATE-----\n'+ certBuf.toString('base64') + '\n-----END CERTIFICATE-----'

pdf.start(fs.readFileSync('./sample.pdf'))
pdf.addPlaceholder({})

const signedData = jsrsasign.KJUR.asn1.cms.CMSUtil.newSignedData({
  content: {hex: pdf.HexToBeSigned},
  certs: [certPem],
  signerInfos: [{
      hashAlg: 'sha256',
      sAttr: {
          SigningTime: {}
      }, 
      signerCert: certPem,
      sigAlg: "SHA256withECDSA",
      signerPrvKey: {
          d: null,
          curve: 'secp384r1'
      }
  }]
})

signedData.dEncapContentInfo.isDetached = true
const dSignedAttrs = signedData.signerInfoList[0].dSignedAttrs.getEncodedHex()
console.log('dSignedAttrs to be signed by external provider:', dSignedAttrs)

// Request here external service to get the signature of dSignedAttrs, and get the output externalSignatureAsn1 (e.g. "30650231008506d2...")

signedData.signerInfoList[0].dSig.hV = externalSignatureAsn1
const cmsHex = signedData.getContentInfoEncodedHex()

pdf.signature = cmsHex
pdf.saveFiles()

console.log(pdf.signature)

pdf object definition is attached pdf.txt

Hi @joelviel,

Thank you for your provided solution. But I guess since the post, the jsrsasign library has been updated and it no longer supports these SignedData properties:

dEncapContentInfo
signerInfoList

Is detached can probably be set by passing datached = true in params object. But I can not find the new way to access the signed attributes from here anymore.

Do you have any idea in this regard? Any resource/tip will be helpful. All my searches are turning out blank for the past two days!

P.S. This is the error (given I change the line isDetached with detach=true):

/Users/sbsatter/Development/IntelliJ/PDFSigningPOC/node/js-sign.js:47
const dSignedAttrs = signedData.signerInfoList[0].dSignedAttrs.getEncodedHex()
                                              ^

TypeError: Cannot read property '0' of undefined
    at Object.<anonymous> (/Users/sbsatter/Development/IntelliJ/PDFSigningPOC/node/js-sign.js:47:47)
    at Module._compile (node:internal/modules/cjs/loader:1092:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1121:10)
    at Module.load (node:internal/modules/cjs/loader:972:32)
    at Function.Module._load (node:internal/modules/cjs/loader:813:14)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
    at node:internal/main/run_main_module:17:47
qamalyanaren commented 2 years ago

Hello, I would like to create desktop application (Electron JS) to sign a pdf using external service returning signature (Base64SignatureOfTheHash), front side sends PDF hash and backend side return signature of the hash. How to insert signed hash to the PDF ?

kacimi03hamza commented 2 years ago

Hello i facing some issue with the code provider above bug

naveenruncode commented 11 months ago

@sbsatter @kacimi03hamza Have you guys found any solution? I'm also facing the same issue.