Skip to content

Commit 69ae169

Browse files
authored
Merge pull request #31 from Indicio-tech/feat/cert-trust-registry-auto-add
feat: auto-register certs as trust anchors and return PEMs by default
2 parents 79b581b + a14cb7b commit 69ae169

4 files changed

Lines changed: 408 additions & 4 deletions

File tree

oid4vc/mso_mdoc/key_generation.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,18 @@ async def generate_default_keys_and_certs(
461461
session, "default_certificate", {"cert_id": cert_id}
462462
)
463463

464+
# Auto-register the certificate as a trust anchor so verifiers
465+
# immediately have the issuer cert in their trust store.
466+
anchor_ids = await storage_manager.auto_register_trust_anchors(
467+
session, cert_pem, source="key-generation"
468+
)
469+
if anchor_ids:
470+
LOGGER.info(
471+
"Auto-registered %d trust anchor(s) from generated certificate: %s",
472+
len(anchor_ids),
473+
anchor_ids,
474+
)
475+
464476
LOGGER.info("Generated default mDoc key: %s and certificate: %s", key_id, cert_id)
465477

466478
return {

oid4vc/mso_mdoc/key_routes.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,15 @@ async def list_certificates(request: web.BaseRequest):
119119
"""List all stored mDoc certificates.
120120
121121
Query parameters:
122-
include_pem: If "true", include the certificate_pem field in results
122+
exclude_pem: If "true", omit the certificate_pem field from results.
123+
PEMs are included by default so callers can use cert data for
124+
trust-registry automation without extra round-trips.
123125
"""
124126
context: AdminRequestContext = request["context"]
125127
storage_manager = MdocStorageManager(context.profile)
126128

127-
# Check for include_pem query parameter
128-
include_pem = request.query.get("include_pem", "").lower() == "true"
129+
# PEMs are now included by default; pass exclude_pem=true to omit them.
130+
include_pem = request.query.get("exclude_pem", "").lower() != "true"
129131

130132
try:
131133
async with context.profile.session() as session:

oid4vc/mso_mdoc/storage/__init__.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"""
2020

2121
from datetime import UTC, datetime
22+
import hashlib
2223
import logging
24+
import re
2325
from typing import Any, Dict, List, Optional, Tuple
2426

2527
from acapy_agent.core.profile import Profile, ProfileSession
@@ -231,10 +233,15 @@ async def store_certificate(
231233
key_id: str,
232234
metadata: Optional[Dict] = None,
233235
) -> None:
234-
"""Store a PEM certificate."""
236+
"""Store a PEM certificate and auto-register it as a trust anchor."""
235237
await certificates.store_certificate(
236238
session, cert_id, certificate_pem, key_id, metadata
237239
)
240+
# Automatically register every cert in the PEM chain as a trust anchor
241+
# so the trust registry stays in sync without manual steps.
242+
await self.auto_register_trust_anchors(
243+
session, certificate_pem, source="cert-store"
244+
)
238245

239246
async def get_certificate(
240247
self, session: ProfileSession, cert_id: str
@@ -364,3 +371,57 @@ async def get_all_trust_anchor_pems(self, session: ProfileSession) -> List[str]:
364371
async def delete_trust_anchor(self, session: ProfileSession, anchor_id: str) -> bool:
365372
"""Delete a trust anchor by ID."""
366373
return await trust_anchors.delete_trust_anchor(session, anchor_id)
374+
375+
async def auto_register_trust_anchors(
376+
self,
377+
session: ProfileSession,
378+
certificate_pem: str,
379+
source: str = "auto",
380+
) -> List[str]:
381+
"""Parse a PEM (possibly a chain) and register each cert as a trust anchor.
382+
383+
Duplicate certificates (by PEM content) are silently skipped so this
384+
method is idempotent.
385+
386+
Args:
387+
session: Active database session
388+
certificate_pem: One or more concatenated PEM certificate blocks
389+
source: Label for metadata (e.g. ``"key-generation"``)
390+
391+
Returns:
392+
List of anchor IDs that were newly created.
393+
"""
394+
_PEM_RE = re.compile(
395+
r"-----BEGIN CERTIFICATE-----[A-Za-z0-9+/=\s]+?"
396+
r"-----END CERTIFICATE-----\n?",
397+
re.DOTALL,
398+
)
399+
individual_pems = _PEM_RE.findall(certificate_pem)
400+
if not individual_pems:
401+
return []
402+
403+
# Fetch existing trust anchor PEMs to avoid duplicates
404+
existing_pems = set(await trust_anchors.get_all_trust_anchor_pems(session))
405+
406+
created: List[str] = []
407+
for pem in individual_pems:
408+
normalized = pem.strip()
409+
if normalized in existing_pems:
410+
continue
411+
# Deterministic-ish anchor ID based on cert fingerprint
412+
fingerprint = hashlib.sha256(normalized.encode()).hexdigest()[:12]
413+
anchor_id = f"auto-{source}-{fingerprint}"
414+
try:
415+
await trust_anchors.store_trust_anchor(
416+
session,
417+
anchor_id=anchor_id,
418+
certificate_pem=normalized,
419+
metadata={"source": source, "auto_registered": True},
420+
)
421+
created.append(anchor_id)
422+
existing_pems.add(normalized)
423+
except Exception:
424+
LOGGER.debug(
425+
"Skipping duplicate trust anchor %s", anchor_id, exc_info=True
426+
)
427+
return created

0 commit comments

Comments
 (0)