Skip to content

feat(baileys): add passkey companion-linking ceremony - #2628

Open
mateus2001ferreira wants to merge 1 commit into
evolution-foundation:mainfrom
mateus2001ferreira:feat/passkey-companion-linking
Open

feat(baileys): add passkey companion-linking ceremony#2628
mateus2001ferreira wants to merge 1 commit into
evolution-foundation:mainfrom
mateus2001ferreira:feat/passkey-companion-linking

Conversation

@mateus2001ferreira

Copy link
Copy Markdown

Description

Adds opt-in support for WhatsApp's passkey (Shortcake/CRSC) companion-linking flow, which some numbers now require when linking a device. Implemented on top of Baileys' public API — no fork or patch of Baileys needed.

How it works:

  • A browser helper extension performs the WebAuthn assertion on the web.whatsapp.com origin.
  • PasskeyCeremony handles the server's passkey_prologue_request / crsc_continuation notifications and replies with the matching IQs (passkey_prologue, companion_nonce, encrypted_pairing_request).
  • Public endpoints GET/POST /passkey-ceremony/:token[/response|/confirm] let the extension read the challenge and submit the assertion; the current stage and open URL are exposed via connectionState.

Gated behind PASSKEY_CEREMONY_ENABLED=true (default off), since a malformed IQ can get a number banned. Also requires:

  • PASSKEY_PUBLIC_URL — API base reachable by the browser running WhatsApp Web
  • CORS_ORIGIN allowing * or https://web.whatsapp.com

Crypto is an independent reimplementation of whatsmeow's pair-passkey.go (MPL-2.0); no code copied. No dependency changes (works on the current Baileys 7.0.0-rc.9).

Type of Change

  • New feature (non-breaking change which adds functionality)

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • My changes generate no new warnings
  • I have made corresponding changes to the documentation
  • I have manually tested my changes thoroughly
  • Any dependent changes have been merged and published

Additional Notes

The feature ships a small browser helper extension (public/passkey-helper.zip) that performs the WebAuthn assertion. Happy to discuss whether shipping it in-repo, as source, or as a separate distribution is preferable. Also glad to add the two env vars to .env.example if the approach is accepted.

Add opt-in support for WhatsApp's passkey (Shortcake/CRSC) companion-linking
flow, implemented on top of Baileys' public API without forking it. A browser
helper extension performs the WebAuthn assertion; the ceremony is driven via
public /passkey-ceremony endpoints and exposed through connectionState.
Gated behind PASSKEY_CEREMONY_ENABLED (default off) since a malformed IQ can
get the number banned. Requires PASSKEY_PUBLIC_URL reachable by the browser.

@sourcery-ai sourcery-ai Bot 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.

Sorry @mateus2001ferreira, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@j4mb0re2

Copy link
Copy Markdown

✅ Passkey linking working with 1Password after additional fixes

Hi!

I would like to share the results of my tests with the new Passkey Companion Linking implementation from this PR.

After some debugging and modifications, I was able to successfully connect my existing Evolution API instance to WhatsApp using a passkey stored in 1Password.

Important: I am not a programmer/developer.

I use Evolution API for my own project. I used ChatGPT to analyze the Evolution API source code, this PR, the Evolution GO Passkey Helper extension and my server logs. ChatGPT also helped me create the modified Chrome extension and the patches described below.


My original problem

My Evolution API installation was working normally before WhatsApp started requiring passkey authentication during companion-device linking.

The QR Code was generated normally.

After scanning it with WhatsApp, however, WhatsApp requested passkey/device authentication and the Evolution API instance never completed the connection.

I was originally using:

  • Evolution API v2.3.6
  • Docker / Docker Swarm
  • Existing PostgreSQL database
  • Existing integrations already built around Evolution API

Because my project already depends on the original Evolution API database structure and integrations, migrating everything to Evolution GO would require significant changes.


Testing Evolution GO first

I installed Evolution GO to understand how the new authentication flow was supposed to work.

Evolution GO detected that WhatsApp required a passkey and generated a URL similar to:

https://web.whatsapp.com/#wapk=...

It also instructed me to install the official Evolution Passkey Helper Chrome extension.

However, I had problems completing the authentication with my setup and 1Password.

This gave us an important reference for how the passkey flow was intended to work.


Creating an alternative Passkey Helper

Using the Evolution GO Passkey Helper as a reference, I asked ChatGPT to analyze how it worked and create an alternative helper specifically for my Evolution API installation.

The architecture that finally worked was:

  • Chrome Manifest V3 extension
  • Background Service Worker
  • Content Script
  • A script running in Chrome's MAIN world on web.whatsapp.com
  • WebAuthn executed directly in the WhatsApp Web page context
  • navigator.credentials.get() used from the MAIN world
  • Communication between the extension and the Evolution API passkey ceremony endpoints

This was important because it allowed the WebAuthn request to be correctly detected by the 1Password Chrome extension.

After this change, 1Password opened normally and allowed me to authenticate using my existing WhatsApp passkey.


Evolution Manager does not display the Passkey URL

There is also an important UI problem.

When I perform the pairing through the Evolution Manager web interface, the QR Code appears normally.

After scanning the QR Code and WhatsApp requesting the passkey, the Evolution Manager does not show the URL required to continue the authentication.

Nothing appears in the web interface telling the user to open the Passkey Helper.

The URL only appeared in the Evolution API server logs:

[Passkey] prologue_request received. Open in the browser helper: https://web.whatsapp.com/#wapk=...

Because of this, I had to:

  1. Open the Docker/server logs
  2. Find the prologue_request
  3. Copy the https://web.whatsapp.com/#wapk=... URL
  4. Paste it manually into Chrome
  5. Let the helper extension process the ceremony
  6. Authenticate with 1Password

It would be very useful if Evolution Manager automatically displayed a button/link using passkeyOpenUrl whenever a passkey ceremony is requested.

For a normal user who does not have access to the server terminal, the current flow is very difficult to discover.


First problem after successful 1Password authentication

After getting WebAuthn/1Password authentication working, Evolution API successfully progressed further.

The helper displayed the confirmation code.

However, when confirming it, Evolution API returned:

ERROR [PasskeyController] confirm: ceremony has no encryption key yet

At the same time, I noticed that Evolution API continued generating/rotating QR Codes even though the passkey ceremony was already active.

For example:

qrcodeCount: 16

qrcodeCount: 17

qrcodeCount: 18

qrcodeCount: 19

The passkey ceremony had already started, but QR processing continued.


hasActiveByInstance() already existed

While analyzing the PR source code, we noticed that the passkey ceremony store already contains:

passkeyCeremonyStore.hasActiveByInstance(instanceId)

However, it did not appear to be used by the normal WhatsApp QR/reconnection handling.

Our hypothesis was that the connection/QR lifecycle could change while the passkey ceremony still depended on cryptographic state kept in memory.

That would explain why the confirmation UI existed but the controller later reported:

ceremony has no encryption key yet


Fix 1 — Stop QR rotation while Passkey Ceremony is active

We modified connectionUpdate() to detect an active passkey ceremony.

Conceptually, the change was:

const passkeyActive =
  process.env.PASSKEY_CEREMONY_ENABLED === 'true' &&
  passkeyCeremonyStore.hasActiveByInstance(this.instanceId);

if (qr && passkeyActive) {
  this.logger.log(
    '[Passkey] ceremony active - ignoring QR rotation to preserve cryptographic state',
  );
}

if (qr && !passkeyActive) {
  // existing QR processing
}

After this modification the server correctly reported:

[Passkey] ceremony active - ignoring QR rotation to preserve cryptographic state

This prevented QR rotation from interfering with the active ceremony.

The previous ceremony has no encryption key yet behavior was no longer the main blocker.

However, pairing still did not finish.


Second problem — WhatsApp/Baileys stream error 515

The next important event in the logs was:

stream:error code="515"

Initially, we also prevented Evolution API from reconnecting while the passkey ceremony was active.

The server reported:

[Passkey] ceremony active - suppressing reconnect to preserve cryptographic state

This successfully protected the ceremony state.

But there was another problem:

the WhatsApp connection never completed.

The browser remained processing and the device was not added to WhatsApp's linked devices.


Important discovery — reconnect must depend on the Passkey stage

We then changed the logic so reconnect behavior depends on the current passkey ceremony stage.

During these stages:

  • challenge
  • awaiting_confirmation
  • confirmation

the reconnect should be suppressed so that the cryptographic state is preserved.

But after:

  • confirmed

the reconnect must be allowed.

This was the key change that finally completed the pairing.


Fix 2 — Stage-aware reconnect

The working logic is currently:

const passkeyState =
  process.env.PASSKEY_CEREMONY_ENABLED === 'true'
    ? passkeyCeremonyStore.stateByInstance(this.instanceId)?.state.stage
    : undefined;

const passkeyInProgress =
  passkeyState === 'challenge' ||
  passkeyState === 'awaiting_confirmation' ||
  passkeyState === 'confirmation';

const shouldReconnect =
  !passkeyInProgress &&
  !codesToNotReconnect.includes(statusCode);

if (passkeyInProgress) {
  this.logger.log(
    `[Passkey] ceremony stage=${passkeyState} - suppressing reconnect to preserve cryptographic state`,
  );

  return;
}

if (passkeyState === 'confirmed') {
  this.logger.log(
    '[Passkey] ceremony confirmed - allowing reconnect to complete WhatsApp pairing',
  );
}

if (shouldReconnect) {
  await this.connectToWhatsapp(this.phoneNumber);
}

🎉 Successful result

After applying both changes, I performed a completely new pairing attempt.

The server received:

[Passkey] prologue_request received

I opened the generated #wapk URL in Chrome.

Our modified Passkey Helper triggered WebAuthn in the WhatsApp Web MAIN world.

1Password opened correctly.

I authenticated using my WhatsApp passkey.

The ceremony progressed.

WhatsApp/Baileys then returned:

stream:error code="515"

But this time Evolution API detected that the ceremony was already confirmed and logged:

[Passkey] ceremony confirmed - allowing reconnect to complete WhatsApp pairing

Immediately afterwards:

CONNECTED TO WHATSAPP

And the instance successfully connected.

I also tested the instance after pairing and message communication worked normally.


Final working flow

The successful flow in my environment is:

Evolution Manager generates QR
              ↓
WhatsApp scans QR
              ↓
WhatsApp requests Passkey
              ↓
passkey_prologue_request
              ↓
Evolution creates Passkey Ceremony
              ↓
#wapk URL generated
              ↓
Open URL in Chrome
              ↓
Passkey Helper executes WebAuthn
inside web.whatsapp.com MAIN world
              ↓
1Password detects the request
              ↓
Authenticate WhatsApp Passkey
              ↓
Signed WebAuthn assertion returned
to Evolution API
              ↓
Preserve Passkey cryptographic state
              ↓
Ignore QR rotation during ceremony
              ↓
Do not reconnect prematurely
              ↓
Passkey ceremony confirmed
              ↓
WhatsApp/Baileys stream 515
              ↓
Reconnect is now allowed
              ↓
CONNECTED TO WHATSAPP
              ↓
Messages working normally

Suggested improvements

Based on my tests, I believe there are three areas worth reviewing.

1. Display passkeyOpenUrl in Evolution Manager

The backend already generates the URL, but I could only see it in the server logs.

When WhatsApp requests a passkey, Evolution Manager should display something like:

"WhatsApp requires Passkey authentication — Open Passkey Helper"

using the generated passkeyOpenUrl.

This would make the feature usable without requiring terminal/server access.

2. Protect the active Passkey Ceremony from QR rotation

When a passkey ceremony is active, new QR updates should probably not replace/interfere with the connection state required by the ceremony.

Using passkeyCeremonyStore.hasActiveByInstance() to detect this solved the problem in my environment.

3. Make reconnect Passkey-stage aware

Completely disabling reconnect while a passkey ceremony exists is not sufficient.

In my test:

challenge / awaiting_confirmation / confirmation

needed the cryptographic state to be preserved.

But after:

confirmed

Evolution needed to allow the reconnect after the WhatsApp/Baileys 515 event.

This final reconnect is what actually completed the WhatsApp connection.


Test environment

  • Evolution API built from PR feat(baileys): add passkey companion-linking ceremony #2628
  • Evolution API reported version: v2.3.7
  • Docker / Docker Swarm
  • PostgreSQL
  • Google Chrome
  • 1Password browser extension
  • WhatsApp account with Passkey enabled
  • Custom Passkey Helper derived from studying the Evolution GO helper architecture

About the custom extension

I also have the modified Chrome Passkey Helper that worked with this setup.

It was created after analyzing the Evolution GO Passkey Helper and adapting the flow so WebAuthn runs in the WhatsApp Web MAIN world and can be handled by 1Password.

I can share the extension source code if it is useful for this PR.

Before publishing it, I would prefer that someone from the project reviews the implementation, since, as mentioned above, I am not a developer and the extension/code changes were created with ChatGPT's assistance.

I can also provide the exact git diff of the Evolution API modifications that are currently working on my server.

I hope these test results help with the Passkey implementation. Thanks for working on this feature!
evolution-go-passkey-helper-community.zip
evolutio-api-passkey-fix.patch
evolution-api-passkey-helper-public.zip

I am also attaching the generic version of the Chrome Passkey Helper used in my successful test. It contains no private server information or credentials and dynamically reads the Evolution API URL and ceremony token from the #wapk payload.
ca52f35fa29eb25dae064080885e2a9d3a1b5627727afda57599ae6c67d315cf

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.

2 participants