Восстанавливаем приватный ключ SSH из GnuPG

Жил я долго и счастливо с 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/ за что ему честь и хвала. В итоге правильный вариант такой:

  1. Достаем любыми способами бекап gnupg, сделанный ДО перемещения ключа в yubikey. Если его у вас нет, то всё: ключ вы не вытащите.
  2. Кладем его куда-нить рядом, но не в родной каталог. У меня это страшно оригинальный каталог с именем “2”
  3. Говорим gnupg, где искать данные: export GNUPGHOME="2/.gnupg/"
  4. Смотрим, какие ключи он видит: gpg --list-secret-keys --with-keygrip
  5. Пробуем посмотреть, что на диске
$ 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 проглатывает его совершенно без каких-либо стеснений. Теперь осталось засунуть его куда надо и подчистить за собой ошметки и незашифрованные ключи.