3

Our web application issues governmental documents for our users. Every one of those documents needs to be signed with a private key. However, because our users find it cumbersome to point their browser to their key file every time they want to issue a new document, we currently store their key files in AWS S3.

The passwords required to decrypt those documents are stored encrypted in our database, using a server environment variable and a salt value.

However, the fact still remains that if an attacker gets access to our server, they'll be able to get all our users private keys and impersonate them.

How would you handle that? I've thought of storing the keys in the browser (using something like IndexedDB) but I don't know if that's safe enough either.

1

2 Answers 2

4

There is a strong incompatibility in your requirements:

  • you want to sign a document on your server (fine till that point)
  • you want to use the user's private key to sign the document (only possible on the client machine, because a user's private key should never be known to a server)

You will have to choose a side. Either you need a strong non repudiable signature from a human being, and this can only occur client side, or a server signature can be enough.

In that later case, the server owner takes responsibility for the signature. It can only be trusted if:

  • the identity of the client has been established via a trustworthy procedure (trusted by the parties that will use the signature)
  • the code of the application and the administration is trustworthy in the same sense.

This can be perfectly valid for example inside a corporate network but is generally not enough for legal action (disclaimer IANAL)

3
  • I do not necessarily need to sign the documents on my server. I need to sign them in an automated, transparent way, though. The whole signing process could be done on the client, but the only way I can see to do that is to save the keys inside the browser. The issue with that is that, from the POV of laymen, such as our clients, storing their keys in their browsers probably sounds much less safe than storing them on a distant and theoretically secure server.
    – H. Sérvio
    Mar 15, 2023 at 0:09
  • 1
    @H.Sérvio If you want to sign them in an automated transparent way, signing them by the application on the server is probably the way to go. I just wanted to warn you that the responsibility cannot be the user's one but is the one of the application owner. For a digital signature to be legaly valid on most countries (at least in EU), the private key has to be under the exclusive control of its owner (and at all time), and the signing process must involve a voluntary action... Mar 15, 2023 at 10:07
  • ... I already said IANAL but I had to sign some documents with a legaly valid signature, and my keys were on a smartcard so I signed them localy using AcrobatReader. Not a very complex operation... Mar 15, 2023 at 10:09
2

The private keys NEED to be stored in the users' machines, not anywhere else. You could technically use local storage or similar for this, but the correct way to do it is to store the private keys in the browser's built-in cryptographic key storage. See https://developer.mozilla.org/en-US/docs/web/api/subtlecrypto, and the methods for generating or importing a private key, and then for using that key to sign data.

If your users need to be able to use multiple computers or to have backups of their keys in case that their computer dies, there are a few options.

  1. Let each user have multiple key pairs, one per machine (including a backup). This is less convenient from a key-management perspective - you'd also need multiple public keys for each user, and to let all counterparties know whenever any user adds a new public key (or else have a trusted way to look up what each user's current and past set of public keys is) - but it has advantages too. Each private key can be generated in situ on each machine, and never leave it in any form. It can be marked non-exportable to make it harder for an attacker to exfiltrate the key. A single key can be revoked (if you fear that e.g. the computer it's on might be compromised) without that user losing all of their keys.
  2. Let users transfer keys between machines themselves, without them going into centralized storage that could, in theory, access all of them. This is somewhat inconvenient for the user, greatly increases the risk of that specific key being compromised, and means that when a key is (or might be) compromised and gets revoked then the user has no trusted keys anymore. But, it hugely simplifies public key management; each user has one, known, public key, and the mapping between public keys and users doesn't change except when adding or removing a user, or needing to rotate/revoke a private key for some reason.
  3. As a compromise between these scenarios, allow the private key to be stored in your infrastructure, but before it ever leaves a user's computer, encrypt it with a symmetric key derived from a password that the user enters. This password should NOT be the same one that is used to log into the system (or, if it is, it needs to be hashed client-side in two different ways, one to derive the private-key-encrypting symmetric key, and one to pass to the server for authentication). Thus, the user has only one private key, but it never leaves their machine in plain text. The user can import their private key on new devices using the private key's encryption password (and presumably being logged into your infrastructure), but the actual private keys can't be decrypted and stolen from your server even in the case of total compromise[1]. The main downsides of this option are that your user still loses their only key pair if it has to be revoked for any reason and that your user must remember a password (possibly an additional password) though that's generally only actually needed when enrolling new devices.

Any of these options will be more secure, and closer to best practices around handling private keys, than what you're doing now. You can potentially have the client do nothing more than store the private key and relay data-to-sign in and signatures out, or have the client implement the entire document signing system in JS or WebASM or so on such that your servers merely need to store and retrieve the documents rather than implementing any actual signature-based logic themselves, or something in between.

The main issue you would run into - which you already have - is key authentication. How does somebody receiving a document know that it was signed by the right user? Unless the recipient knows for sure which public key corresponds to which user, they can't be sure that the document was actually signed by somebody else (possibly not a user at all, such as malware running on the server). The recipient can ask the server "hey, what's user X's public key?" of course, but a malicious or compromised server might lie in its response, perhaps returning the attacker's public key (which was used to fraudulently sign the modified document) instead of the legitimate user's public key (which can't verify the signature on the modified document). There are many attempts to solve this problem - public key infrastructure (PKI) with certificates and certificate authorities (as seen with TLS and S/MIME), web of trust with public key signing (as seen with PGP/GnuPG), trust-on-first-use (as seen with SSH), server-based key exchange with out-out-band verification (as seen with Signal, WhatsApp, and similar networks), and perhaps some others. All have drawbacks.

[1] Caveat on this point: with browser-based encryption, there's always the risk that a malicious or compromised site serves the browser a script that captures the key in plain text right off the client, or captures the user's login keystrokes, or whatever. This issue makes it impossible to build a proper zero-trust secure system on the web.

4
  • Okay, so, first of all, thank you for answering the question. I should've clarified, however that the generation of the keys and their validation is not done by our application. Basically, our users need to have their certificates issued by the government and how they get them is none of our business: we just import the pre-existing keys. What we do is sign a document they want to issue and send it to a government API. The government API will, then, verify the signature and return a successful response if everything is alright.
    – H. Sérvio
    Mar 14, 2023 at 23:53
  • I also have a question about the third option you've given: would the user need to type their password every time they issued a document? I can't see how that wouldn't be the case unless the password were to be stored in the browser (which I guess is also a no-go from a security perspective). Our users would definitely not like to type their password for every document they're trying to issue, though, that much I can say.
    – H. Sérvio
    Mar 14, 2023 at 23:57
  • @H.Sérvio The third case - like both the first and second - assumes that the private key is stored in the browser, using IndexedDB to store CryptoKey objects. If you want to store the keys more securely, you could wrap (encrypt) them before storage, and place the wrapping key (but not the wrapped key) on your infrastructure, such that it's worthless by itself but a user who is logged in can unwrap their stored key without needing another password.
    – CBHacking
    Mar 15, 2023 at 0:32
  • In no case would a password be required for every document. The CryptoKey object, once available (either extracted from IndexedDB or newly generated or imported using the browser's cryptographic API), persists like any other JS object and can be used multiple times. As for the key import, you can make that happen client-side; the web crypto API is capable of importing all common key formats, and your script could then send the public key to your server to be e.g. sent onward to the government for later verification of signatures.
    – CBHacking
    Mar 15, 2023 at 0:36

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .