Closed Mansarde closed 4 years ago
Yes it means its possible. Then you should be able to reimport your key in gnupg
@Mansarde did you take it any further? I am interested in this kind of tool, too
Sorry @bereska, I don't have the expertise (nor time unfortunately) to create such a tool. I just wanted to know if it was possible in principle, so I can sleep more easy.^^ As my question has already been answered ( thx @cslashm ), I guess I should close this.
But if anyone has the knowhow, time and willingness to create such a tool, then I would be very delighted if they could let me know! 😊
@Mansarde no problem, thank you
Hey, :) Does any of you, especially @cslashm has any idea how to achieve this? So to get plain gpg private key from pickled backup, given we have the seed, PIN, and naturally the Ledger itself at hand.
Or perhaps the other way around, we have seed, PIN, etc, and we want to generate Plain GPG private key.
@aryasenna @bereska
Hereafter is the way:
The key data is retrieved here: https://github.com/LedgerHQ/openpgp-card-app/blob/master/src/gpg_data.c#L701 So the private part is encrypted with AES_CBC with ISO9797/M2 (0x80... padding)
The AES key is derived here : https://github.com/LedgerHQ/openpgp-card-app/blob/master/src/gpg_data.c#L691
If you follow the calls you will retrieve the key derivation method from Seed. IIRC and correctly read the code it gives:
seed = bip32_derive(curve = CX_CURVE_SECP256K1, path=/0x80475047/slot+1)
h = sha256(seed | "key " | 0x0001)
aes_key = shake256(data=h, len=16)
So you will be able to decrypt the private key part
Is it clear? :)
Ty @cslashm! :)
I'm reading the codes as we speak. And speaking of following the function call:
it stops at os_perso_derive_node_bip32()
, I assume this is a BOLOS API.
1.) Can BOLOS implementation be assumed to be identical to Python Library https://pypi.org/project/bip32/ and/or https://pypi.org/project/bip-utils/ ?
I'm looking at Bip32.FromSeedAndPath()
2.) In your pseudo-code, where is the actual Ledger seed being used in the derivation?
seed = bip32_derive(curve = CX_CURVE_SECP256K1, path=/0x80475047/slot+1)
h = sha256(seed | "key " | 0x0001)
aes_key = shake256(data=h, len=16)
Is this what you meant: https://github.com/LedgerHQ/openpgp-card-app/blob/64662c181f4c906288564cbfadc2db53df4534b0/src/gpg_gen.c#L32
3.) In your excellent write-up here (but I'm not sure it is still valid), it goes into details like length being half of keysize for the SHA3-XOF(SHA256()
(that is basicaly SHAKE, right?).
aes_key = shake256(data=h, len=16)
why is this one hardcoded to 16?
To be honest, I'm a bit at lost where to start 😆.
I'm thinking to start from the gpgcard.py
, there I can get the encrypted private key either by loading my pickled backup, or reading it directly from my Ledger.
https://github.com/LedgerHQ/openpgp-card-app/blob/64662c181f4c906288564cbfadc2db53df4534b0/pytools/gpgcard/gpgcard.py#L327-L336
And then proceeding to decrypt each key from that point. Assuming I successfully obtained the AES key for each one of them.
1.) Can BOLOS implementation be assumed to be identical to Python Library https://pypi.org/project/bip32/ and/or https://pypi.org/project/bip-utils/ ?
Yes it should :)
2.) In your pseudo-code, where is the actual Ledger seed being used in the derivation?
seed = bip32_derive(curve = CX_CURVE_SECP256K1, path=/0x80475047/slot+1) h = sha256(seed | "key " | 0x0001) aes_key = shake256(data=h, len=16)
Is this what you meant: https://github.com/LedgerHQ/openpgp-card-app/blob/64662c181f4c906288564cbfadc2db53df4534b0/src/gpg_gen.c#L32
this is the seed computation part (step 1)
step 2 starts here: https://github.com/LedgerHQ/openpgp-card-app/blob/64662c181f4c906288564cbfadc2db53df4534b0/src/gpg_gen.c#L50
and step3 starts here: https://github.com/LedgerHQ/openpgp-card-app/blob/64662c181f4c906288564cbfadc2db53df4534b0/src/gpg_gen.c#L54
3.) In your excellent write-up here (but I'm not sure it is still valid), it goes into details like length being half of keysize for the
SHA3-XOF(SHA256()
(that is basicaly SHAKE, right?).
SHA3-XOF is shake256 in our case
aes_key = shake256(data=h, len=16) why is this one hardcoded to 16?
because AES is always 16 bytes long. the shake256 function generate arbitrary length byte from its input
To be honest, I'm a bit at lost where to start .
The best way (IMHO) is indeed with starting with gpgcard.py
gpgcard.sig_key
,gpgcard.dec_key
, gpgcard.auth_key
For the bip32 derivation you may have a look at https://pypi.org/project/bip32/ or https://pypi.org/project/bip-utils/
Ty man, that is very helpful.
Now, I was just trying to run gpgcard.get_all()
. Just my luck somehow all private_0x
and xxx_key
variable are all empty?
from gpgcard import GPGCard
gpgcard = GPGCard()
gpgcard.connect("pcsc:Ledger")
gpgcard.get_all()
print(gpgcard.token)
print(gpgcard.login)
print(gpgcard.slot)
print(gpgcard.dec_key)
print(gpgcard.sig_key)
print(gpgcard.aut_key)
Output
$ python ./test.py
Ledger Nano X [Nano X] (0001) 00 00
b'aryasenna'
b'\x00'
b''
b''
b''
ALL other attributes are filled though, like lang, url, slot, url, etc. :/ Ofc there is private key inside the slot # 0
gpg --card-status
Reader ...........: Ledger Nano X [Nano X] (0001) 00 00
Application ID ...: D2760001XXXXXXXXXXXXXXXXXXX
Application type .: OpenPGP
Version ..........: 3.3
Manufacturer .....: unknown
Serial number ....: 7BXXXXXX
Name of cardholder: Arya Senna
Language prefs ...: en
Salutation .......: Mr.
URL of public key : https://keybase.io/aryasenna/pgp_keys.asc
Login data .......: aryasenna
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 12 12 12
PIN retry counter : 3 3 3
Signature counter : 3
Signature key ....: F16D BD99 AE4F XXXXXXXX
created ....: 2011-08-21 06:28:09
Encryption key....: 895A FF7D 9702 XXXXXXXX
Authentication key: 127B 5524 075F XXXXXXXX
General key info..: pub rsa2048/7D52D8D6F7FD1587 2011-08-21 Arya Senna (Personal 2) <blah@nospam.com>
sec> rsa2048/7D52D8D6F7FD1587 created: 2011-08-21 expires: never
card-no: 2XXX 7BXXXXXX
ssb> rsa2048/5625946BE26309B9 created: 2020-11-22 expires: 2022-11-22
card-no: 22XXX 7BXXXXXX
ssb> rsa2048/D09A2AA8F8E86CC7 created: 2020-11-22 expires: 2022-11-22
card-no: 2XXX 7BXXXXXX
I can decrypt, sign, and ssh auth just fine from slot # 0. 😕
Hi again @cslashm
I just skipped getting the data directly from my Ledger and instead read my pickled backup, so anyway I got all encrypted sig_key
, dec_key
, and auth_key
Few questions if you don't mind:
sig_key
, dec_key
, and auth_key
. Is this the IV or Ledger firmware specific padding?import hashlib
from Crypto.Hash import SHAKE256
from Crypto.Cipher import AES
seed = b"my extended private key derived from bip32 using correct path"
def bytes_bw_or(a, b) :
return bytes(x | y for x, y in zip(a, b))
h = hashlib.sha256( bytes_bw_or ( bytes_bw_or(seed, 'sig'.encode()) , bytes([1]))).digest()
shake = SHAKE256.new()
shake.update(h)
aes_key = shake.read(16)
0x80475047/slot+1
translate into BIP32 seed format of "m/0'/1'/2"
? Do I use extended private key as seed for SHA256 hashing? My naive guess would be "m/2152157255'/2")
for the first slot.2. Each key loaded from my pickled backup is of 1064 bytes length, what is this, why is it not multiple of 16? I actually noticed the first 24 bytes are identical for all
sig_key
,dec_key
, andauth_key
. Is this the IV or Ledger firmware specific padding?
Just wanted to add that when you remove these first 24 bytes (that seem identical for all the keys) from the 1064 bytes, then you'll get 1040 bytes, and that is a multiple of 16. Not sure if that helps as I am out of my depth regarding the rest, but maybe it illuminates something for you. ¯\_(ツ)_/¯
Does this feature have any implications for extracting the device's master private key (seed phrase)? I'm trying to understand if the GPG keys are just derived from that seed (with a BIP32 hardened derivation) which would suggest no -- but it's funny to see a process where extracting private keys from Ledger is possible.
Some time ago I created a backup of the GPG keys via:
python3 -m gpgcard.gpgcli --pinpad --backup --backup-keys
This created the filegpg_backup_slot1.pickle
I verified that restoring that pickle file into the Ledger works, which means the pickle file does indeed contain my private keys. I'm assuming though that they're in an encrypted format and will only get decrypted/readable during the restore within the Ledger itself, because importing them on a Ledger with a different seed won't work, right?
My question: Would it be possible, having all necessary knowledge (Ledger seed, all PINs, etc.), to write a tool that can extract/decrypt the GPG private keys from my pickle file? I'm curious if this is theoretically possible, because I would like to create a backup of my Ledger's private GPG keys that can be imported on a normal computer using only GPG.