21

I am building a web site that provides user login. For that, I am currently researching good strategies for dealing with authentication.

How I'm doing it right now

My current concept is modeled after what seems to be the common consensus right now. Passwords are salted with 64 bytes from /dev/urandom and then hashed with 100 rounds of SHA-512. After every round, the original password is concatenated to the result and then fed into the next round. When a user wants to log in, they send their credentials to the server, where the described procedure is then repeated (using the same salt, obviously) and compared to the hash in the database.

This strategy seems adequately secure to me (please correct me if I'm wrong, it is basically just the result of reading a lot of online guides and watching YouTube videos). However, I think it has a major flaw, which is the client having to send the password in plain text to the server. Yes, I naturally use HTTPS, but still, if the connection was somehow compromised for whatever reason, so is the password.

The alternative concept

So I thought of an entirely different approach: using PGP keys. When a user signs up, they generate a PGP key pair, encrypt the private key using a password of their choice and send it to the server. When they want to log in again, the server generates a string of random characters and encrypts it using the public key. The encrypted random string and key pair are then sent to the client, who will need to decrypt it again to prove they have the private key's password.

This method would prevent the password from ever being transmitted over the network and even allow for cool stuff like end-to-end encrypted chat between users. The only drawback I could find is the server having to give out encrypted private keys to basically anyone who requests them, making brute-force attacks way easier. I could mitigate that by running a computationally expensive key expansion algorithm on the client side and use the result of that for encrypting the private key.

But I still don't really trust the whole thing, and so I would love to hear your feedback on whether this is a good idea or if I should just stick with how I'm doing it right now.

EDIT:

Based on some of the answers, I think my question is a little misleading. My requirement is that the only thing users ever need to provide for successful authentication is their username/password and nothing else, regardless of what device they are using or whether they were logged in on that device before.

Sandtler
  • 329
  • 2
  • 7
  • 4
    This requires the client to store the key somehow on the local system (can be inside the browsers local storage) which kind of binds the key to the device, i.e. it is far less flexible than a password. If you find this limitation acceptable or even useful then why not use client certificates then which essentially already do what you are trying to reimplement with PGP? – Steffen Ullrich Jan 08 '20 at 19:39
  • Potential duplicate: https://security.stackexchange.com/questions/72330/why-are-pgp-signatures-not-more-widely-used-as-an-authentication-method and https://security.stackexchange.com/questions/135828/what-are-potential-risks-of-using-pgp-for-website-login – schroeder Jan 08 '20 at 20:32
  • 8
    Your current hashing method is quite close to PBKDF2. I'd suggest using an actual PBKDF2 implementation from a mature library or, if you can, use more robust key stretching algorithms, such as bcrypt, scrypt, or Argon2. If you're dead-set on SHA-512, you can keep using it with PBKDF2, though most libraries will default to SHA-256 and might not let you configure that option. – Ghedipunk Jan 08 '20 at 22:09
  • 15
    Um, logon via TLS certificates is a thing... No need to involve GPG/PGP, whose "web of trust" model contributes nothing here. – Marcus Müller Jan 08 '20 at 23:36
  • Why not just WebAuthn? – Natanael Jan 09 '20 at 01:16
  • 9
    Inventing _any_ new technique on your own is not a good idea. – chrylis -cautiouslyoptimistic- Jan 09 '20 at 09:10
  • Another thing to point out: doing this in a "web app" has serious problems because the code is loaded new and unauthenticated every time; if anyone manages to inject malicious code into the site they can compromise the code that loads the key on the client. – pjc50 Jan 09 '20 at 12:46
  • 2
    Just think about the following: The user will enter his password into a website, the website is delivered via https, if https is compromised the attacker can just manipulate the website and directly get the password. As soon as the user enters the password, you have to trust your https-connection a hundred percent, so transmitting it is not really any more risky than entering it into the password-field. – Falco Jan 09 '20 at 13:42
  • "The encrypted random string and key pair are then sent to the client, who will need to decrypt it again to prove they have the private key's password." - this is contrary to your latest edit. Now your question is confusing. – schroeder Jan 09 '20 at 17:11
  • _"if the connection was somehow compromised for whatever reason..."_ If an attacker can compromise TLS itself, 1) they have far more valuable targets than your website, and 2) any form of user authentication you do is largely meaningless – GrandOpener Jan 09 '20 at 20:33
  • You should use OPAQUE (see https://eprint.iacr.org/2018/163 and https://blog.cryptographyengineering.com/2018/10/19/lets-talk-about-pake/ ). It is a PAKE protocol that does something quite similar to what you are saying (they store an encrypted key pair on the server), but avoids the drawbacks of your scheme. – qbt937 Jan 10 '20 at 07:07
  • 2
    **100** rounds of SHA-512 - that is off by *at least* a factor of 1000. You want the hashing to take an appreciable fraction of a second (as high as possible without causing a noticeable delay to the user). You need to do some benchmarking, starting with an initial guess of 1 million iterations. I second the motion to use a standard library function for password hashing - and preferably not PBKDF2, but bcrypt/scrypt/Argon2. – Martin Bonner supports Monica Jan 10 '20 at 13:56
  • Independent of whatever problems may or may not exist with various proposals to solve the problem you believe you have: What is your threat model for an attacker who is in a position to 'snoop' your https content but hasn't also gained the ability to simply inject a little bit of client side keylogging to the password entry form? – Affe Jan 10 '20 at 18:37
  • I believe that quite a few 'protected' .onion sites, like the marketplaces, use this as the second step in 2FA. It may be helpful to check a few of them out to glean more info on implementation. Reddit has a list. – Adam Gold Jan 10 '20 at 22:30

8 Answers8

69

You should never try to secure a "real" web application with a scheme that you invented on your own. As such we shouldn't discuss practicalities on how you would actually implement or use such a method.

Will it work?

Your scheme does not send a password over the wire. What immediately jumps out is that you send the private key to the server, which never does anything with it, for no reason. Yes, it is supposedly still protected by the password, but why?

As others have pointed out, the private key is supposed to remain with the owner.

Even if you consider the private key's password encryption, there is the following problems (at least):

  • Handing out the private key in your scheme is essentially the same as making the password hash public: Anyone can now do an offline dictionary attack against the key's password; and nobody can stop that.
  • Even worse, they can get the key for a given login, meaning that they can check if a user exists and then attack the password for the user name.

With a small modification, your scheme would be a very simple form of public key authentication:

  • The client creates a key pair, and sends the public key to the server
  • To log in, the server sends out a random string.
  • The client signs the string and sends it back to the server.
  • The server verifies the signature to make sure the client has the private key

Something like this is actually already possible in https using client certificates. The same thing is used for ssh.

The Challenge-Response mechanism mentioned by ThoriumBR is another way of authenticating, although the implementation described there assumes that the server stores the password in cleartext -- which is much more dangerous than sending it over an encrypted connection.

Will it be "better"?

No. The scenario that you want to defend against is supposedly that someone gets inside the encrypted https connection. If you assume that this is the case, they would not only be able to read the password but also steal your session cookie and all the data that is shown in your session. In that case, the attacker has already won, regardless of whether they read the password or not.

You could of course roll your own encryption and authentication, but the question is why you'd think that this will be better than the https connection?

The realistic scenario isn't that someone breaks the https connection, it is that someone breaks into the server's database and tries to "recover" the passwords in an offline attack. This is what most authentication schemes try to defend against.

The danger with passwords is not that they are interceptable, but that they can be guessed.

What should you do in real life?

For a "real" web application, use an established framework and a well-known authentication library. Don't write it yourself, because it is easy to get wrong and very hard to get right.

Don't use SHA-anything for hashing the passwords, use a dedicated password-hashing function such as bcrypt or PBKDF2 to make attacks more difficult (see this answer also). You can also use or add some TOTP mechanism for your app, which is better than just a password.

Addendum

You added the following after I wrote my answer:

My requirement is that the only thing users ever need to provide for successful authentication is their username/password and nothing else, regardless of what device they are using or whether they were logged in on that device before.

This explains why you want to send the private key back to the server, although it doesn't change the reason why it is a bad idea.

You could still use a challenge-response mechanism, like ThoriumBR suggested. There are even advanced challenge-repsonse mechanisms such as SRP which use clever math to avoid storing the password on the server.

However, all those mechanisms, and also client-side hashing (see this answer), have one thing in common:

They need to execute code on the client side.

In a web app, this can only be Javascript sent by the server. Implementing the client side is not the problem (there is even a OpenPGP library), but since you worry about the security of the https connection, you have another problem:

Any man in the middle could therefore simply inject malicious client side code to steal the password, rendering your whole clever mechanism obsolete.

averell
  • 1,083
  • 7
  • 10
  • There is [TLS-SRP](https://en.wikipedia.org/wiki/TLS-SRP) which don't depend on Javascript, but [major browsers don't support it](https://stackoverflow.com/questions/2778629/tls-srp-in-browsers). There are some issues with SRP in general: lack of a PBKDF (in some implementations), the lack of weak password checking and, the major problem, the user interface would be either ugly (handled by the browser, like Basic HTTP Auth) or unsafe (handled by the website, which allows spoofing). WebAuthn is getting more traction. – Gustavo Rodrigues Jan 10 '20 at 13:16
  • 1
    Nit: You have to specify a digest function to PBKDF2 (which basically runs it in a loop). I absolutely *would* recommend SHA-n (n > 1) for the digest function. – Martin Bonner supports Monica Jan 10 '20 at 14:02
  • I clearly see your point, so I decided to discard any plans of some fancy homebrew authentication and just moved to bcrypt, which conveniently already has a complete implementation on npm. Do you think a cost factor of 12 (which takes about 200ms on my machine) is sufficient? Thank you for the answer by the way. – Sandtler Jan 11 '20 at 21:23
  • @Sandtler I would recommend the [OWASP Password storage cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) - it currently recommends a work factor of 12 (and adding 1 every 18 months). – averell Jan 11 '20 at 21:56
11

As the others said, private keys aren't shared. full stop. Otherwise, they are called public keys.

But: Cryptographically signing on is pretty standard. HTTPS supports that:

Authentication using HTTPS client certificates; and all modern browsers have the built-in means to generate the necessary keys when asked to.

Your problem has already been solved; the user generates a key pair, gives you its public key, keeps the private key, and upon connection, the server asks the client to prove they own the private key (essentially, by letting them sign something).

Done!

Other cryptographically established methods include at least one method I'm aware of:

Single-sign on via Kerberos ticket – this is extremely wide-spread in intranets that use Active Directory. Again, modern browsers (at least Edge, Internet Explorer, and Firefox) support this out of the box – on both windows and Linux (haven't tried on OS X or FreeBSD)

Marcus Müller
  • 5,843
  • 2
  • 16
  • 27
  • Firefox only does Kerberos after the site has manually been added to a list of sites that you trust for Kerberos (or if your intranet has a deployment tool that configures this for you) for privacy reasons – Erik A Jan 09 '20 at 11:46
  • @ErikA that is not generally correct, it seems. I can use kerberos tickets on all my linux machines for sign on in my university, freshly after I set them up. No external deployment tool necessary. Might depend on the local DNS configuration (i.e. local domain allowed by default, maybe?). – Marcus Müller Jan 09 '20 at 13:43
  • That's odd, it's [documented](https://developer.mozilla.org/en-US/docs/Mozilla/Integrated_authentication) that Firefox rejects all SPNEGO messages by default, which is used to negotiate Kerberos authentication, and afaik there's no exception for the local network. Afaik, Kerberos requires SPNEGO to work over HTTP(S). And for my personal experience, I certainly can't use Kerberos on a blank Firefox install and must configure it as outlined in that document. – Erik A Jan 09 '20 at 14:07
  • For a quick test, I flattened my firefox profile here, and opened the login page, clicked on "Windows Logon" (having run `kinit user@DOMAIN` from my fedora's shell, good naming, guys), and it just workedTM. – Marcus Müller Jan 09 '20 at 14:50
  • @ErikA: The reason is Kerberos forwarding attacks are real things. samba used to operate exclusively in that mode a long time ago. – Joshua Jan 09 '20 at 21:33
9

While I like the idea of PGP for login, this is not the way it's supposed to work. The private key should remain on the client side, always, and never be shared with anyone. This setup encourages bad practices and provide incorrect education about PGP.

What you could do is, have users upload their public key to the server, and then you present them a challenge (basically, an encrypted string) upon login, that requires the private key and of course the associated passphrase..

The problem is that few people know PGP or have a serious inclination to use it. If you can do it, go for it.

Kate
  • 6,967
  • 20
  • 23
  • 6
    "While I like the idea of PGP for login".. why? As far as I can see there's nothing in PGP that's useful for this scenario over just using client certificates for authentication - which is an established standard. – Voo Jan 09 '20 at 12:27
  • isn't sending a public key or any kind of key exchange is vulnerable to man in the middle attacks? which is the main concern of the OP in the first place to bypass his current form of key exchange for another kind! This is addressed in Marcus Muller's answer. – Unicornist Jan 10 '20 at 21:05
5

As Anonymous said, the private key must always be private. Sending the private key to any entity negates any security provided by your login.

Basically, what you are designing is called Challenge Response Authentication, and can be done even without a keypair:

  • One party (the server) generates a random token and sends it to the other

  • The other (the client) concatenates his own password with the token, hashes it, and sends back

  • The server knows both the token and the password, concatenate those values, hash, and compare with what the client sent

If you want to use a keypair, you can use the keypair to sign:

  1. Generate a random value R1 and send to the user

  2. User receives R1, and signs it using its private key

  3. Server checks the signature against user's public key

This is technically easy to do, but I doubt most users will have the necessary know-how to generate a keypair, and use the keypair to sign things.

ThoriumBR
  • 50,648
  • 13
  • 127
  • 142
5

An alternative approach that implements the same concepts is SRP. Ignoring the math in the protocol, the idea is that when a user signs up the client generates a password verifier which is sufficient to verify credentials, but not to act as them and sends it to the server to store.

At login time the parties then exchange messages based on random values that eventually leads to the server using the stored verifier to ensure that the client has the correct password. The messages that the client send to the server are one-way derived from the password and the random value so the password is never sent on the wire. The random message from the server ensures that the messages from the client are valid only for the current login attempt (prevents MITM from being able to replay login attempts if HTTPS fails).

While the protocol is daunting to implement, there are out of the box implementations out there, e.g. AWS Cognito and its client JS SDK, so it is practical to implement in any web app.

erm410
  • 151
  • 1
4

While the scheme described in the question has a significant problem in enabling offline bruteforce attacks against the password, it is possible to redesign it without this vulnerability.

For example, one can generate private key directly from password. That avoids the need of storing the private keys at all - basically the password becomes the private key.

However in a practical web application implementation, you would have to do the cryptography in client-side javascript. And if HTTPS as transport is broken, it is quite likely also to enable compromising that piece of javascript, giving out the password in plaintext.

Such a scheme correctly implemented would be slightly more secure than current standard practice of plaintext over https + hashing on the server side. But without extensive review, it is easy to make subtle but critical mistakes in cryptography code, and effort would be better spent securing other parts of the application.

jpa
  • 951
  • 6
  • 11
  • This seems to be about exactly what I was looking for, thank you. I'll read some articles on how PBKDF2 works in detail and, if it fits my needs, see if there is a mature framework that implements this authentication mechanism as complete as possible in order to keep my own security-critical code to a bare minimum. – Sandtler Jan 09 '20 at 15:34
4

This already exists.

OpenPGP has "authentication" as a possible key usage, and a PGP key can be used to authenticate an SSH session if gpg-agent is used in ssh-agent mode. I use that daily as a single-sign-on solution with a smartcard, but there is no reason why this wouldn't work with a regular key.

There are also various plugins that allow signing messages during a TLS handshake using gpg-agent, allowing users to use their PGP key for SSL client certificate authentication.

As the key never leaves the user's device, it isn't even necessary to generate a new key, users can just enroll with their existing key.

You need enough of a PKI infrastructure on the server side to decide which keys are valid for which users. Most services (e.g. gitlab, github and launchpad) allow you to upload your public key into a form in the user profile editor.

Simon Richter
  • 1,482
  • 11
  • 8
  • You're examples of gitlab and github is for applications that actually use the ssh itself via their clients (git, etc) for the authentication. the OP is not considering this as an option, instead looking for a way to use PGP for web based authentication. – Unicornist Jan 10 '20 at 21:12
  • ... which you already cited that is used inside TLS handshake process itself. So is it correct to consider that sending a plain text pass over HTTPS is already using a kind of PGP authentication mechanism in itself? – Unicornist Jan 10 '20 at 21:13
  • @Unicornist, no, only when the certificate from the TLS session is used as part of the authentication -- which means connecting the browser to gpg-agent. Gitlab/GitHub are examples of how you would enroll users. – Simon Richter Jan 11 '20 at 01:08
0

Can you?

Yes

Should you?

No

Why not?

Don't reinvent the wheel. Unless it's broken... which it is not.

There are plenty of tried-and-true authentication methods that don't involve the transmission of private keys, which should ALWAYS stay with the user. I would recommend against using PGP call-response as outlined by @ThoriumBR as the main authentication method (though it has uses which I describe later).

What you should do

Stick to dedicated libraries and frameworks for authentication such as the password-hashing functions (bcrypt, PBKDF2, TOTP) that @averell posted in their answer. If throughout the user-experience of your website/app PGP key encryption is required, only use it when necessary.

For example, messaging between users can easily be achieved without storing any private keys on the server by letting (or requiring) users upload their public PGP key to the site, allowing others to sign with that key. The user can decrypt using the private key stored on their device. It is a trivial matter to copy your private keys to whatever device you'll be using them on.

Thanks to OpenPGP many libraries have been created to facilitate what you're trying to do including the OpenPGP.js JavaScript library.

PGP for 2FA

Since PGP is an otherwise integral part of your application anyway, you can add a layer of security to logins by using PGP as 2FA for the account. You can also use the challenge-response

  1. Client signs in with trusted framework for authentication.
  2. Client adds their public PGP key to their account.
  3. Server authentication workflow contains the public PGP key as a variable of whatever framework you use.
  4. Server generates a 2FA string (short numerical, long message, the content is not important) which it then signs with the public PGP key associated with the user sending the authentication request.
  5. User decrypts the message client-side with their private PGP key, obtaining the encrypted message necessary to fill in the 2FA form.

Note that under no circumstance does the server know the private key. This is hugely important for security. The private key stays with the client, always. As for the public key, add it anywhere that's necessary, there's no security risk to storing the public key on the server. One could argue that fingerprinting a user between applications/sites using their public key is at the very least a privacy risk, one which can be avoided by using different keys for different apps. This is an edge case though and is not problematic unless a user is using the same public key on a site/app they'd rather not be associated with publicly.