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:
@glyph-oss/connectvalidates the envelope, result, canonical payload, hashes, and expected bindings.- Your dApp uses
k12andverifyfrom@qubic.org/crypto@1.0.0for 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:
versionisglyph-connect-callback-envelope/2.proof.algorithmisqubic-schnorrq-sha256.resulthas a known status/type shape and matches the expected nonce and type.payload.request_hashmatches the original envelope whenexpectedRequestHashis set.payload.networkmatches the original network whenexpectedNetworkis set.payload.dapp_originmatches the canonical original origin whenexpectedDappOriginis set.payload.expmatches the original expiry, includingnull, whenexpectedExpis set.payload.relay.callback_urlmatches the expected callback binding whenexpectedCallbackUrlis set.payload.nonceandpayload.request_typematch the parsed result.payload.result_hashis the SHA-256 hash of canonical JSON forresult.proof.signed_payloadis canonical JSON forpayload.proof.public_keyis intrustedPublicKeyswhen 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.
