snapshot
This commit is contained in:
180
mtik_cert_pusher/ssl_util.py
Normal file
180
mtik_cert_pusher/ssl_util.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#!python3
|
||||
# dlitz 2025-2026
|
||||
import contextlib
|
||||
import fcntl
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
BestAvailableEncryption,
|
||||
Encoding,
|
||||
KeySerializationEncryption,
|
||||
PrivateFormat,
|
||||
load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
pkcs12,
|
||||
)
|
||||
|
||||
|
||||
def pipe_with_buffered_data(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 = _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
|
||||
|
||||
|
||||
def _open_pipes(*, 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
|
||||
|
||||
|
||||
class ResultParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SSLUtil:
|
||||
|
||||
openssl_prog = "openssl"
|
||||
|
||||
def cert_fingerprint_sha256(self, pem_cert: str) -> bytes:
|
||||
"""Return the SHA256 fingerprint of the certificate, as bytes."""
|
||||
cert_obj = x509.load_pem_x509_certificate(pem_cert.encode())
|
||||
result = cert_obj.fingerprint(hashes.SHA256())
|
||||
assert isinstance(result, bytes) and len(result) == 32, (result,)
|
||||
return result
|
||||
|
||||
def cert_serial(self, cert: str) -> int:
|
||||
"""Return the serial number of the certificate, as integer. Might be negative."""
|
||||
cert_obj = x509.load_pem_x509_certificate(cert.encode())
|
||||
result = cert_obj.serial_number
|
||||
assert isinstance(result, int), ("serial number not integer?", result)
|
||||
return result
|
||||
|
||||
def cert_skid(self, pem_cert: str) -> bytes:
|
||||
"""Return the SubjectKeyIdentifier of the certificate, as bytes."""
|
||||
cert_obj = x509.load_pem_x509_certificate(pem_cert.encode())
|
||||
pubkey_obj = cert_obj.public_key()
|
||||
skid_obj = x509.SubjectKeyIdentifier.from_public_key(pubkey_obj)
|
||||
assert skid_obj.digest == skid_obj.key_identifier
|
||||
result = skid_obj.key_identifier
|
||||
assert isinstance(result, bytes)
|
||||
assert len(result) == 20
|
||||
return result
|
||||
|
||||
def skid_from_pubkey(self, pubkey: str) -> bytes:
|
||||
pubkey_obj = load_pem_public_key(pubkey.encode())
|
||||
result = x509.SubjectKeyIdentifier.from_public_key(pubkey_obj).key_identifier
|
||||
assert isinstance(result, bytes)
|
||||
assert len(result) == 20
|
||||
return result
|
||||
|
||||
def skid_from_private_key(self, private_key: str) -> bytes:
|
||||
private_key_obj = load_pem_private_key(private_key.encode())
|
||||
pubkey_obj = private_key_obj.public_key()
|
||||
result = x509.SubjectKeyIdentifier.from_public_key(pubkey_obj).key_identifier
|
||||
assert isinstance(result, bytes)
|
||||
assert len(result) == 20
|
||||
return result
|
||||
|
||||
def create_pkcs12_from_key_and_certificates(
|
||||
self,
|
||||
*,
|
||||
name: str | None = None,
|
||||
key: str,
|
||||
cert: str,
|
||||
chain: str | None = None,
|
||||
passphrase: str,
|
||||
) -> bytes:
|
||||
private_key_obj = load_pem_private_key(key.encode(), password=None)
|
||||
cert_obj = x509.load_pem_x509_certificate(cert.encode())
|
||||
chain_objs = x509.load_pem_x509_certificates(chain.encode()) if chain else []
|
||||
|
||||
if name is None:
|
||||
pubkey_obj = cert_obj.public_key()
|
||||
skid_obj = x509.SubjectKeyIdentifier.from_public_key(pubkey_obj)
|
||||
skid = skid_obj.key_identifier.hex()
|
||||
fingerprint = cert_obj.fingerprint(hashes.SHA256()).hex()
|
||||
name = f"SKID:{skid} FPR:{fingerprint}"
|
||||
|
||||
result = pkcs12.serialize_key_and_certificates(
|
||||
name=name.encode(),
|
||||
key=private_key_obj,
|
||||
cert=cert_obj,
|
||||
cas=chain_objs,
|
||||
encryption_algorithm=self.pkcs12_encryption_algorithm(passphrase.encode()),
|
||||
)
|
||||
assert isinstance(result, bytes)
|
||||
assert result
|
||||
return result
|
||||
|
||||
# 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 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 pkcs12_encryption_algorithm(
|
||||
self, passphrase: bytes
|
||||
) -> KeySerializationEncryption:
|
||||
return (
|
||||
PrivateFormat.PKCS12.encryption_builder()
|
||||
.key_cert_algorithm(pkcs12.PBES.PBESv2SHA256AndAES256CBC)
|
||||
.kdf_rounds(20000)
|
||||
.hmac_hash(hashes.SHA256())
|
||||
.build(passphrase)
|
||||
)
|
||||
Reference in New Issue
Block a user