Skip to content

Instantly share code, notes, and snippets.

@dlubawy
Last active November 13, 2025 10:16
Show Gist options
  • Select an option

  • Save dlubawy/dba7bac090fa6b23dd69979b43103cc6 to your computer and use it in GitHub Desktop.

Select an option

Save dlubawy/dba7bac090fa6b23dd69979b43103cc6 to your computer and use it in GitHub Desktop.
Convert age identity to PEM with self-signed certificate

Age to PEM

Converts an age identity key to a PEM file with a self-signed X.509 certificate using a random RSA key.

Requirements

  • Requires YubiKey Manager installed to import the key/cert to your YubiKey
  • pip install bech32 cryptography

Usage

Existing Key

  1. python age-to-pem.py -f ./age-identity.txt
  2. ykman piv keys import SLOT ./X25519.pem
  3. ykman piv certificates import SLOT certificate.pem

New Key Generation

  1. age-keygen | python age-to-pem.py
  2. ykman piv keys import SLOT ./X25519.pem
  3. ykman piv certificates import SLOT certificate.pem
import argparse
import datetime
import fileinput
import getpass
import sys
from base64 import b32decode
import bech32
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, x25519
from cryptography.x509.oid import NameOID
BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
def self_sign(private_key, output="./certificate.pem"):
signing_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
subject = issuer = x509.Name(
[
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "age-plugin-yubikey"),
x509.NameAttribute(NameOID.COMMON_NAME, "YUBIKEY"),
]
)
custom_oid = x509.ObjectIdentifier("1.3.6.1.4.1.41482.3.8")
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
.not_valid_after(datetime.datetime.max)
.add_extension(
x509.SubjectAlternativeName([x509.DNSName("localhost")]),
critical=False,
)
.add_extension(
x509.UnrecognizedExtension(custom_oid, bytes([0x02, 0x02])),
critical=False,
)
.sign(signing_key, hashes.SHA256())
)
# Write our certificate out to disk.
with open(output, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
def main(filename=None, output=None, password=None):
(name, data) = (None, None)
if not filename or filename == "-":
for line in fileinput.input():
if line[0] != "#":
(name, data) = bech32.bech32_decode(line.rstrip())
break
else:
with open(filename, "r") as f:
for line in f.readlines():
if line[0] != "#":
(name, data) = bech32.bech32_decode(line.rstrip())
break
output = output if output else "./X25519.pem"
if (name, data) == (None, None):
print(f"Failed to parse bech32: {line}")
sys.exit(1)
b32str = "".join(BASE32_ALPHABET[num] for num in data)
b32str += "=" * (8 - (len(b32str) % 8))
decoded = b32decode(b32str)
private_key = x25519.X25519PrivateKey.from_private_bytes(decoded)
if not password:
password = getpass.getpass()
while password != getpass.getpass(prompt="Confirm password: "):
print("Try again.")
password = getpass.getpass().encode(encoding="utf-8")
with open(output, "wb") as f:
f.write(
private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.BestAvailableEncryption(
password.encode(encoding="utf-8")
),
)
)
self_sign(private_key)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Parse age identity to PEM.")
parser.add_argument("-f", "--filename", help="age identity file")
parser.add_argument("-o", "--output", help="PEM output filename")
parser.add_argument(
"-p", "--password", help="Plaintext password to protect private key"
)
args = parser.parse_args()
main(args.filename, args.output, args.password)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment