Skip to main content

cbc-bridge-service — Application Logic

Version: 1.0 Status: Draft Owner: Government / Emergency Last Updated: 2026-04-21

Companion: DOMAIN_MODEL · API_CONTRACTS · DATA_MODEL · EVENT_SCHEMAS · FAILURE_MODES


1. Use Cases


UC-01: BroadcastEmergency (gRPC hot path)

Trigger. Authorised government caller (civil-defence, NDMA, police) or regulator-portal-service calls CbcBridgeService/BroadcastEmergency over mTLS with a detached PKCS#7 signature in the X-Gov-Signature header.

Input. BroadcastEmergencyRequest:

  • requestId (UUIDv4 idempotency key)
  • headline (≤ 60 chars)
  • bodyVariants (1–4 entries: en, fa, ps, ar)
  • geoTarget (cell_ids[] | polygon GeoJSON | region ISO-3166-2 | country)
  • severity (P0_EXTREME | P1_MAJOR | P2_ADVISORY)
  • expiryMinutes (≤ 120 for P0; ≤ 240 for P1; ≤ 720 for P2)
  • isDrill (bool)
  • nonce (16-byte random; anti-replay)
  • requestedAt (RFC 3339 UTC)

Output. BroadcastEmergencyResponse:

  • broadcastId
  • acceptedAt (RFC 3339 UTC)
  • expectedDispatchBy (≤ acceptedAt + 60 s)
  • signatureAuditId

SLA. P95 ≤ 1 500 ms (HSM signature verify + DB write + outbox publish). P99 ≤ 3 000 ms.

Steps:

  1. Transport auth. mTLS handshake presents a client certificate chained to the national-PKI trust anchor. On OCSP-stapled revocation or CRL-cached revocation, connection is rejected at TLS layer with unknown_ca alert — no application code runs.
  2. Replay-attack check. Compute presentedCertFingerprint = sha256(clientCert.der). Reject with UNAUTHENTICATED if:
    • |now - requestedAt| > 120 sREASON_CLOCK_SKEW (3GPP TS 23.041 dispatch window assumption)
    • SETNX cbc:nonce:{presentedCertFingerprint}:{nonce} 1 EX 86400 returns 0 (replay observed) → REASON_REPLAY
  3. Canonicalisation. Build the RFC 8785 JCS canonical JSON of the request body (excluding signature), compute sha256 digest.
  4. HSM signature verify. Call the HSM (PKCS#11 C_Verify) with the presented cert's public key, the digest, and the detached signature. Any HSM failure is fail-closed (never fall back to in-process verification). Persist result to cbc.signature_audit.
  5. Caller-registry lookup. Resolve caller by (certSubject, certFingerprintSha256) in cbc.authorised_callers WHERE active = TRUE AND notBefore ≤ now ≤ notAfter. If not found → PERMISSION_DENIED with REASON_CALLER_NOT_REGISTERED.
  6. Scope check. severity ∈ caller.allowedSeverities AND geoTarget ⊆ caller.allowedRegions; otherwise PERMISSION_DENIED with REASON_CALLER_SCOPE_VIOLATION.
  7. Idempotency. Hash requestId → broadcastId in Redis (cbc:idem:{requestId} 24 h). Repeat requests return the original broadcastId without side-effects.
  8. Payload validation. Run UC-06 translation pre-flight to confirm each language encodes within the 15-page CBS limit; reject with INVALID_ARGUMENT on overflow.
  9. Persist Broadcast row. Transaction writes:
    • cbc.broadcasts row with state = ACCEPTED, hash-chain fields computed against prior row in monthly partition
    • cbc.audit row with transition = REQUESTED
    • cbc.outbox event cbc.broadcast.requested.v1 Commit → outbox relay publishes to NATS within 1 s.
  10. Return response. broadcastId, acceptedAt, expectedDispatchBy = acceptedAt + 60 s, signatureAuditId.
  11. Async handoff. DispatcherWorker picks up ACCEPTED broadcasts (Postgres LISTEN/NOTIFY + poll fallback) and runs UC-02 PerMnoDispatch.

Error taxonomy:

gRPC statusReason codeCondition
INVALID_ARGUMENTREASON_MISSING_LANGUAGEP0 without en + fa + ps
INVALID_ARGUMENTREASON_PAGE_LIMITAny language encodes to > 15 CBS pages
INVALID_ARGUMENTREASON_GEO_UNKNOWN_CELLScell_ids[] contains unknown cells
INVALID_ARGUMENTREASON_POLYGON_OVERSIZEDPolygon area > national area
UNAUTHENTICATEDREASON_SIGNATURE_INVALIDHSM verify = SIGNATURE_INVALID
UNAUTHENTICATEDREASON_CLOCK_SKEW`
UNAUTHENTICATEDREASON_REPLAYNonce reused
PERMISSION_DENIEDREASON_CALLER_NOT_REGISTEREDCert subject not in registry
PERMISSION_DENIEDREASON_CALLER_SCOPE_VIOLATIONSeverity or region outside caller scope
FAILED_PRECONDITIONREASON_HSM_UNAVAILABLEHSM session cannot be opened
RESOURCE_EXHAUSTEDREASON_RATE_LIMITEDPer-caller rate limit (default 10 reqs/min for P0_EXTREME)

UC-02: PerMnoDispatch (worker)

Trigger. DispatcherWorker observes state = ACCEPTED on cbc.broadcasts. Concurrency limit: cbcDispatcherMaxConcurrent (default 8 — emergencies rare).

Steps:

  1. Advisory lock. pg_try_advisory_xact_lock(broadcastIdHashed) — exactly-once worker pickup.
  2. State transition. ACCEPTED → DISPATCHING; append cbc.audit row with transition = DISPATCHED.
  3. Adapter dispatch fan-out. For each active MNO (from cbc.mno_bind_registry), in parallel with per-MNO 30 s deadline:
    • Load adapter strategy: STANDARD_3GPP (AWCC, Roshan), ERICSSON_PROPRIETARY (MTN_AF), HUAWEI_PROPRIETARY (Etisalat, Salaam) — configured per MNO.
    • UC-03 ResolveCellIds against per-MNO cbc.mno_cell_database.
    • UC-06 TranslateToCbs produces per-language CBS PDUs with Message Identifier, Serial Number, Data Coding Scheme, Page Parameter per 3GPP TS 23.041 §9.4.1.2.
    • Write cbc.mno_dispatches row with status = PENDING.
    • Invoke adapter: adapter.send(mnoBindId, cbsPdus, resolvedCellIds, timeout = 30s) over the MNO CBE endpoint (TCP over leased/IPSec per ADR-0004 §6).
    • On success → status = DISPATCHED + cbeAckReference; await async ACK.
    • On timeout → status = TIMEOUT; do not retry automatically (regulator must investigate).
    • On vendor error → status = FAILED with errorCode + errorDetail; open per-adapter circuit breaker after 3 consecutive vendor errors within 60 s.
  4. UC-04 AggregateAcks runs continuously and produces terminal state.

Budget. Sub-60 s dispatch target from ACCEPTED. The wall-clock budget is dominated by the slowest MNO (30 s cap) plus 3–5 s overhead. Per-MNO fail-open posture ensures a slow MNO does not block the others.


UC-03: ResolveCellIds

Trigger. Subroutine of UC-02.

Steps by geoTarget.kind:

KindAlgorithm
CELL_IDSValidate each cell id is present in per-MNO mno_cell_database. Unknown ids → fail UC-01 step 8 with REASON_GEO_UNKNOWN_CELLS.
POLYGONFor each cell in the MNO cell database, compute ST_Within(cell.geom, polygon) (PostGIS). Expand polygon by accuracyMeters on cells near edges (buffered match).
REGIONLoad region polygon from cbc.regions_geom (ISO-3166-2 province/district). Use POLYGON algorithm.
COUNTRYReturn every active cell for the MNO (no filtering).

The resolved cellIds[] is frozen into the MnoDispatch row — subsequent MNO cell-DB refreshes do not retroactively change audit evidence (DOMAIN §2.4 invariant).


UC-04: AggregateAcks

Trigger. Event consumer on cbc.adapter.ack.internal.v1 (emitted by adapter when CBE asynchronously acknowledges), and periodic sweeper running every 2 s over DISPATCHING broadcasts.

Steps:

  1. For broadcast bc_..., query cbc.mno_dispatches WHERE broadcastId = :id for the latest attempt per MNO.
  2. Compute:
    • ackedCount = count(status = ACKED)
    • failedCount = count(status ∈ {FAILED, TIMEOUT, REJECTED})
    • pendingCount = count(status ∈ {PENDING, DISPATCHED})
  3. Terminal-state rules:
    • pendingCount > 0 and elapsed since dispatchedAt < aggregatorDeadlineSeconds (default 45 s) → do nothing; wait.
    • ackedCount = totalMnosstate = ACKED; emit cbc.broadcast.acked.v1.
    • ackedCount ≥ 1 AND ackedCount < totalMnos (after deadline) → state = PARTIAL; emit cbc.broadcast.partial.v1.
    • ackedCount = 0 AND failedCount = totalMnosstate = FAILED; emit cbc.broadcast.failed.v1.
  4. Append cbc.audit row with appropriate transition.
  5. Update finalisedAt = now().

SLA. Finalisation within 60 s of BroadcastEmergency acceptance for ≥ 95% of broadcasts (per NFR catalog).


UC-05: CancelBroadcast (dual-control)

Trigger. Caller A invokes CancelBroadcast(broadcastId) within the 60 s dispatch window.

Steps:

  1. Verify signature (same path as UC-01 steps 1–4).
  2. Resolve caller. If caller ≠ broadcast.caller and caller.callerId ∉ broadcast.caller.dualControlPartnersPERMISSION_DENIED.
  3. SETNX cbc:cancel:pending:{broadcastId} {callerIdA} EX 60. If this is the first approver, return 202 AWAITING_APPROVAL with approvalToken.
  4. On the second call (different caller), GETDEL cbc:cancel:pending:{broadcastId} and verify caller is distinct.
  5. Transaction:
    • For each mno_dispatches WHERE status ∈ {PENDING, DISPATCHED}: invoke adapter cancel(cbeAckReference) best-effort; mark status = CANCELLED if adapter acks, else leave and record errorDetail = CANCEL_FAILED.
    • broadcast.state = CANCELLED; append cbc.audit row; emit cbc.broadcast.cancelled.v1.
  6. Return per-MNO breakdown of effective vs ineffective cancellations.

Invariant. MNOs already in ACKED cannot be cancelled (the broadcast has reached handsets in that MNO's network).


UC-06: TranslateToCbs (pre-flight)

Trigger. Subroutine of UC-01 (validation) and UC-02 (dispatch).

Converts a LanguageVariant into 3GPP TS 23.041 CBS PDU pages.

function translateToCbsPdu(headline, body, lang, severity, isDrill, serialNumber):
msgId = severityToMessageIdentifier(severity, isDrill)
# P0=4370, P1=4371, P2=4372 (DOMAIN §2.1), drill=4373..4379

dcs = (lang == 'en') ? 0x00 /* GSM-7 default alphabet */
: 0x11 /* UCS-2 */
# 3GPP TS 23.038 §5

localisedPrefix = isDrill ? localisedDrillPrefix(lang) : ""
fullText = localisedPrefix + headline + '\n' + body

# Encode per DCS
bytes = (dcs == 0x00) ? encodeGsm7(fullText)
: encodeUcs2BigEndian(fullText)

# Page sizing per 3GPP TS 23.041 §9.4.1.2
bytesPerPage = (dcs == 0x00) ? 82 : 82 # 82 octets of user data per page
pages = chunk(bytes, bytesPerPage)

if len(pages) > 15:
raise INVALID_ARGUMENT(REASON_PAGE_LIMIT)

return [
CbsPduPage(
messageIdentifier = msgId, # 16-bit
serialNumber = serialNumber, # 16-bit
dataCodingScheme = dcs, # 8-bit
pageParameter = (pageNo << 4) | totalPages, # 8-bit
cbData = pages[pageNo-1]
)
for pageNo in 1..len(pages)
]

function localisedDrillPrefix(lang):
# Per CBC-US-015; translations maintained in customer-portal glossary (EP-CUST-09)
return {
'en': 'DRILL - NO ACTION REQUIRED\n',
'fa': 'تمرین - هیچ اقدامی لازم نیست\n',
'ps': 'تمرین - هیڅ اقدام اړین نه دی\n',
'ar': 'تمرين - لا يتطلب أي إجراء\n',
}[lang]

The resulting PDU list is wrapped in an adapter-specific envelope (standard-3GPP, Ericsson, or Huawei) before being sent to the CBE.


UC-07: VerifyAuditChain (background worker)

Trigger. Cron 0 3 * * * (03:00 Asia/Kabul) via AuditVerifierWorker. Distributed lock cbc:audit:lock:verifier (EX 3600).

Steps:

  1. Scan yesterday's partition of cbc.broadcasts and cbc.audit in primary-key order.
  2. For each row, recompute rowHash = sha256(canonicalJson(row minus rowHash) ‖ prevHash).
  3. Compare to stored rowHash.
  4. On mismatch → emit cbc.audit.chain.broken.v1, page on-call (CbcAuditChainBroken CRITICAL), open incident IR-CBC-{date}.
  5. On success → emit cbc.audit.chain.verified.v1 with verifiedUpto = maxOccurredAt.

The verifier is idempotent; re-running only re-emits the verified event.


UC-08: ScheduleDrill

Trigger. Platform admin invokes POST /v1/cbc/drill or internal DrillSchedulerWorker cron.

Steps:

  1. Create cbc.drills row with state = SCHEDULED.
  2. Emit cbc.drill.scheduled.v1.
  3. At scheduledAt, the scheduler:
    • Generates a system-caller signed BroadcastEmergencyRequest using the internal NDMA-surrogate cert (held in HSM slot cbc/drill-initiator).
    • Calls UC-01 with isDrill = true, severity = P2_ADVISORY (drills default advisory), CBS test-slot Message Identifier (4373..4379, round-robin).
    • Awaits finalisation.
  4. After finalisation, DrillReportWorker:
    • Computes per-MNO ack latency, resolvedCellIds count, partial-failure breakdown.
    • Renders after-action report (HTML + PDF) to object storage.
    • Emails NDMA + Ghasi NOC + ATRA observer (per CBC-US-016).
    • Emits cbc.drill.completed.v1 with reportRef.

UC-09: RefreshMnoCellDatabase

Trigger. CellDbRefreshWorker cron 0 2 * * 1 (02:00 Monday Asia/Kabul).

Steps:

  1. For each MNO with an export endpoint:
    • Fetch GET https://{mno-endpoint}/cell-export?since={lastSnapshotAt} over IPSec or leased link, authenticated with per-MNO API key (Vault secret/cbc/mno/{mno}/cell-export-key).
    • Validate schema (cellId, lat, lng, accuracyMeters, optional decommissioned).
    • Insert into a staging table.
    • Atomic swap — in a single transaction, bump snapshotVersion, replace rows for mnoId, emit cbc.mno.cell.refreshed.v1.
    • Coverage metric: percent of national-area polygons (100 synthetic test polygons) resolvable to ≥ 1 cell. Alert CbcCellDatabaseStale if below 85%.
  2. Manual upload path (admin REST) executes the same staging+swap path.

UC-10: VerifyAuthorisedCaller (admin diagnostic)

Trigger. Admin invokes POST /v1/cbc/callers/verify with a candidate certificate + sample signature.

Steps:

  1. Execute UC-01 steps 1–6 without persisting a broadcast. Returns a synthetic VerifyResult.
  2. Use case: Legal or NDMA onboarding a new caller, verifying their PKI chain before the mouRef is counter-signed.

2. Performance & Budgets

OperationTargetEnforcement
BroadcastEmergency P951 500 msHSM verify ≤ 300 ms; DB write ≤ 50 ms
BroadcastEmergency P993 000 msBudget guard in handler
Dispatch-to-finalisation P9560 sAggregator sweeper 2 s cadence; per-MNO 30 s timeout
Per-MNO adapter send P955 sCircuit breaker opens after 3 consecutive failures in 60 s
Drill cadence skew≤ 5 min from scheduledScheduler cron + backlog retry
Audit-chain verifier runtime≤ 30 min over 24 h partitionParallelism 4 workers; checkpointed
HSM C_Verify P95300 msPKCS#11 session pool size 8 per pod

3. Concurrency & Consistency

  • Broadcast acceptance. Single-leader via Postgres row-version on cbc.broadcasts; hash chain requires serial append within partition.
  • Dispatch workers. Multiple replicas; advisory lock pg_try_advisory_xact_lock keyed by broadcastId.
  • Per-MNO dispatch. Independent per MNO; no ordering across MNOs.
  • Cancel race. The SETNX first-approver lock plus transactional status ∈ {PENDING, DISPATCHED} check prevents cancel vs ack races: if the adapter acks while cancel is pending, the MNO's row progresses to ACKED and the cancel is marked ineffective for that MNO.
  • Audit chain. Single append path — DispatcherWorker and BroadcastEmergency handler both serialize on the partition's advisory lock (pg_advisory_xact_lock(hashtext('cbc.audit.' || partition_name))) to guarantee prevHash correctness.

4. Idempotency

OperationKeyTTLReturn
BroadcastEmergencycbc:idem:{requestId}24 hOriginal broadcastId
CancelBroadcastcbc:cancel:pending:{broadcastId}60 sOriginal approvalToken
ScheduleDrill (cron)cbc:drill:sched:{yyyy-mm-dd}1 dayOriginal drillId
MnoCellDb refreshcbc:celldb:{mnoId}:{yyyy-mm-dd}1 dayOriginal snapshotVersion

Idempotency values are written to Redis before the DB transaction so a crash between Redis SETNX and DB commit leaves the request re-playable.


5. Ports (hexagonal adapters)

PortInbound/OutboundImplementations
GrpcBroadcastPortInboundgRPC server on :50061 (mTLS)
RestAdminPortInboundHTTPS on :3061 fronted by Kong
HsmPortOutboundPKCS#11 via pkcs11.so sidecar (softhsm2 locally; Thales Luna / nCipher nShield in prod)
CbeAdapterPortOutboundStandard3gppCbeAdapter, EricssonProprietaryCbeAdapter, HuaweiProprietaryCbeAdapter (one per MNO)
NatsPublisherPortOutboundJetStream on CBC_EVENTS stream
VaultSecretsPortOutboundVault AppRole; per-MNO CBE creds at secret/cbc/mno/{mno-id}/cbe-credentials
OcspCrlPortOutboundOCSP stapling verifier + CRL fetch cached in Redis 4 h
ObjectStorePortOutboundS3-compatible (MinIO / sovereign) — drill reports, PKI cert archive

Ports are thin interfaces; core domain logic depends only on ports, not implementations (swap CbeAdapterPort impl per MNO without touching domain code).


6. Rule Evaluation & Priority Lanes

CBC broadcasts bypass the compliance-engine evaluation pipeline (no tenant-content policy applies to authorised government broadcasts; ADR-0004 §8 places CBC on the P0 lane). The service enforces its own minimal gating:

  1. Authorised-caller registry check (UC-01 step 5–6).
  2. HSM-bound signature verify (UC-01 step 4).
  3. Anti-replay nonce window (UC-01 step 2).
  4. Language coverage for severity (DOMAIN §2.1.1).
  5. Geographic target within caller.allowedRegions.

No further content-policy evaluation is performed. The regulator accepts this by virtue of the mouRef governing each AuthorisedCaller.


7. Release (cancellation) pathway

Cancellation is authoritative — once accepted, the broadcast transitions to CANCELLED and the NOC + regulator consumers receive cbc.broadcast.cancelled.v1. Re-submitting the "same" broadcast must use a new requestId; there is no "un-cancel" semantic.


8. Drill vs Real Broadcast at Runtime

Drills share 100% of the hot path with real emergencies — same handler, same worker, same adapters, same audit. The only differences, enforced at code-level by the domain layer:

  • isDrill = true ⇒ CBS Message Identifier constrained to [4373, 4379].
  • isDrill = true ⇒ every language variant body is prepended with localisedDrillPrefix(lang) (UC-06).
  • isDrill = truecbc.audit.v1 carries transition = DRILL_EXECUTED instead of DISPATCHED; cbc.drill.* events emitted in addition.
  • isDrill = true ⇒ no ATRA-reporting events (regulator receives drills via a separate cbc.drill.completed.v1 report).