Жил я долго и счастливо с ssh, где приватный ключ хранился в Yubikey и GnuPG (https://blog.kiltum.tech/2025/03/04/yubikey-ssh-final/). Все было хорошо до тех пор, пока мне не потребовалось засунуть приватный ключ в другую систему, которая тупа и вообще ничего другого не принимает как класс.
“Ну фигня вопрос, счас загуглю и быстренько сделаю”. А вот фиг по всей моей физиономии. Все рецепты, что предлагал гугл вместе с нейронками – абсолютно не рабочие. По одной простой причине: они все расчитаны на то, что ключи будут в формате RSA. А у меня-то ed25519. В итоге то у gpg нет такого ключика, то формат не туда, то еще какая фигня на постном масле.
(тут пропущено Н матов и Н часов поиска решения). Помог https://blog.mattcen.com/2025/05/21/recovering-an-ssh-key-from-gnupg/ за что ему честь и хвала. В итоге правильный вариант такой:
- Достаем любыми способами бекап gnupg, сделанный ДО перемещения ключа в yubikey. Если его у вас нет, то всё: ключ вы не вытащите.
- Кладем его куда-нить рядом, но не в родной каталог. У меня это страшно оригинальный каталог с именем “2”
- Говорим gnupg, где искать данные:
export GNUPGHOME="2/.gnupg/"
- Смотрим, какие ключи он видит:
gpg --list-secret-keys --with-keygrip
- Пробуем посмотреть, что на диске
$ cat 2/.gnupg/private-keys-v1.d/AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317.key
Created: 20250304T080555
Key: (protected-private-key (ecc (curve Ed25519)(flags eddsa)(q
#40898E1AC0E50C6876EFF81E3B66007FE94771000036A8E3012BCD86A6340E9D47#)
(protected openpgp-s2k3-ocb-aes ((sha1 #BDF41AEC411DB9B2#
"43018240")#E4DEADEBE62D9A25E2D00#)#2B2A0B522BEFFA464304F71E097277
C16D5E0ADC32E6A6667506BCD017CA75AE703169A725FF87F92A58864013A1A4DFC25D
FBCE2154DAF45B347203#)(protected-at "20250304T080559")))
Очень похоже на правду, поэтому командуем разшифровать этот файл
$ echo 'PASSWD AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317' | gpg-connect-agent
S KEYGRIP AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317
S CACHE_NONCE DF9204BEA240BA3CD2986C2E
OK
Оно спросит пароль, потом новый (оставьте пустым) и еще раз “уверены ли мы”. И вот теперь мы его обнаруживаем расшифрованным (я данные заменил на Х)
$ cat 2/.gnupg/private-keys-v1.d/AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317.key
Created: 20250304T080555
Key: (private-key (ecc (curve Ed25519)(flags eddsa)(q
#{{{XXXXX}}}#)
(d #XXXXXX#)
))
Теперь честно копипастим скрипт, меняя пути
#!/usr/bin/env python
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "cryptography",
# ]
# ///
import os
import pathlib
import sys
import cryptography.hazmat.primitives.serialization
import cryptography.hazmat.primitives.asymmetric
keygrip = sys.argv[1]
gpg_file = pathlib.Path("~/2/.gnupg/private-keys-v1.d/").expanduser() / f"{keygrip}.key"
with open(gpg_file, "r") as f:
gpg_data = f.read()
# Rather than elegantly parse the S-expression in the .key file, split it on
# the hashes which surround the key material, and pull out the public (q) and
# private (d) parts.
(_, gpg_q, _, gpg_d, _) = gpg_data.split("#")
gpg_d = bytes.fromhex(gpg_d)
d = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(
gpg_d
)
private_bytes = d.private_bytes(
encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM,
format=cryptography.hazmat.primitives.serialization.PrivateFormat.OpenSSH,
encryption_algorithm=cryptography.hazmat.primitives.serialization.NoEncryption(),
)
os.umask(0o0077)
ssh_key_file = pathlib.Path("~/.ssh").expanduser() / f"id_ed25519.{keygrip}"
with open(ssh_key_file, "w") as f:
f.write(private_bytes.decode())
print(f"SSH key written to {ssh_key_file}")
И пущаем его (как ставить модули в питон, если у вас их нет – в гугл)
python3 t.py AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317
SSH key written to /Users/vvkaloshin/.ssh/id_ed25519.AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317
Успешный успех! Я проверил: ключик действительно тот и ssh -i
проглатывает его совершенно без каких-либо стеснений. Теперь осталось засунуть его куда надо и подчистить за собой ошметки и незашифрованные ключи.