Skip to content

POST / dispatcher must read attachments from CESR-ATTACHMENT header (keripy wire format) #4

Description

@seriouscoderone

Summary

POST / returns the signed receipt only when the request body is the full CESR stream — event JSON plus all inline attachments (signatures, seal-source couples, datetime). Standard keripy HTTP-witness clients (keri.app.agenting.HTTPMessengerkeri.app.agenting.streamCESRRequests) don't put attachments in the body: they split them into the CESR-ATTACHMENT HTTP header and send only the event JSON in the body. The kerihost witness dispatcher reads only the body, sees no inline signatures, can't verify the event, and returns HTTP 204 No Content with an empty body instead of a signed receipt.

End-to-end effect: every Locksmith wallet (and any other standard keripy wallet using stock WitnessReceiptor) ends up with baser.wigs = 0 on inceptions it submits to keri.host. The wallet's WitnessReceiptor.receiptDo then loops indefinitely on "Waiting for witness receipts for ...", even though the Lambda fires and accepts the event.

This is the fourth in the keri.host compatibility series:

Diagnostic

Standard keripy streamCESRRequests in keri/app/agenting.py (keripy 2.0.0.dev6) splits each CESR event into the request body (the event JSON or msgpack) and the CESR-ATTACHMENT header (the attachments — signatures, seal source couples, datetime stamps, etc.). I captured a real wallet-produced request:

POST /
Content-Type: application/cesr
Content-Length: 345                                     ← only the JSON event
CESR-ATTACHMENT: -VAn-AABAAA...AAA1AAG2026-05-22T22c05c55d990493p00c00
CESR-DESTINATION: BNbRfMPQQge6wKO0uof9Y0e_QYOfiF08k9drc6pOgzjt

{"v":"KERI10JSON000159_","t":"icp","d":"EE56...","i":"EE56...",...}

The body has zero CESR attachments. All signatures + the datetime stamp are in the CESR-ATTACHMENT header value as a CESR-encoded counter group.

Repro

Same EE56 event posted two different ways:

Way 1: everything inline in body (curl --data-binary @file) → 200 + receipt

$ curl -X POST "https://witness.keri.host/" \
       -H "Content-Type: application/cesr" \
       --data-binary @/tmp/inception-full.cesr     # 505 bytes: event + sig + datetime
HTTP/2 200
content-type: application/cesr
content-length: 281

<rct event + -CAB<witness eid><signature> attachment>

Way 2: keripy wire format (body=event-only, sigs in header) → 204 empty

$ curl -X POST "https://witness.keri.host/" \
       -H "Content-Type: application/cesr" \
       -H "CESR-DESTINATION: BNbRfMPQQge6wKO0uof9Y0e_QYOfiF08k9drc6pOgzjt" \
       -H "CESR-ATTACHMENT: -VAn-AABAAA..." \
       --data-binary @/tmp/event-only.json          # 345 bytes: just the icp JSON
HTTP/2 204
content-length: 0

Same event SAID (EE56hav20fixmmszoObJomTHBLPirN_yCs2h8NtrQFTZ), same witness, same destination. Only the wire-format split differs. The receipt only comes back for Way 1.

Locksmith's wallet always sends Way 2. The Lambda IS invoked (verified via CloudWatch), it just returns 204 because it has nothing to verify.

Reference implementation

keri.app.indirecting.HttpEnd.on_post (keripy reference HTTP-witness server) reassembles the full CESR stream from body + headers['CESR-ATTACHMENT'] before parsing. Roughly:

async def on_post(self, req, rep):
    cr = httping.parseCesrHttpRequest(req=req)
    # cr.payload is the JSON event; cr.attachments is the CESR-ATTACHMENT header value
    full_stream = (
        json.dumps(cr.payload).encode() if isinstance(cr.payload, dict)
        else cr.payload
    ) + cr.attachments
    self.exc.parser.parse(ims=bytearray(full_stream), ...)

The parseCesrHttpRequest helper in keri.app.httping does the body+header reassembly. Any standard keripy witness binary built on top of keripy will use this path implicitly.

Suggested fix

In the kerihost witness-handler Lambda's request-decoding step (the part that runs before the CESR parser), reassemble the full stream from body + CESR-ATTACHMENT header before passing to the parser:

// Pseudocode for the Rust handler
let body = event.body.unwrap_or_default();
let attachment = event.headers
    .get("CESR-ATTACHMENT")
    .or_else(|| event.headers.get("cesr-attachment"))    // case-insensitive
    .map(|v| v.as_bytes())
    .unwrap_or_default();

let full_stream: Vec<u8> = [body.as_bytes(), attachment].concat();
parser.parse(&full_stream)?;

After that change, the existing receipt-on-fresh-event logic (#3's fix) will see the inline signatures it needs to verify, and standard keripy wallets will get the receipt they expect.

Test that would have caught this

def test_post_root_parses_cesr_attachment_header():
    # Build an inception event using a hab
    icp_serder, sigs = hab.makeOwnEvent(sn=0)
    body = icp_serder.raw                       # just the JSON
    attachment = sigs                           # signatures as CESR

    response = client.post(
        "/",
        headers={
            "Content-Type": "application/cesr",
            "CESR-DESTINATION": witness_pre,
            "CESR-ATTACHMENT": attachment.decode(),
        },
        data=body,
    )

    assert response.status_code == 200, "should return 200 + receipt for split-attachment request"
    assert response.headers["content-type"] == "application/cesr"
    assert len(response.content) > 0, "should include the receipt CESR in the body"
    # Parse the response and confirm it's a `rct` event + -CAB attachment

This complements the existing test_post_root_returns_synchronous_witness_receipt (which appears to test the all-in-body shape only).

Environment

  • Witness AID: BNbRfMPQQge6wKO0uof9Y0e_QYOfiF08k9drc6pOgzjt
  • Deployment: serverless-witness stack in us-east-1
  • keripy: 2.0.0.dev6
  • Reference for the wire-format expectations: keri.app.agenting.streamCESRRequests (sender) and keri.app.indirecting.HttpEnd.on_post (receiver)
  • Test AID: EE56hav20fixmmszoObJomTHBLPirN_yCs2h8NtrQFTZ in joseph vault — wits=[BNbRf...], toad=1, wigs=0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions