str4d / age-plugin-yubikey

YubiKey plugin for age
Apache License 2.0
600 stars 26 forks source link

FR: Put the same age key on multiple Yubikeys #75

Open orolhawion opened 2 years ago

orolhawion commented 2 years ago

For backup reasons I would like to be able to put an age private key on more than one Yubikey so in case one Yubikey breaks, gets lost, is unusable in some other way, I would have another key that could decrypt my files. I might have overlooked such a feature, if so, please let me know how to do it.

mihaigalos commented 2 years ago

This will list all identities for plugged in YubiKeys:

$ age-plugin-yubikey --identity > identities

You can then encrypt to multiple identities:

$ identities=$(cat identities | grep Recipient | sed -e "s/ //g" | cut -d':' -f2 | sed -e 's/^age\(.*\)/ -r age\1/g'  | tr -d '\n')
$ rage $identities -e input.file -o output.file.age

Then decrypt to a single identitiy (a single plugged in YubiKey):

$ age-plugin-yubikey --identity > identity 2>/dev/null
$ cat $secret_file | rage -d -i identity

Have a look at how I've done it in pass for more details.

mihaigalos commented 2 years ago

BTW: the actual keys stored on the Yubikeys are still different^.

orolhawion commented 2 years ago

ok, so you mean private age keys should be different and I should encrypt to all of them, which in fact makes this topic closable. thanks for the heads up. :)

Merovius commented 2 years ago

I am interested in the same thing for the same reason. I'd prefer having the same secret key on all my YubiKeys, so that if I have to replace one, I don't have to go around re-encrypting everything.

I understand the tradeoffs involved and I understand why people prefer to have separate private keys per hardware token. But I'd like the option to use an offline generated key.

mihaigalos commented 2 years ago

My 2 cents: I understand your point. Not sure if it's technically possible.

I believe a Yubikey-unique part (possibly hardware-coded) is involved in the process of generating an identity.

It is therefore unique and if you lose the Yubikey, you are left with whatever others you used to encrypt. You will need to decrypt and reeencrypt to the list of Yubis you still possess.

Would like it if somebody could confirm/refute this^.

orolhawion commented 2 years ago

I usually put my gpg identity on two different yubikeys, as stated for backup reasons. So I thought this should be possible with age keys also.

mihaigalos commented 2 years ago

@str4d ?

kmille commented 1 year ago

Another way to achieve this would be by generating the private key on an "unsafe device" and then importing it to the YubiKey(s). Is there a way/tutorial how to import private keys to the Yubikey?

Merovius commented 1 year ago

@kmille It's easy to do that using ykman, but age-plugin-yubikey refuses to use keys which have not been generated on the Yubikey.

pinpox commented 1 year ago

@kmille It's easy to do that using ykman, but age-plugin-yubikey refuses to use keys which have not been generated on the Yubikey.

Is there a technical reason/limitation behind this or could that be changed on age-plugin-yubikey's side? I'm used to the workflow as decribed by @orolhawion and @Merovius from gpg. I have also generated my gpg keys on an airgapped laptop and copied the keys to multiple yubikeys and also printed them out on paper to be stored safely as last-resort backup.

I would love to move from gpg to age for all encrypting purpouses, is there any chance we could allow keys not generated on the yubikey itself and copied via ykman?

This would also solve https://github.com/str4d/age-plugin-yubikey/pull/3 Also, if you don't want to use this approach or disallow them I guess it could just be configurable whether you want to allow it?

pinpox commented 1 year ago

@mihaigalos I have a key and can help test this but need some guidance. How do you generate a key in the correct format to be imported? @Merovius how did you generate your key?

pinpox commented 1 year ago

I'm not sure how to import that key. I generated one like shown in the screenshot and saved the two strings to files:

shell ❯ cat secret-key
0c 06 41 0d 17 db 68 5a 84 47 e7 e9 d7 a5 2f 2f

shell ❯ cat private-identity
22 f5 0a 2e 1a db

I then tried to import them into my testing yubikey:

shell ❯ ykman piv keys import 82 ./secret-key 
ERROR: Could not parse private key.

I suppose that is not the correct format that ykman exports for importing? Can you give me directions on how to test this?

I also have tried removing the spaces.

pinpox commented 1 year ago

I found some information regarding this. Citing from: https://smallstep.com/blog/access-your-homelab-anywhere/

It confirms that the private key is hardware-bound and non-exportable.

Crucially, the YubiKey will only attest a private key that's been generated directly on the YubiKey. If you try to attest an imported private key, it will fail:

Could this be the cause?

orolhawion commented 1 year ago

I don't quite get how you get from age keys to Yubico OTPs...

Merovius commented 1 year ago

You can generate a private key with a tool of your choice, for example:

openssl genpkey -algorithm EC -out private.key -pkeyopt ec_paramgen_curve:P-384 -pkeyopt ec_param_enc:named_curve

You can then import it into an arbitrary slot of your YubiKey using ykman. For example, to install it into the Key Management Slot (which is used to encrypt E-Mails, so it seems an appropriate slot to use for age):

$ ykman piv keys import 9d private.key
$ ykman piv keys info 9d
Key slot:               9D (KEY_MANAGEMENT)              
Algorithm:              ECCP384
Origin:                 IMPORTED
PIN required for use:   ONCE
Touch required for use: NEVER

Going from there to actually getting age-plugin-yubikey to use that key is the step I'm failing at - but in theory, all you should need to generate an age identity is the public key, which you can derive from the private key. And all you need to decrypt a message is to use PIV to get the YubiKey to decrypt it for you.

So I'd expect age-plugin-yubikey to be able to 1. output an identity for any private key stored in any key slot on the YubiKey and 2. if asked to decrypt a message for a given public key, iterate through the key slots and retrieve their public keys, to find the required one (for example using the Get metadata PIV command) and then 3. use that slot to decrypt the message.

The documentation implies that only the retired key slots are used. But even if I store a private key into slot 82 (using the above procedure) it is not listed by age-plugin-yubikey --list-all, which leads me to believe that age-plugin-yubikey simply does not support any of this.

kagehisa commented 9 months ago

The documentation implies that only the retired key slots are used. But even if I store a private key into slot 82 (using the above procedure) it is not listed by age-plugin-yubikey --list-all, which leads me to believe that age-plugin-yubikey simply does not support any of this.

My Rust skills are limited at best but as far as I can see, there is a certificate check whenever a key is read from a slot. So I guess (emphasis on GUESS) is that keys generated with age-plugin-yubikey are signed or identified with a certeain certificate, in order to make sure this is not a key used by any other PIV service.

pinpox commented 7 months ago

that keys generated with age-plugin-yubikey are signed or identified with a certeain certificate

@str4d Can you confirm this?

If true, would you be able to make that optional or disable it alltogether so we can use exteranally generated age keys? This issue is truely the only thing keeping me with GPG, I would love to find a solution on how to use my own key (which I can have a backup of) on the yubikey for everything

Ra2-IFV commented 4 weeks ago

did you guys try this...

$ openssl ecparam -name prime256v1 -genkey -noout -out test0001.key.pem

$ openssl req -new -key test0001.key.pem -x509 -nodes -out test0001.cert.pem -days 3650 -subj "/CN=age identity test0001/OU=0.5.0/O=age-plugin-yubikey"

$ ykman piv certificates import 82 --pin-policy ALWAYS --touch-policy CACHED test0001.cert.pem
Enter PIN: 
Certificate imported into slot RETIRED1
$ ykman piv keys import 82 test0001.key.pem
Enter PIN: 
Private key imported into slot RETIRED1.
$ age-plugin-yubikey -l

#       Serial: 11451419, Slot: 1
#         Name: age identity test0001
#      Created: Thu, 1 Jan 1970 00:00:00 +0000
#   PIN policy: Unknown (yes it's unknown here)
# Touch policy: Unknown (also unknown)
*HERE IS THE RECIPIENT*

$ age-plugin-yubikey --identity --slot 1 > ident
Recipient: *HERE IS THE RECIPIENT*
$ RECIPIENT='*HERE IS THE RECIPIENT*'
$ echo foobar | rage -e -r $RECIPIENT | rage -d -i ident 

foobar
Ra2-IFV commented 4 weeks ago

Move further: write this to age-plugin-yubikey.cnf

[ req ]
distinguished_name        = req_distinguished_name
x509_extensions           = v3_age-plugin-yubikey
prompt = no

[ req_distinguished_name ]
0.organizationName        = age-plugin-yubikey
organizationalUnitName    = 0.5.0
commonName                = age identity test0001

[ v3_age-plugin-yubikey ]
subjectKeyIdentifier      = none
1.3.6.1.4.1.41482.3.8     = DER:0101

#            PinPolicy::Default => 0,
#            PinPolicy::Never => 1,
#            PinPolicy::Once => 2,
#            PinPolicy::Always => 3,

#            TouchPolicy::Default => 0,
#            TouchPolicy::Never => 1,
#            TouchPolicy::Always => 2,
#            TouchPolicy::Cached => 3,

Where the 2nd bit of DER:0101 represents PinPolicy, 4th bit represents TouchPolicy. (e.g 0102 means PinPolicy is Never, TouchPolicy is Always), then create CSR and the certificate with:

openssl req -config age.cnf -new -key test0001.key.pem -out test0001.csr
openssl x509 -req -sha256 -days 3650 -in test0001.crt -signkey test0001.key.pem -out test0001.cert.pem

Note: Use -days to set expiration date, if you're using OpenSSL 3.4 you can use the -not_before today -not_after 99991231235959Z option.

And import it, then check the output of age-plugin-yubikey:

ykman piv certificates import 82 test0001.cert.pem
ykman piv keys import 82 --pin-policy NEVER --touch-policy ALWAYS test0001.key.pem
$ age-plugin-yubikey -l
#       Serial: 11451419, Slot: 1
#         Name: age identity test0001
#      Created: Thu, 1 Jan 1970 00:00:00 +0000
#   PIN policy: Never  (A PIN is NOT required to decrypt)
# Touch policy: Always (A physical touch is required for every decryption)
age1yubikey1blahblahblahblah

Yes, age-plugin-yubikey cannot read the policies directly, so it use extensions to remind itself. Remember to set the SAME policies when IMPORTING the PRIVATE key!

I may write a simple shell/powershell script if someone needs it, so you don't have to type complicated commands to finally being able import your private key to your yubikeys, before the developers implement it.