|
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) |