Source code for ykman.util

# Copyright (c) 2015 Yubico AB
# All rights reserved.
#
#   Redistribution and use in source and binary forms, with or
#   without modification, are permitted provided that the following
#   conditions are met:
#
#    1. Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#    2. Redistributions in binary form must reproduce the above
#       copyright notice, this list of conditions and the following
#       disclaimer in the documentation and/or other materials provided
#       with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from yubikit.core import Tlv, int2bytes
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from typing import Tuple
import ctypes

import logging


logger = logging.getLogger(__name__)


PEM_IDENTIFIER = b"-----BEGIN"


[docs] class InvalidPasswordError(Exception): """Raised when parsing key/certificate and the password might be wrong/missing."""
def _parse_pkcs12(data, password): try: key, cert, cas = pkcs12.load_key_and_certificates( data, password, default_backend() ) if cert: cas.insert(0, cert) return key, cas except ValueError as e: # cryptography raises ValueError on wrong password raise InvalidPasswordError(e)
[docs] def parse_private_key(data, password): """Identify, decrypt and return a cryptography private key object. :param data: The private key in bytes. :param password: The password to decrypt the private key (if it is encrypted). """ # PEM if is_pem(data): encrypted = b"ENCRYPTED" in data if encrypted and password is None: raise InvalidPasswordError("No password provided for encrypted key.") try: return serialization.load_pem_private_key( data, password, backend=default_backend() ) except ValueError as e: # Cryptography raises ValueError if decryption fails. if encrypted: raise InvalidPasswordError(e) logger.debug("Failed to parse PEM private key ", exc_info=True) except Exception: logger.debug("Failed to parse PEM private key ", exc_info=True) # PKCS12 if is_pkcs12(data): return _parse_pkcs12(data, password)[0] # DER try: return serialization.load_der_private_key( data, password, backend=default_backend() ) except Exception: logger.debug("Failed to parse private key as DER", exc_info=True) # All parsing failed raise ValueError("Could not parse private key.")
[docs] def parse_certificates(data, password): """Identify, decrypt and return a list of cryptography x509 certificates. :param data: The certificate(s) in bytes. :param password: The password to decrypt the certificate(s). """ logger.debug("Attempting to parse certificate using PEM, PKCS12 and DER") # PEM if is_pem(data): certs = [] for cert in data.split(PEM_IDENTIFIER): if cert: try: certs.append( x509.load_pem_x509_certificate( PEM_IDENTIFIER + cert, default_backend() ) ) except Exception: logger.debug("Failed to parse PEM certificate", exc_info=True) # Could be valid PEM but not certificates. if not certs: raise ValueError("PEM file does not contain any certificate(s)") return certs # PKCS12 if is_pkcs12(data): return _parse_pkcs12(data, password)[1] # DER try: return [x509.load_der_x509_certificate(data, default_backend())] except Exception: logger.debug("Failed to parse certificate as DER", exc_info=True) raise ValueError("Could not parse certificate.")
[docs] def get_leaf_certificates(certs): """Extract the leaf certificates from a list of certificates. Leaf certificates are ones whose subject does not appear as issuer among the others. :param certs: The list of cryptography x509 certificate objects. """ issuers = [cert.issuer for cert in certs] leafs = [cert for cert in certs if cert.subject not in issuers] return leafs
[docs] def is_pem(data): return data and PEM_IDENTIFIER in data
[docs] def is_pkcs12(data): """ Tries to identify a PKCS12 container. The PFX PDU version is assumed to be v3. See: https://tools.ietf.org/html/rfc7292. """ try: header = Tlv.parse_from(Tlv.unpack(0x30, data))[0] return header.tag == 0x02 and header.value == b"\x03" except ValueError: logger.debug("Unable to parse TLV", exc_info=True) return False
[docs] def display_serial(serial: int) -> str: """Displays an x509 certificate serial number in a readable format.""" if serial >= 0x10000000000000000: return ":".join(f"{b:02x}" for b in int2bytes(serial, 20)) return f"{serial} ({hex(serial)})"
[docs] class OSVERSIONINFOW(ctypes.Structure): _fields_ = [ ("dwOSVersionInfoSize", ctypes.c_ulong), ("dwMajorVersion", ctypes.c_ulong), ("dwMinorVersion", ctypes.c_ulong), ("dwBuildNumber", ctypes.c_ulong), ("dwPlatformId", ctypes.c_ulong), ("szCSDVersion", ctypes.c_wchar * 128), ]
[docs] def get_windows_version() -> Tuple[int, int, int]: """Get the true Windows version, since sys.getwindowsversion lies.""" osvi = OSVERSIONINFOW() osvi.dwOSVersionInfoSize = ctypes.sizeof(osvi) ctypes.windll.Ntdll.RtlGetVersion(ctypes.byref(osvi)) # type: ignore return osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwBuildNumber