Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
joelviel opened this issue Dec 6, 2019 · 22 comments

Comments

@joelviel
Copy link

joelviel commented Dec 6, 2019

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

@joelviel joelviel changed the title Sign PDF with external service returning signing from hash Sign PDF with external service returning signature + certificate from hash (no private key access) Dec 6, 2019
@vbuch
Copy link
Owner

vbuch commented Dec 6, 2019

Take a look at those issues that are related:
#40, #34, #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
Copy link
Contributor

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
Copy link
Author

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
Copy link

stale bot commented Mar 11, 2020

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 stale bot added the wontfix label Mar 11, 2020
@stale
Copy link

stale bot commented Mar 21, 2020

This issue has been automatically because it was stale.

@stale stale bot closed this as completed Mar 21, 2020
@tafelito
Copy link

tafelito commented Aug 6, 2020

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

@joelviel
Copy link
Author

joelviel commented Aug 6, 2020

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
Copy link

tafelito commented Aug 6, 2020

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
Copy link
Author

joelviel commented Aug 6, 2020

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
Copy link

tafelito commented Aug 6, 2020

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
Copy link

tafelito commented Aug 6, 2020

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
Copy link
Author

joelviel commented Aug 7, 2020

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
Copy link
Owner

vbuch commented Aug 7, 2020

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

@tafelito
Copy link

tafelito commented Aug 7, 2020

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
Copy link
Owner

vbuch commented Aug 7, 2020

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
Copy link

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
Copy link
Author

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
Copy link

jchapelle commented Jul 24, 2021 via email

@sbsatter
Copy link

sbsatter commented Oct 7, 2021

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
Copy link

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
Copy link

Hello i facing some issue with the code provider above
bug

@naveenruncode
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants