Skip to content

fix: self-issuance/self-connection OOB and DID exchange failures - #4174

Open
nb-vivek-bodar wants to merge 6 commits into
openwallet-foundation:mainfrom
Northern-Block:fix/self-issuance-oob-3300-main
Open

fix: self-issuance/self-connection OOB and DID exchange failures#4174
nb-vivek-bodar wants to merge 6 commits into
openwallet-foundation:mainfrom
Northern-Block:fix/self-issuance-oob-3300-main

Conversation

@nb-vivek-bodar

@nb-vivek-bodar nb-vivek-bodar commented Jul 17, 2026

Copy link
Copy Markdown

Description

Fixes self-issuance / self-connection failures when an agent creates an out-of-band invitation and receives it back into its own wallet:

  • StorageDuplicateError in find_oob_record_for_inbound_message: a self-connection produces two OobRecords (role=sender and role=receiver) sharing one invi_msg_id. Now disambiguated by the recipient verkey the message was delivered to, with a message-direction heuristic as fallback (didexchange request / handshake-reuse always belong to the role=sender record).

  • DIDXManagerError ("No corresponding connection request found") in accept_complete: the OOB connection-reuse cleanup deleted the wallet's own mirror ConnRecord (reciprocal DIDs) while it was still awaiting its own DIDXComplete. The reciprocal-DID case is now detected and skipped; genuine stale-connection-reuse cleanup is unchanged.

  • ConnRecord state regression in self-connections: removed two redundant post-send ConnRecord.state saves that could overwrite a state the (synchronous, self-addressed) delivery had already advanced.

  • record-not-found problem report for the holder's request-credential (invitation with both a handshake protocol and an attached offer): after the handshake, the receiver-role OobRecord is deleted, so the inbound attached offer's pthid lookup matched and consumed the wallet's own role=sender OobRecord. A receiver-bound message-type guard now prevents attached offer / presentation-request messages from ever consuming a role=sender OobRecord. Additionally, V20CredManager.receive_request falls back to a thread/role-only lookup when the connection-filtered lookup misses (the issuer record was created connectionless), while rejecting records already bound to a different connection.

  • Multitenant: StorageNotFoundError dispatching an OOB attached message (Record not found: connection/...): for OOB attached messages the inbound message carries a connection_id, and Dispatcher.handle_v1_message resolved that ConnRecord using the dispatcher's own root profile instead of the profile the message was dispatched with. In a multitenant agent the connection lives in the recipient subwallet, so the attached message (e.g. the credential offer) was never handled whenever a connection was associated with it (existing-connection reuse, or handshake completed before attachment processing). The lookup now uses the message's profile, consistent with the find_inbound_connection branch directly below; in a single-wallet agent both profiles are the same object, so behavior there is unchanged.

Normal two-party flows are unaffected: the OOB guard only fires when an inbound offer / presentation request resolves to the wallet's own role=sender record (impossible in a two-party exchange), and the receive_request fallback only runs where the previous code already failed with StorageNotFoundError.

Regression tests are included for every failure point, parametrized across askar and askar-anoncreds, including guards that normal two-party OOB / did-exchange flows still behave as before. Verified manually end-to-end in a multitenant deployment (invitation with handshake + attached offer received into the same wallet, through to the credential stored and both exchange records reaching done).

Related Issue

Closes #3300

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Chore / maintenance

Checklist

  • I have read the Contributing Guide
  • My changes follow the existing code style
  • I have added/updated tests where applicable
  • I have updated documentation if needed

Fixes two related failures reported in openwallet-foundation#3300 when an agent creates an
out-of-band invitation and receives it back into its own wallet
(self-issuance / self-connection):

- oob_processor.find_oob_record_for_inbound_message raised
  StorageDuplicateError because two OobRecords (role=sender and
  role=receiver) share the same invi_msg_id in a self-connection.
  Disambiguate using the inbound message type: messages a receiver
  always sends back to a sender (didexchange request, handshake-reuse)
  belong to the role=sender record; other messages (e.g. attached
  protocol messages like credential offers) belong to the
  role=receiver record.

- DIDXManager.accept_complete raised DIDXManagerError ("No
  corresponding connection request found") because the OOB
  "connection reuse" cleanup in find_oob_record_for_inbound_message
  deleted the wallet's own other-role ConnRecord, mistaking it for a
  stale connection superseded by a legitimate RFC 0434 connection
  reuse. In a self-connection, that "old" ConnRecord is still awaiting
  its own DIDXComplete. Detect this case by checking for reciprocal
  DIDs between the "old" and current connection records (unique to a
  self-connection) and skip the deletion when it applies, leaving the
  genuine stale-connection-reuse cleanup path unchanged.

Also removes two redundant ConnRecord.state saves (in
DIDXManager.receive_invitation and DIDXRequestHandler.handle) that ran
after sending a message whose delivery can, for a self-connection,
synchronously advance the same ConnRecord further before the redundant
save executes, regressing it back to an earlier state.

Added regression tests parametrized across askar and askar-anoncreds
wallet backends covering: self-issuance via an OOB attachment without
a prior connection, a full connectionless self-issuance round trip, a
self-connection did-exchange handshake reaching completion for both of
the wallet's own ConnRecords, and confirmation that normal two-party
OOB/did-exchange flows are unaffected.

Signed-off-by: Vivek <vivek@northernblock.io>
Companion doc for the self-issuance/self-connection fix, detailing
the two root causes from openwallet-foundation#3300 and how each was resolved.

Signed-off-by: Vivek <vivek@northernblock.io>
…d-not-found

In a self-connection created from an OOB invitation carrying both a
handshake protocol and an attached credential offer (issue openwallet-foundation#3300), the
holder's request-credential was answered with a problem report
(record-not-found) and both exchange records ended up abandoned:

- After the handshake, the receiver-role OobRecord is deleted (normal
  cleanup), so the inbound attached offer's pthid lookup matched the
  wallet's own role=sender OobRecord and consumed (deleted) it, even
  though an offer always flows sender -> receiver and can never belong
  to the sender's own record. Add a receiver-bound message-type guard
  in find_oob_record_for_inbound_message so attached offer/presentation
  request messages never consume a role=sender OobRecord.

- With no OobRecord left when the request-credential arrived,
  V20CredManager.receive_request filtered by the inbound connection id,
  which can never match an issuer exchange record created as an OOB
  attachment (connection_id=None). Fall back to a thread/role-only
  lookup and adopt the connection on the record.

Either change alone unblocks the flow; together they are
defense-in-depth. Regression tests added for both.

Signed-off-by: Vivek <vivek@northernblock.io>
For OOB attached messages the inbound message carries a connection_id,
and Dispatcher.handle_v1_message resolved that ConnRecord using the
dispatcher's own root profile instead of the profile the message was
dispatched with. In a multitenant agent the connection lives in the
recipient subwallet, so the lookup raised StorageNotFoundError and the
attached message (e.g. a credential offer) was never handled.

Use the profile passed in with the message, consistent with the
find_inbound_connection branch directly below. In a single-wallet agent
both profiles are the same object, so behavior there is unchanged.

Signed-off-by: Vivek <vivek@northernblock.io>

@swcurran swcurran left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the "...OOBFix.md" documentation as a permanent artifact. Documentation of bugs that are fixed go into the PR, and the CHANGELOG when we release. If it is useful to document "Self-Connection" as a feature (which we probably shouldn't), that might be reasonable, but not how something that should work was broken and then fixed.

Suggest removing that, and either not including anything, or finding a place in the existing documentation to say "self-connection is supported" and any details an implementer needs to know.

@sonarqubecloud

Copy link
Copy Markdown

Bug-fix narratives belong in the PR description and release
CHANGELOG, not as a permanent docs page.

Signed-off-by: Vivek <vivek@northernblock.io>
@nb-vivek-bodar

Copy link
Copy Markdown
Author

Removed the doc file. That's already covered in the PR description, so I don't think we need a separate page for it.

@nb-vivek-bodar
nb-vivek-bodar requested a review from swcurran July 21, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error with out-of-band protocol while self-issuance

2 participants