glyphdocs
Qubic tooling

Verify signed callbacks

Strictly verify v2 callback bindings with @qubic.org/crypto.

Verify signed callbacks

Glyph Wallet 0.16.5 signs v2 callback envelopes, including explicit user rejections. The signed envelope binds the returned result to the request and its delivery route. Verification has two distinct parts:

  1. @glyph-oss/connect validates the envelope, result, canonical payload, hashes, and expected bindings.
  2. Your dApp uses k12 and verify from @qubic.org/crypto@1.0.0 for the final Qubic SchnorrQ check.

Do both before using an identity, transaction hash, message signature, permission list, or verification result.

For the conceptual flow and the difference between Relay read access and wallet callback delivery, see Understand Glyph Connect and Relay v2.

Strict server handler

Keep the original request and envelope available when the callback is received:

import { k12, verify } from "@qubic.org/crypto";
import {
  verifyCallbackEnvelope,
} from "@glyph-oss/connect";

const body = await request.json();
const result = await verifyCallbackEnvelope(body, {
  expected: {
    nonce: originalRequest.nonce,
    type: originalRequest.type,
  },
  expectedRequestHash: originalEnvelope.request_hash,
  expectedNetwork: originalEnvelope.network,
  expectedDappOrigin: originalRequest.dapp.origin,
  expectedExp: originalRequest.exp ?? null,
  expectedCallbackUrl: originalEnvelope.callback,
  requireSigned: true,
  trustedPublicKeys: [trustedWalletCallbackPublicKey],
  verifySignature: ({ algorithm, payload, signature, publicKey }) => {
    if (algorithm !== "qubic-schnorrq-sha256") return false;
    return verify(k12(payload, 32), signature, publicKey);
  },
});

The SDK passes the UTF-8 bytes of canonical signed_payload, plus decoded signature and public key bytes. @qubic.org/crypto expects the signed message digest, so hash those exact bytes with k12(payload, 32) before calling verify. This verifier runs in the dApp; Glyph Wallet is the signer and keeps the key material. Do not replace strict callback verification with a browser-held private key or shape-only parsing.

What the SDK checks

For a signed envelope, the SDK checks all of the following:

  • version is glyph-connect-callback-envelope/2.
  • proof.algorithm is qubic-schnorrq-sha256.
  • result has a known status/type shape and matches the expected nonce and type.
  • payload.request_hash matches the original envelope when expectedRequestHash is set.
  • payload.network matches the original network when expectedNetwork is set.
  • payload.dapp_origin matches the canonical original origin when expectedDappOrigin is set.
  • payload.exp matches the original expiry, including null, when expectedExp is set.
  • payload.relay.callback_url matches the expected callback binding when expectedCallbackUrl is set.
  • payload.nonce and payload.request_type match the parsed result.
  • payload.result_hash is the SHA-256 hash of canonical JSON for result.
  • proof.signed_payload is canonical JSON for payload.
  • proof.public_key is in trustedPublicKeys when a trust list is supplied.
  • The caller-provided verifier returns true.

For official Relay v2 callback URLs, the callback binding is normalized before comparison so the signed payload does not need to expose the write secret itself. Pass the exact callback URL from the original envelope to expectedCallbackUrl.

Require signed input

Set requireSigned: true for every callback or Relay subscription that must be authenticated. If the body is an unsigned result, the verifier throws instead of falling back to shape parsing.

Any of these options also causes parseOrVerifyCallback() to require a signed envelope: expectedRequestHash, expectedNetwork, expectedDappOrigin, expectedExp, expectedCallbackUrl, trustedPublicKeys, or verifySignature.

Do not trust a parsed result alone

This accepts JSON shape but does not prove who produced the result:

const result = parseCallbackResponse(body, expected);

Use it only for an explicitly unsigned integration where authenticity is provided by another channel. For v2 Wallet callbacks, use verifyCallbackEnvelope() and a real SchnorrQ verifier instead.

On this page