Source code for saltext.vcf.states.vcf_vrli_certificate

"""State module for the vRLI appliance certificate.

Idempotency: the state parses the leaf ``cert`` PEM to extract its
serial number (a hex string) and compares it to the ``serialNum``
reported by ``GET /api/v2/certificate``. Reinstalls only when they
differ. Requires the standard-library ``ssl`` module (bundled).
"""

import binascii
import re

from saltext.vcf.clients import vrli_certificate as c

__virtualname__ = "vcf_vrli_certificate"


def __virtual__():
    return __virtualname__


def _ret(name):
    return {"name": name, "changes": {}, "result": True, "comment": ""}


_HEX_ONLY = re.compile(r"[^0-9a-fA-F]")


def _serial_from_pem(pem):
    """Return the leaf serial as a lowercase hex string (no ``0x``, no ``:``).

    We use ``ssl.PEM_cert_to_DER_cert`` + a minimal ASN.1 walk so we
    don't pull in ``cryptography`` for a build that already ships
    only ``requests`` + ``paramiko``.
    """
    import ssl as _ssl

    try:
        der = _ssl.PEM_cert_to_DER_cert(pem)
    except Exception as exc:  # noqa: BLE001 - normalize bad-PEM errors
        raise ValueError(f"could not parse PEM certificate: {exc}") from exc

    # Minimal ASN.1 walk to reach the tbsCertificate.serialNumber INTEGER.
    # Certificate ::= SEQUENCE {
    #     tbsCertificate       TBSCertificate,        <-- also a SEQUENCE
    #     signatureAlgorithm   ...,
    #     signatureValue       ... }
    # TBSCertificate ::= SEQUENCE {
    #     version         [0] EXPLICIT Version DEFAULT v1,   <-- optional
    #     serialNumber        CertificateSerialNumber,       <-- INTEGER
    #     ... }
    def _read_len(buf, idx):
        n = buf[idx]
        idx += 1
        if n & 0x80:
            n_bytes = n & 0x7F
            n = int.from_bytes(buf[idx : idx + n_bytes], "big")
            idx += n_bytes
        return n, idx

    def _consume(buf, idx):
        tag = buf[idx]
        idx += 1
        length, idx = _read_len(buf, idx)
        return tag, buf[idx : idx + length], idx + length

    _tag, cert_body, _end = _consume(der, 0)
    _tag, tbs_body, _end = _consume(cert_body, 0)
    idx = 0
    tag = tbs_body[idx]
    if tag == 0xA0:  # [0] EXPLICIT version
        _tag, _v, idx = _consume(tbs_body, idx)
    _tag, serial_bytes, _idx = _consume(tbs_body, idx)
    # Strip a leading 0x00 sign byte if the high bit of the next octet is set.
    if len(serial_bytes) > 1 and serial_bytes[0] == 0x00:
        serial_bytes = serial_bytes[1:]
    return binascii.hexlify(serial_bytes).decode()


def _normalize_serial(raw):
    if raw is None:
        return None
    return _HEX_ONLY.sub("", str(raw)).lower().lstrip("0") or "0"


[docs] def certificate_present(name, cert, key, chain=None, profile=None): """Ensure the appliance is presenting the given leaf certificate. *cert* — PEM string for the leaf. *key* — PEM string for its private key. *chain* — optional intermediate chain PEM. Idempotency: compares the leaf's serial number against the ``serialNum`` field on ``GET /api/v2/certificate``. Non-matching triggers a POST that replaces the cert (which restarts the API listener — expect the next state in the run to reconnect). """ ret = _ret(name) desired_serial = _normalize_serial(_serial_from_pem(cert)) current = c.get(__opts__, profile=profile) current_serial = _normalize_serial(current.get("serialNum")) if current else None if current_serial == desired_serial: ret["comment"] = f"Appliance certificate already at serial {desired_serial}; no change" return ret if __opts__.get("test"): ret["result"] = None ret["comment"] = ( f"Appliance certificate would be replaced ({current_serial!r} " f"-> {desired_serial!r}); API listener will restart" ) return ret c.install(__opts__, cert, key, chain_pem=chain, profile=profile) ret["changes"] = {"serialNum": {"old": current_serial, "new": desired_serial}} ret["comment"] = ( f"Installed new appliance certificate (serial {desired_serial}); " f"API listener restarted" ) return ret