feat(trust): add get_certificates() for Trust store certificate inventory
Reads certificates via POST /api/trust/cert/search, normalising each row
to {name, issuer, valid_from, valid_to, in_use_by}. Field mapping (Unix
timestamps for validity, %caref for the resolved issuer label) verified
against a live OPNsense 24.7 instance. Never surfaces crt_payload/
prv_payload/csr_payload -- those carry private key material.
This commit is contained in:
@@ -1059,6 +1059,38 @@ class OPNsenseDriver(FirewallDriver):
|
|||||||
})
|
})
|
||||||
return sorted(result, key=lambda x: x["name"].lower())
|
return sorted(result, key=lambda x: x["name"].lower())
|
||||||
|
|
||||||
|
def get_certificates(self) -> list[dict[str, Any]]:
|
||||||
|
"""Return certificates from the OPNsense Trust store.
|
||||||
|
|
||||||
|
Calls ``POST /api/trust/cert/search`` and normalises each row to
|
||||||
|
``{name, issuer, valid_from, valid_to, in_use_by}`` (Unix
|
||||||
|
timestamps for the validity fields). Never includes
|
||||||
|
crt_payload/prv_payload/csr_payload -- those rows carry private key
|
||||||
|
material and must not leave the Trust store.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = self._post("/api/trust/cert/search")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("get_certificates() failed: %s", exc)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _as_int(value: Any) -> int:
|
||||||
|
try:
|
||||||
|
return int(value or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
result: list[dict[str, Any]] = []
|
||||||
|
for row in data.get("rows", []):
|
||||||
|
result.append({
|
||||||
|
"name": row.get("commonname") or row.get("descr") or row.get("refid", ""),
|
||||||
|
"issuer": row.get("%caref") or "",
|
||||||
|
"valid_from": _as_int(row.get("valid_from")),
|
||||||
|
"valid_to": _as_int(row.get("valid_to")),
|
||||||
|
"in_use_by": _as_int(row.get("in_use")),
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
def get_dhcp_leases(self) -> list[dict[str, Any]]:
|
def get_dhcp_leases(self) -> list[dict[str, Any]]:
|
||||||
"""Return active DHCP leases from OPNsense.
|
"""Return active DHCP leases from OPNsense.
|
||||||
|
|
||||||
|
|||||||
@@ -972,6 +972,94 @@ class TestGetBgpNeighbors:
|
|||||||
assert key in peer
|
assert key in peer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# get_certificates()
|
||||||
|
#
|
||||||
|
# Field names verified 2026-07-15 against a live OPNsense 24.7 instance's
|
||||||
|
# actual POST /api/trust/cert/search response. Confirmed: valid_from/valid_to
|
||||||
|
# are Unix timestamps as strings (not a formatted date string), issuer comes
|
||||||
|
# from the resolved "%caref" label (not the raw "caref" ref-id), and rows
|
||||||
|
# also carry crt_payload/prv_payload/csr_payload -- get_certificates() must
|
||||||
|
# never surface those (private key material).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
CERT_SEARCH_RESPONSE = {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"refid": "69d55db78963e",
|
||||||
|
"descr": "gw.home.example.com (ACME Client)",
|
||||||
|
"commonname": "gw.home.example.com",
|
||||||
|
"caref": "6a24988658ae6",
|
||||||
|
"%caref": "YR1 (ACME Client)",
|
||||||
|
"cert_type": "server_cert",
|
||||||
|
"in_use": "1",
|
||||||
|
"valid_from": "1780779726",
|
||||||
|
"valid_to": "1788555725",
|
||||||
|
"crt_payload": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
|
||||||
|
"prv_payload": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"refid": "def456",
|
||||||
|
"descr": "Internal cert",
|
||||||
|
"commonname": "",
|
||||||
|
"caref": "",
|
||||||
|
"%caref": "",
|
||||||
|
"cert_type": "server_cert",
|
||||||
|
"in_use": "0",
|
||||||
|
"valid_from": "1780000000",
|
||||||
|
"valid_to": "1790000000",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetCertificates:
|
||||||
|
def test_returns_one_entry_per_row(self, driver):
|
||||||
|
driver._post = lambda path, data=None: CERT_SEARCH_RESPONSE
|
||||||
|
certs = driver.get_certificates()
|
||||||
|
assert len(certs) == 2
|
||||||
|
|
||||||
|
def test_calls_trust_cert_search(self, driver):
|
||||||
|
calls = []
|
||||||
|
driver._post = lambda path, data=None: calls.append(path) or CERT_SEARCH_RESPONSE
|
||||||
|
driver.get_certificates()
|
||||||
|
assert calls == ["/api/trust/cert/search"]
|
||||||
|
|
||||||
|
def test_maps_expected_fields(self, driver):
|
||||||
|
driver._post = lambda path, data=None: CERT_SEARCH_RESPONSE
|
||||||
|
cert = driver.get_certificates()[0]
|
||||||
|
assert cert["name"] == "gw.home.example.com"
|
||||||
|
assert cert["issuer"] == "YR1 (ACME Client)"
|
||||||
|
assert cert["valid_from"] == 1780779726
|
||||||
|
assert cert["valid_to"] == 1788555725
|
||||||
|
assert cert["in_use_by"] == 1
|
||||||
|
|
||||||
|
def test_falls_back_to_descr_when_commonname_empty(self, driver):
|
||||||
|
driver._post = lambda path, data=None: CERT_SEARCH_RESPONSE
|
||||||
|
cert = driver.get_certificates()[1]
|
||||||
|
assert cert["name"] == "Internal cert"
|
||||||
|
|
||||||
|
def test_missing_caref_defaults_to_empty_string(self, driver):
|
||||||
|
driver._post = lambda path, data=None: CERT_SEARCH_RESPONSE
|
||||||
|
cert = driver.get_certificates()[1]
|
||||||
|
assert cert["issuer"] == ""
|
||||||
|
|
||||||
|
def test_never_includes_private_key_or_cert_payload(self, driver):
|
||||||
|
driver._post = lambda path, data=None: CERT_SEARCH_RESPONSE
|
||||||
|
for cert in driver.get_certificates():
|
||||||
|
assert "prv_payload" not in cert
|
||||||
|
assert "crt_payload" not in cert
|
||||||
|
assert "csr_payload" not in cert
|
||||||
|
|
||||||
|
def test_no_rows_returns_empty_list(self, driver):
|
||||||
|
driver._post = lambda path, data=None: {"rows": []}
|
||||||
|
assert driver.get_certificates() == []
|
||||||
|
|
||||||
|
def test_api_error_returns_empty_list(self, driver):
|
||||||
|
driver._post = lambda path, data=None: (_ for _ in ()).throw(Exception("404"))
|
||||||
|
assert driver.get_certificates() == []
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _post()
|
# _post()
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user