-
-
Notifications
You must be signed in to change notification settings - Fork 184
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
Provide default sign method, add external signer and webcrypto example. #220
Conversation
Just reading through the description for now. I think @dhensby's opinion will be of much value here. The way I imagined this was with the Abstract implemention staying in utils and then having signers like |
Alright thanks for your initial thoughts already! It might make more sense to keep the abstract classes in the utility package indeed. Maybe the |
So indeed the existing |
I restructured the commits a bit and made some small final changes, this is ready for review now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The addition of dependencies holds me back a lot. We've just reached the point where you have control over what dependencies come into your app. What I mean:
It isn't long ago when we split the packages from a single node-signpdf into multiple @signpdf-scoped ones. Now if you want to have the @signpdf/signer-p12
, it has a dep on node-forge
. So signer-p12 is a node-forge implementation of a @signpdf Signer
. With this change here implementation is (partly)moved away from node-forge and dependencies are added to utils. The problem with dependencies of utils is that utils are in turn used by most other packages so these dependencies land in all of them.
This is why I believe Signer
should remain as less implemented as possible - so the specifics of how we generate that signature remain in the specific implementations.
I think I mentioned it earlier but will say agian how this lines up with my thinking so far:
If your app wants to have webcrypto signer (the one you have in examples) it needs to be either your custom implementation (as in the examples) or a new package @signpdf/signer-webcrypto
with peer dependencies on pkijs, asn1js... This way the other apps wanting to use it will need to: npm i @signpdf/signpdf @signpdf/signer-webcrypto pkijs asn1js
. Dependencies remain in their control. On the other hand users of the signer-p12
will still need to npm i @signpdf/signpdf @signpdf/signer-p12 node-forge
and because node-forge already has the tooling for asn1 and PKI, the app won't need pkijs and asn1js.
I hope I'm making sense.
In terms of code it seems right. Just having seconds thoughts about how it fits in the current way we're doing things. I would really wait for @dhensby's opinion here.
In regards to the signature size (without having debugged or tracked down) I am guessing it could be some zero-trimming/padding difference somewhere as you are using the new deps to convert things.
Ok, thanks for your comments and further clarifications. For what it's worth I will share my personal opinion here (from a user's perspective), but of course I haven't as much experience with it as I only discovered this library a few months ago. I think as a user, I shouldn't really be bothered by how the signature is generated and put into the PDF (as a CMS signed data structure) and which dependencies are necessary for that. If I want to use a P12 certificate, it would be nice to just install the Of course we can create separate signers for each service, i.e. a |
Hey, sorry for the delay in looking at this. Firstly, great work and effort, @dcbr! Looks like you've put a good deal of time and thought into this, so thanks for that. I do share @vbuch's concerns about dependencies and the potential onward bloat that may be caused by this... However, I do wonder if there also needs to be a slight mental shift in what the library takes ownership of. At the moment the library is essentially, sign with forge and a P12 cert or... do it all yourself. A lot of the code here is similar to what I'm doing in my azure signer that I run in production, the entire CMS object needs to be constructed in the application and then signed in the KeyVault... That adds a lot of complication because there needs to be an understanding of how to construct the CMS object, encode it and so on. At the moment this library doesn't really need to concern itself with that because it essentially hands that responsibility off to As this addition shows, if you want to write any other kind of signer, you have to implement all the low-level logic for signature creation, not just signing. In that sense, the I do like this approach because it allows us to take control of the process and really provide the "plug-ability" of the consumer to just provide the cryptographic engine (ie: webcrypto, an external KMS/HSM, etc). But it does mean that there is quite a large increase in scope and responsibility of the library, especially to make it extensible in a way that is actually useful. At the moment, this PR has a fairly rigid CMS structure in place that doesn't really have any extensibility; for example, I'm creating LTV CMS signatures in my project and that requires embedding the OCSP response in the CRL info to the CMS object - that's not possible the way this PR is written at the moment... That of course opens the question of whether we even want it to be that flexible. My assumption on it when I first looked at making the signing step abstract (so being able to replace the I'm not going to answer those questions, that's more for @vbuch to decide on, I think... But what I would say is that whilst I like this approach, it wouldn't actually do anything for my personal use case but it would increase the maintenance burden of the package. |
Thanks for your comments @dhensby, I entirely agree with your point of view.
Regarding this point, I feel like this PR is a first step towards a more flexible implementation of the CMS generation. For example, I have been able to create PAdES B-B and B-T compliant signatures with it, with minimal extra code additions (just adding the right signed or unsigned attributes). I think it would be nice if this library supported different such signature types and your implementation of LTV CMS signatures would be a nice addition then as well (haven't delved into that myself yet, but it also comes down to adding extra (un)signed attributes IIRC?). |
I moved the base |
The main thing that determines if we take this forward is if @vbuch is happy to increase the scope of the package in this way or if it's better to continue to rely on external implementations (like |
Sorry for being a bit slow here. It's not about me being happy with increasing the scope. If the scope is large more people will need to take care of it :) I think from the point of view of PDF signing, all you care about is having an async sign() method. No further abstactions needed. On the other hand I see the value in ExternalSigner that gives you the structure but allows you to work further with specifics. With that said does it make sense to the old abstract Signer that only has sign() with zero implementation and then have another abstract PkiSigner that does the rest? Just thinking here. The idea was always to just show how it can be done. The more readable (least abstract) the code - the better. I'm not sure we've kept it readable enough through the years to be honest. But that was the aim. pki has moved into /signer as a dep, but /signer is a dep of signer-p12 so effectively p12 still requires pki. I don't know really. Your words all make sense and I may be protecting some values that I shouldn't... |
If this is worrying you, I can obviously help to maintain this part of the project if you want. No hard guarantees on responsiveness (I have my slower and faster periods), but I would be glad to help in resolving issues, reviewing PRs or further contributing to these abstract base classes.
I think it does make sense, taking into account the extra dependencies the base implementation brings with it.
That's a very honorable aim and I hope I can rework this PR together with you to make sure we achieve that. I think the signing process can still remain readable, even with an extra abstract
Indeed, but it is now a peer dependency (just like |
So that's settled.
Sounds right.
Yes. IMO keep the changes minimal and then, when there is need and time, refactors can happen. |
Alright, I'm wrapping up some other stuff and then I'll come back to this and do the necessary refactorings |
Alright, the conflicts are resolved and the |
This PR adds a new
ExternalSigner
abstract class that can be used to simplify the signing of pdfs using an external service (e.g. azure keyvaults or smartcards). An example implementation is also provided for signing with theWebCrypto
API and it was also tested with Belgian eID (identity card; code for this not included in this PR).Details
Signer
class is modified and now provides a default implementation for thesign
function. It uses thePKI.js
library to construct the CMS signed data structure expected by a PKKLite pkcs7.detached signature. The benefit of this library overnode-forge
(previously used for this purpose inP12Signer
) is that it supports asynchronous signing, which is necessary when communicating with external services. It also leverages theWebCrypto
API under the hood.Subclasses now only have to implement at least the
getCertificate()
(to retrieve the signing certificate) andgetKey()
(to retrieve the private key used for signing) methods. Furthermore, the used signing or hashing algorithms can be overridden and the used crypto object can be modified as well.ExternalSigner
abstract class is added to the@signpdf/utils
package, which supports generating signatures using an external signature provider.To use this class, users need to subclass it and implement at least the
getCertificate()
(to retrieve the signing certificate from the external service) andgetSignature(hash, data)
(to retrieve the signature of the given hash from the external service) methods. Similar to theSigner
base class, the used signing or hashing algorithms can be overridden and the used crypto object can be modified as well.webcrypto.js
andwebcrypto-external.js
example scripts are added that show an example implementation of theSigner
andExternalSigner
classes respectively. Both use theWebCrypto
API, which supports signing with RSA and ECDSA.