Skip to content

Provisioned credential values are routed to subscribers and persisted indefinitely in the event log (§9.8.2, §14) #91

@nficano

Description

@nficano

Category: security Severity: blocker
Location: Sources/ARCP/Runtime/ARCPRuntime.swift:147-151, Sources/ARCP/Runtime/SubscriptionManager.swift:75-86, Sources/ARCP/Store/EventLog.swift:37-86
Spec: ARCP v1.1 §9.8.2, §14 ("Credential confidentiality")

What

job.accepted envelopes carry credentials[].value (real bearer secrets). Every outbound envelope, including job.accepted, goes through ARCPRuntime.send, which (a) persists the full envelope JSON — secret included — into the SQLite event log forever (no redaction, no deletion at job end), and (b) routes it through SubscriptionManager.route, which wraps the whole envelope (secret included) in a subscribe.event and delivers it to every subscriber whose filter matches.

The spec is explicit: subscribers MUST NOT receive credentials they did not submit; value MUST NOT be logged or persisted beyond the job. Here a dashboard/auditor subscribed with an empty or job.accepted-matching filter receives live credential material, and the event log retains it indefinitely (the resume replay at handleResume will even re-transmit it).

Evidence

private func send(_ envelope: Envelope, transport: any Transport) async throws {
    try await transport.send(envelope)
    try? await persistOptional(envelope, sessionId: envelope.sessionId)   // writes value to SQLite
    await subscriptionManager.route(envelope: envelope)                    // fans value to subscribers
}

job.accepted is sent through JobManager.sendrawSend → this method. SubscriptionManager.route wraps the envelope verbatim:

payload: .subscribeEvent(SubscribeEventPayload(event: encode(envelope)))

and EventLog.append stores envelope_json (the full JSON) with no field stripping.

Proposed fix

  1. In JobManager/ARCPRuntime, emit job.accepted with credentials to the owning transport only; build a redacted copy (credentials removed) for both persistOptional and subscriptionManager.route. Concretely, in ARCPRuntime.send, detect .jobAccepted payloads with non-nil credentials, and pass a redacted variant to persistOptional and route.
  2. Add the same redaction to handleResume replay so credential material is never re-transmitted on resume.
  3. Add a test: subscribe with empty filter, submit a job that provisions credentials, assert the subscribe.event payload's job.accepted has credentials: null; and assert EventLog.replay returns the job.accepted with credentials stripped.

Acceptance criteria

  • A subscriber never receives a non-redacted credentials array for a job it did not submit.
  • The event log never stores credentials[].value.
  • Resume replay does not re-transmit credential values.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions