Sign In With QoreChain (SIWQ), version 1
Status: stable for version 1 of the message format. Source:
spec/siwq-v1.md.
SIWQ lets a QoreChain account prove control of its keys to an off-chain service.
The message follows the layout of
EIP-4361 and Sign In With Solana,
adapted to QoreChain addresses and chain ids. The signature has two parts: an
ADR-36 secp256k1 signature, which any Keplr-compatible wallet can produce with
signArbitrary, and an ML-DSA-87 (FIPS 204) signature by the account's
registered post-quantum key.
Message
${scheme://}${domain} wants you to sign in with your QoreChain account:
${address}
${statement}
URI: ${uri}
Version: 1
Chain ID: ${chain-id}
Nonce: ${nonce}
Issued At: ${issued-at}
Expiration Time: ${expiration-time}
Not Before: ${not-before}
Request ID: ${request-id}
Resources:
- ${resources[0]}
- ${resources[1]}
...
- Lines are separated by a single
\n; there is no trailing newline. scheme://is optional.domainis the RFC 3986 authority of the requesting site (host and optional port).addressis theqor1bech32 form of the account.- The statement line is optional. Without it the address is followed by two empty lines, as in EIP-4361.
Versionis1.Chain IDis a QoreChain chain id such asqorechain-vladi.Nonceis at least 8 alphanumeric characters chosen by the server, single use.- Timestamps are RFC 3339 / ISO 8601.
Expiration Time,Not Before,Request IDandResourcesare optional and appear in this order.
A verifier must re-render the parsed fields and refuse a message that does not match byte for byte.
Signatures
{
"signatureType": "ml-dsa-87-hybrid",
"classical": { "keyType": "secp256k1", "publicKey": "<base64>", "signature": "<base64>" },
"pq": { "algorithmId": 1, "signature": "<base64>" }
}
Classical part (ADR-36)
The account's secp256k1 key signs the amino JSON sign document below, serialized
with sorted keys and no whitespace, where data is the base64 of the UTF-8
message and signer the qor1 address:
{"account_number":"0","chain_id":"","fee":{"amount":[],"gas":"0"},"memo":"","msgs":[{"type":"sign/MsgSignData","value":{"data":"...","signer":"qor1..."}}],"sequence":"0"}
The digest is SHA-256 for secp256k1 accounts and KECCAK-256 for
eth_secp256k1 accounts; the signature is 64 bytes r || s, low-S.
publicKey is the 33-byte compressed key, which must derive to address under
keyType.
Post-quantum part
pq.signature = ML-DSA-87.Sign(sk, UTF-8("qorechain-siwq-v1|" + message))
The domain prefix keeps a sign-in signature from being usable as a transaction signature and the other way round (see Domain separation).
signatureType is secp256k1 when the pq part is absent. Verifiers should
refuse such sign-ins unless they explicitly accept classical-only accounts.
Verification
A verifier must check, in this order:
- the message parses and is in canonical form;
domainequals the domain the request came from;nonceequals the nonce the server issued for this session, and the nonce has not been used before;Chain IDis the expected network;Expiration Timeis in the future, andNot BeforeandIssued Atare not in the future (allowing a small clock skew);- the classical public key derives to
addressand the ADR-36 signature verifies; - the ML-DSA-87 signature verifies under the public key registered on chain
for
address(/qorechain/pqc/v1/account/{address}), not under a key supplied by the client.
Test vectors
vectors/qoresign-v1.json, field siwq. See Test vectors.