Files
mtik-cert-pusher/pkcs12_export.py
2026-03-17 17:30:08 -06:00

117 lines
3.8 KiB
Python

#!python3
# dlitz 2025
import fcntl
import os
import re
import subprocess
from contextlib import ExitStack, contextmanager
class PKCS12Exporter:
openssl_prog = "openssl"
def export_pkcs12(self, privkey_data, cert_data, chain_data, passphrase):
assert re.search(r"^-----BEGIN (.*) PRIVATE KEY-----\n", privkey_data, re.M)
assert re.search(r"^-----BEGIN CERTIFICATE-----\n", cert_data, re.M)
assert "PRIVATE KEY" not in cert_data
assert chain_data is None or "PRIVATE KEY" not in chain_data
fullchain_data = cert_data + "\n" + (chain_data or "") + "\n"
all_data = privkey_data + "\n" + fullchain_data
with self.pipe_with_buffered_data(passphrase, text=True) as passphrase_r:
cmd = [
self.openssl_prog,
"pkcs12",
"-export",
"-passout",
f"fd:{passphrase_r.fileno():d}",
# "-macalg", "SHA256",
# "-keypbe", "AES-256-CBC",
# "-certpbe", "NONE",
]
return subprocess.check_output(
cmd, pass_fds=[passphrase_r.fileno()], input=all_data.encode()
)
def open_pipes(self, *, text=False, encoding=None, errors=None):
rfd, wfd = os.pipe()
rfile = wfile = None
try:
rfile = open(rfd, "r" if text else "rb", encoding=encoding, errors=errors)
wfile = open(wfd, "w" if text else "wb", encoding=encoding, errors=errors)
return rfile, wfile
except:
if rfile is not None:
rfile.close()
else:
os.close(rfd)
if wfile is not None:
wfile.close()
else:
os.close(wfd)
raise
def pipe_with_buffered_data(self, data, *, text=False, encoding=None, errors=None):
# Open a pipe and place a small string into its buffer, then return a file
# open for reading the pipe.
rfile, wfile = self.open_pipes(text=text)
try:
if text:
bdata = data.encode(encoding=wfile.encoding, errors=wfile.errors)
else:
bdata = data
pipe_buffer_size = fcntl.fcntl(wfile.fileno(), fcntl.F_GETPIPE_SZ)
if pipe_buffer_size < len(bdata):
pipe_buffer_size = fcntl.fcntl(
wfile.fileno(), fcntl.F_SETPIPE_SZ, len(bdata)
)
assert pipe_buffer_size >= len(bdata)
wfile.write(data)
wfile.close()
return rfile
except:
wfile.close()
rfile.close()
raise
if __name__ == '__main__':
from argparse import ArgumentParser
from pathlib import Path
import getpass
import sys
parser = ArgumentParser(
description="push TLS privkey & certificate to MikroTik RouterOS router"
)
parser.add_argument(
"-k", "--privkey", type=Path, required=True, help="private key file"
)
parser.add_argument("--cert", type=Path, required=True, help="certificate file")
parser.add_argument(
"--chain", type=Path, help="separate certificate chain file (optional)"
)
parser.add_argument("-o", "--output", type=Path, help="output file")
args = parser.parse_args()
privkey_data = args.privkey.read_text()
cert_data = args.cert.read_text()
chain_data = args.chain.read_text() if args.chain is not None else None
key_passphrase = getpass.getpass("set the passphrase:")
pkcs12_data = PKCS12Exporter().export_pkcs12(
privkey_data=privkey_data,
cert_data=cert_data,
chain_data=chain_data,
passphrase=key_passphrase,
)
if args.output:
args.output.write_bytes(pkcs12_data)
else:
sys.stdout.buffer.write(pkcs12_data)