a proof of concept for a multi-factor authentication system using PGP, possible alternative to TOTP (RFC 6238).
$ go build -v -o pgp-mfa
$ ./pgp-mfa import-key <key-file> # armored / binary format supported, - for stdin
$ gpg --export <key-id> | ./pgp-mfa import-key - # import from stdin
$ ./pgp-mfa challenge <length> [key-id] # if no key-id is provided, you'll be prompted to select one
the idea is not to replace RFC 6238, or any other MFA system, but to provide an alternative that could be used in production.
- still works offline
- not time based: it is not necessary to have a clock on the device, nor for it to be in sync with current real time.
- no shared secrets: TOTP relies on a shared secret between the server and the client, which could be compromised, by using PGP, only the public key is shared.
- emailable challenges: PGP keys contains an email address, the server could use this information to send the challenge to the user by email.
- benefits from expirability: PGP keys can expire, allowing a 0 interaction self-destruction of the mean of access to the account.
- the server generates a random string, and encrypts it with the public key of the user.
- the encrypted message is sent to the user.
- user has to decrypt the message using their private key (and passphrase if one is set).
- the server checks whether the decrypted message is the same one as the one originally sent
- (optional) the server can check whether the challenge has expired, and reject the solution if it has.
run benchmark with go test -bench=.
and see the results. uses go's crypto/rand package to generate random bytes.
test name | description |
---|---|
Ed25519_16 | uses a public ed25519 key to encrypt / decrypt a 16 bytes challenge |
Ed25519_32 | 32 bytes challenge |
Ed25519_64 | 64 bytes challenge |
Ed25519_128 | 128 bytes challenge |
Ed25519_256 | 256 bytes challenge |
Ed25519_512 | 512 bytes challenge |
Rsa3072-16 | uses a public rsa3072 key to encrypt / decrypt a 16 bytes challenge |
Rsa3072-32 | 32 bytes challenge |
Rsa3072-64 | 64 bytes challenge |
Rsa3072-128 | 128 bytes challenge |
Rsa3072-256 | 256 bytes challenge |
Rsa4092-16 | uses a public rsa4092 key to encrypt / decrypt a 16 bytes challenge |
Rsa4092-32 | 32 bytes challenge |
Rsa4092-64 | 64 bytes challenge |
Rsa4092-128 | 128 bytes challenge |
Rsa4092-256 | 256 bytes challenge |
ChallengesGeneration_16 | measures how fast can the machine can generate challenges of 16 bytes |
ChallengesGeneration_32 | 32 bytes |
ChallengesGeneration_64 | 64 bytes |
ChallengesGeneration_128 | 128 bytes |
ChallengesGeneration_256 | 256 bytes |
ChallengesGeneration_512 | 512 bytes |
goos: linux
goarch: amd64
pkg: github.com/quintessence-sec/pgp-mfa
cpu: AMD Ryzen 9 7950X 16-Core Processor
BenchmarkEd25519_16-32 27735 42647 ns/op
BenchmarkEd25519_32-32 27698 43268 ns/op
BenchmarkEd25519_64-32 27820 42442 ns/op
BenchmarkEd25519_128-32 28976 41584 ns/op
BenchmarkEd25519_256-32 28774 42483 ns/op
BenchmarkEd25519_512-32 27616 43069 ns/op
BenchmarkRsa4092_16-32 6246 178434 ns/op
BenchmarkRsa4092_32-32 6500 181093 ns/op
BenchmarkRsa4092_64-32 6232 182861 ns/op
BenchmarkRsa4092_128-32 6586 180314 ns/op
BenchmarkRsa4092_256-32 6598 181688 ns/op
BenchmarkRsa4092_512-32 6369 181804 ns/op
BenchmarkRsa3072_16-32 10000 109607 ns/op
BenchmarkRsa3072_32-32 10000 110057 ns/op
BenchmarkRsa3072_64-32 10000 110457 ns/op
BenchmarkRsa3072_128-32 10000 109201 ns/op
BenchmarkRsa3072_256-32 10000 110007 ns/op
BenchmarkRsa3072_512-32 9853 111029 ns/op
BenchmarkChallengesGeneration_16-32 3557168 334.9 ns/op
BenchmarkChallengesGeneration_32-32 3463393 345.9 ns/op
BenchmarkChallengesGeneration_64-32 2635272 446.3 ns/op
BenchmarkChallengesGeneration_128-32 1996668 605.8 ns/op
BenchmarkChallengesGeneration_256-32 1361889 874.7 ns/op
BenchmarkChallengesGeneration_512-32 730452 1443 ns/op
PASS
ok github.com/quintessence-sec/pgp-mfa 36.111s
according to the results, we can deduce that the most optimal configuration is to use an ed25519 key, with a challenge length of 128 bytes.
the charset of challenges is, by default: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+/\'"!@#$%^&*()[]{}<>?,.;:
, which is a total of 90 characters.
if we consider an ed25519 key we can process 128 bytes in 41.584 microseconds (≈ 3.078 MB/s ~ 24046.875 attempts/s).
challenge length | number of possible solutions | probability of 1st try success | number of years to brute-force |
---|---|---|---|
16 bytes | 1.853E+31 | 5.397E-30% | 7.706E+26 |
32 bytes | 3.434E+62 | 2.912E-61% | 1.428E+58 |
64 bytes | 1.179E+125 | 8.482E-124% | 4.903E+120 |
128 bytes | 1.390E+250 | 7.194E-249% | 5.781E+245 |
256 bytes | 1.932E+500 | 5.175E-499% | 8.036E+495 |
512 bytes | 3.734E+1000 | 2.678E-999% | 1.553E+996 |
this is, of course, assuming that:
- the randomness of the challenge is perfectly random
- the brute-force attacker has no access to the server
- there are no delays between retries, and there are no maximum number of attempts
- the challenge does not expire