domingo, 8 de diciembre de 2024

Python exportar certificados de los navegadores

1. Localización de los ficheros de certificados

En Linux-Mozilla los certificados se guardan en la carpeta hija de:

~/.mozilla/firefox

que contiene entre otros el fichero "cert9.db"

Por tanto hay que buscar las carpetas contenedoras de dicho fichero (puede haber varias)

En Linux-Chrome se guardan en la carpeta:

~/.pki/nssdb


2. Como se hace en Linux bash?

Para manejar los certificados contenidos en cada carpeta hay que instalar algunas librerías del paquete NSS (Network Security Services).

sudo apt install libnss3-tools

Para listar los certificados contenidos en una carpeta de las mencionadas arriba, por ejemplo para mozilla, donde XXXX es la carpeta que contiene el fichero "cert9.db":

certutil -L -d sql:$HOME/.mozilla/firefox/XXXX

Ojo hay que tener cuiidado de no incluir "cer9.db" en la ruta

Y para Chrome

certutil -L -d sql:$HOME/.pki/nsb

Y nos mostará algo parecido a:

Certificate Nickname                              Trust Attributes
                                                   SSL,S/MIME,JAR/XPI
Mi nick                                            u,u,u
ACME Root CA                                       CT,C,C

Ahora podemos extraer los certificados a partir de la carpeta y el Nickname. En el lisado anterior vemos un nickname llamado "Mi nick". Para obtener este ceerificamos SIN CLAVE PRIVADA hacemos:

certutil -L -n "Mi nick" -d sql:$HOME/.pki/nsb -a > cert.pem

Pero hay que tener cuidado pues no incluye la clave privada. Si queremos que incluya la clave pivada, hay que guardar una contraseña en un fichero y ejecutar el comando. En este ejmplo hacemos las dos cosas

# Guardamos la conraseña en un fichero
echo "mypasword" > temp_pass_file_path

#Guardamos el certificado con la clave privada, protegidos por la contraseña "mypassword" 
pk12util -o, cert.pem -d sql:$HOME/.pki/nsb -n "My nick" -k $HOME/.pki/nsb/key4.db -w temp_pass_file_path

Ahora supongamos que queremos leer el fichero pem generado "cert.pem" y queremos guardarlo con otro nombre y con otra contraseña:

#Cambiamos el nombre y nos pedirá la nueva contraseña
openssl rsa -in cert.pem -out nuevo_certificado.pem -aes256

#Comprobamos que podemos leer el fichero con la nueva contraseña
openssl rsa -in nuevo_certificado.pem

3. Como se hace en Windows powershell?

Ejecutamos este programa de powershell para extrar todos los certificados a formato pfx y los protegemos con la contraseña "password_pfx"

# Ruta donde se guardarán los archivos PFX
$outputFolder = "C:\ruta\certificados"

# Crear la carpeta si no existe
if (-not (Test-Path -Path $outputFolder)) {
    New-Item -ItemType Directory -Path $outputFolder
}

# Iterar sobre todos los certificados que se listan con "Get-ChildItem Cert:\CurrentUser\My"
$certificates = Get-ChildItem Cert:\CurrentUser\My
foreach ($cert in $certificates) {
    $filePath = Join-Path -Path $outputFolder -ChildPath ("cert_" + $cert.Thumbprint + ".pfx")
    $password = ConvertTo-SecureString -String "password_pfx" -Force -AsPlainText

    Export-PfxCertificate -Cert $cert -FilePath $filePath -Password $password
    Write-Host "Exportado: $filePath"
}

Y para convertir de pfx a pem utilizamos el power shell, manteniendo la misma contraseña:

# Ruta donde están los archivos PFX
$pfxFolder = "C:\ruta\certificados"
# Ruta donde se guardarán los archivos PEM
$pemFolder = "C:\ruta\certificados_pem"

# Crear la carpeta para los PEM si no existe
if (-not (Test-Path -Path $pemFolder)) {
    New-Item -ItemType Directory -Path $pemFolder
}

# Convertir cada PFX a PEM
$pfxFiles = Get-ChildItem -Path $pfxFolder -Filter "*.pfx"
foreach ($pfx in $pfxFiles) {
    $pemFilePath = Join-Path -Path $pemFolder -ChildPath ($pfx.BaseName + ".pem")

    # Ejecutar OpenSSL para la conversión
    & "openssl" pkcs12 -in $pfx.FullName -out $pemFilePath -nodes -password pass:password_pfx
    Write-Host "Convertido a PEM: $pemFilePath"
}


4. En Python

Solo se ha comprobado en Linux.

Veamos el código python

import os
import platform
import subprocess
from cryptography.hazmat.primitives.serialization import Encoding, pkcs12, NoEncryption
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
from cryptography import x509
from Crypto.IO import PEM
from Crypto.PublicKey import RSA
from OpenSSL.crypto import PKCS12, FILETYPE_PEM, dump_certificate, dump_privatekey
import base64
import ssl
import tempfile

# ------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
for i in range(2):sys.path.append(str(Path(__file__).parents[i]))
try: # if in the same folder then needs a try and import directly
	from basicutils import xmfiles
except Exception as error:
	import xmfiles	
# ------Fin imprescindible


#def save_certificate_to_file(cert, filename):
#    """Save a certificate to a file in PEM format."""
#    try:
#        with open(filename, "wb") as f:
#            f.write(cert.public_bytes(Encoding.PEM))
#        print(f"Certificate saved to {filename}")
#    except Exception as e:
#        print(f"Error saving certificate to file: {e}")

def change_password(input_pem, input_password, output_pem, output_password):
    '''
    Cambia la contraseña de un archivo PEM'''
    try:
        # Leer contenido del archivo PEM
        with open(input_pem, "rb") as file:
            pem_data = file.read()

        # Desencriptar la clave privada usando la contraseña existente
        key, _ = PEM.decode(pem_data, passphrase=input_password.encode("utf-8"))

        # Cargar la clave como objeto RSA
        rsa_key = RSA.import_key(key)

        # Re-encriptar la clave privada con la nueva contraseña
        encrypted_key = rsa_key.export_key(format="PEM", passphrase=output_password, pkcs=8)

        # Escribir el nuevo archivo PEM con la nueva contraseña
        with open(output_pem, "wb") as file:
            file.write(encrypted_key)

        print(f"Archivo PEM guardado como '{output_pem}' con la nueva contraseña.")
    except Exception as e:
        print(f"Error: {e}")

        
def parse_certificate(browser:str, cert_data)->dict:
    """Parse a certificate into a dict and print details of an X.509 certificate."""
    try:
        cert = load_der_x509_certificate(cert_data)
    except ValueError:
        try:
            cert = load_pem_x509_certificate(cert_data)
        except Exception as e:
            print(f"Failed to parse certificate: {e}")
            return None

    try:
        #print("\nCertificate Details:"); #print(f"  Subject: {cert.subject}")
        # Obtener el CN
        subject = cert.subject
        cn = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value
        print(f"  CN: {cn}")
        browseCN=browser+"-"+cn
        
        #print(f"  Issuer: {cert.issuer}") ; #print(f"  Serial Number: {cert.serial_number}")
        #print(f"  Valid From: {cert.not_valid_before}");  #print(f"  Valid To: {cert.not_valid_after}")
        #print(f"  Signature Algorithm: {cert.signature_algorithm_oid}")
    except Exception as e: 
        print(f"Failed to print certificate details: {e}")    
    return {browseCN:cert} if cn else {'No CN':cert}

def export_certificate_and_key_linux(nssDir:str, output_path:str, nickname:str, pkcs12_password:str,exportKey:bool=False):
    ''' 
        Exports the certificate and key in a file "pem" and also into 
        2 diferent files, one for the cert (.crt) and another for te key (.key)
        if exportKey is False, the files ".crt" and ".key" are not created 
        Parameters:
            nssDir (str): The path to the NSS database.
            output_path (str): The path where the files will be saved.
            nickname (str): The nickname of the certificate.
            pkcs12_password (str): The password for the PKCS#12 file.
            exportKey (bool): Whether to create crt and key files.
    '''
    try:
        # Step 1: Define paths
        #nss_dir = os.path.expanduser("~/.pki/nssdb")
        if not os.path.exists(nssDir):
            raise FileNotFoundError(f"NSS database not found in {nssDir}. Ensure Chrome has certificates stored there.")

        # Step 2: Write password to a temporary file
        with tempfile.NamedTemporaryFile(delete=False, mode="w", prefix="pkcs12_", suffix=".pass") as temp_pass_file:
            temp_pass_file.write(pkcs12_password)
            temp_pass_file_path = temp_pass_file.name

        # Step 3: Export the certificate and key as a PKCS#12 file
        pkcs12_file = os.path.join(output_path, f"{nickname}.p12")
        pk12util_cmd = [
            "pk12util",
            "-o", pkcs12_file,
            "-d", f"sql:{nssDir}",
            "-n", nickname,
            "-k", f"{nssDir}/key4.db",
            "-w", temp_pass_file_path
        ]

        print("Exporting PKCS#12 file...")
        subprocess.run(pk12util_cmd, check=True)
        print(f"PKCS#12 file exported to: {pkcs12_file}")

        if exportKey:
            # Step 4: Convert PKCS#12 to separate certificate and private key
            cert_file = os.path.join(output_path, f"{nickname}.crt")
            key_file = os.path.join(output_path, f"{nickname}.key")
            openssl_cmd = [
                "openssl", "pkcs12",
                "-in", pkcs12_file,
                "-clcerts",
                "-nokeys", "-out", cert_file,
                "-nodes",
                "-password", f"pass:{pkcs12_password}"
            ]
            print("Extracting certificate...")
            subprocess.run(openssl_cmd, check=True)
            print(f"Certificate exported to: {cert_file}")

            openssl_key_cmd = [
                "openssl", "pkcs12",
                "-in", pkcs12_file,
                "-nocerts",
                "-out", key_file,
                "-nodes",
                "-password", f"pass:{pkcs12_password}"
            ]
            print("Extracting private key...")
            subprocess.run(openssl_key_cmd, check=True)
            print(f"Private key exported to: {key_file}")

    except subprocess.CalledProcessError as e:
        print(f"Error executing command: {e}")
    except FileNotFoundError as e:
        print(e)
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

def export_user_certs_windows(output_path, password="my-password", export_format="pem"):
    """
    Busca un certificado por nombre en el almacén de Windows, extrae su clave privada y lo guarda como PEM o PFX.
    
    Args:
        name (str): Nombre del certificado a buscar.
        output_path (str): Ruta donde se guardará el archivo exportado.
        password (str, optional): Contraseña para proteger el archivo PFX.
        export_format (str): Formato del archivo de salida ('pem' o 'pfx').
    """
    import wincertstore
    try:
        with wincertstore.CertSystemStore("MY") as store:
            for cert in store.itercerts(usage=wincertstore.CERT_FIND_ANY):

                # Extraer el certificado y la clave privada
                private_key = cert.get_key()
                certificate = cert.get_cert()

                # Verificar si la clave privada está disponible
                if not private_key:
                    print("Advertencia: Clave privada no disponible o no exportable.")
                    return

                if export_format == "pem":
                    # Exportar como PEM
                    cert_pem = dump_certificate(FILETYPE_PEM, certificate)
                    key_pem = dump_privatekey(FILETYPE_PEM, private_key)
                    with open(output_path, "wb") as f:
                        f.write(key_pem)
                        f.write(cert_pem)
                    print(f"Certificado exportado como PEM: {output_path}")
                elif export_format == "pfx":
                    # Exportar como PFX
                    pfx = PKCS12()
                    pfx.set_privatekey(private_key)
                    pfx.set_certificate(certificate)
                    pfx_data = pfx.export(password.encode() if password else None)
                    with open(output_path, "wb") as f:
                        f.write(pfx_data)
                    print(f"Certificado exportado como PFX: {output_path}")
                else:
                    print("Formato no soportado. Usa 'pem' o 'pfx'.")
    except Exception as e:
        print(f"Error: {e}")  

def list_user_certificates_linux():
    """List user-installed certificates on Linux and display their details."""
    certs = []
    certificates=[]
    allCertsDict={}
    home_dir = os.path.expanduser("~")

    # Firefox Certificates
    firefox_path = os.path.join(home_dir, ".mozilla/firefox")
    if os.path.exists(firefox_path):
        #profiles = [d for d in os.listdir(firefox_path) if d.endswith(".default")]
        #for profile in profiles:
        #    cert_db = os.path.join(firefox_path, profile, "cert9.db")
        #    if os.path.exists(cert_db):
        #        certs.append(("Firefox", cert_db))
        certDBs=xmfiles.findFileInFolderRecursive('cert9.db', firefox_path)
        for i, certDB in enumerate(certDBs):
            certs.append(("Mz-"+str(i), certDB[1]))

    # Chrome Certificates
    chrome_path = os.path.join(home_dir, ".pki/nssdb")
    if os.path.exists(chrome_path):
        certs.append(("Ch", chrome_path))

    # Display certificate details using `certutil`
    for browser, cert_db in certs:
        print(f"\n{browser} Certificates from {cert_db}:")
        try:
            output = subprocess.check_output(["certutil", "-L", "-d", f"sql:{cert_db}"], text=True)
            print(output)
            #================================================================
            # Extract certificate nicknames and details
            lines = output.splitlines()[2:]  # Skip the header lines
            for line in lines:
                
                try:
                    #nickname = line.split()[0]
                    nickname = line.split('  ')[0]
                    print(f"Nickname: {nickname}  Browser: {browser}")
                    # Export each certificate in PEM format
                    cert_output = subprocess.check_output(
                        ["certutil", "-L", "-n", nickname, "-d", f"sql:{cert_db}", "-a"], text=True
                    )
                    certDict=parse_certificate(browser,[cert_output.encode(), nickname, cert_db ])
                    allCertsDict.update(certDict) #add to all
                   
                except Exception as e:
                    print(f"Failed to export certificate '{line}'")
        
        except Exception as e:
            print(f"Error reading {browser} certificates: {e}")
    #return certs
    return allCertsDict



def list_user_certificates_mac():
    """List user-installed certificates on macOS and display their details."""
    user_certs = []
    allCertsDict={}
    try:
        output = subprocess.check_output(
            ["security", "find-certificate", "-c", "User", "-a", "-p"],
            text=True
        )
        certs = output.split("-----END CERTIFICATE-----")
        for cert in certs:
            if cert.strip():
                pem_cert = cert + "-----END CERTIFICATE-----"
                print("\nCertificate PEM Format:")
                print(pem_cert)
                user_certs.append(pem_cert)
                certDict=parse_certificate(pem_cert.encode())
                allCertsDict.update(certDict)
    except Exception as e:
        print(f"Error accessing user certificates on macOS: {e}")
    #return user_certs
    return allCertsDict


def main():
    
    print("Listing all user-installed certificates...\n")
    user_certs = []

    if platform.system() == "Linux":
        # Linux
        home_dir = os.path.expanduser("~")
        chrome_path = os.path.join(home_dir, ".pki/nssdb")
        #export_certificate_and_key_linux(chrome_path, "/home/edu/MyCerts/Prova", "EPN1", "my-password")
            
        print("User Certificates (Linux):")
        user_certs = list_user_certificates_linux()
        
        print("===================================================================")
        for k,v in user_certs.items():
            print(k)
            print("         ",v)
            export_certificate_and_key_linux(nssDir=v[2], output_path="/home/edu/MyCerts/Prova", 
                nickname=v[1],pkcs12_password="my-password", exportKey=False )
            print("-------------------------------")

    

    elif platform.system() == "Windows":
        # Windows
        print("User Certificates (Windows):")
        export_user_certs_windows("C:\\Users\\edu\\MyCerts\\Prova", export_format="pem")

    elif platform.system() == "Darwin":
        # macOS
        print("User Certificates (macOS):")
        user_certs = list_user_certificates_mac()

    
if __name__ == "__main__":
    main()










No hay comentarios :

Publicar un comentario