feat: add verifyOffchainMessage helper for signed v1 offchain messages#1725
feat: add verifyOffchainMessage helper for signed v1 offchain messages#1725amilz wants to merge 1 commit into
Conversation
…sages Adds a `verifyOffchainMessage` helper to `@solana/offchain-messages` that verifies a signed version 1 offchain message returned by an untrusted signer (eg. a wallet) in a single call. Verifying the returned signature alone is insufficient, because a compromised wallet can hand back a valid signature over data unrelated to what the dApp requested. The helper encodes the message the caller expected the signer to sign, asserts that it matches the bytes the signer reports having signed, and then asserts that the signature is a valid Ed25519 signature of those bytes by the signer. It supports version 1 messages only, matching the v1-only `solana:signOffchainMessage` wallet-standard feature. A new `SOLANA_ERROR__OFFCHAIN_MESSAGE__CONTENT_DOES_NOT_MATCH_EXPECTED` error code is thrown when the signed bytes do not match the expected message. Because encoding the expected message already enforces the spec's structural rules (non-empty and duplicate-free required signatories serialized in sorted order, and non-empty content), the helper does not re-implement those checks and instead surfaces the encoder's errors.
🦋 Changeset detectedLatest commit: b63dcdb The changes in this PR will be included in the next version bump. This PR includes changesets to release 47 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
In general I think this would be a good addition to Kit, various consumers of offchain message signing should be verifying that the message is as they expect.
I think this API is doing too much though, and in particular is tied to the v1 spec such that a hypothetical v2 would probably require a breaking change.
I think the easiest way to handle that is probably to make this v1 specific, and make it the responsibility of apps to call the appropriate version. That would move decoding to the app, but I think reducing the scope of this method would be good.
I'd then restructure it so that it has a signature like:
function assertV1OffchainMessagesEqual(
receivedMessage: OffchainMessageV1,
expectedMessage: OffchainMessageV1 // or omit `version`
)Then it would:
- check version of
receivedMessage, throw if not 1 - check content equal, throw otherwise
- check requiredSignatories equal, throw otherwise
I'd use different error codes for each check, with the precise expected/received value that mismatches in context. We should double check we don't consider that sensitive, but I think it'd make sense.
Then the consumer would:
await verifySignature(publicKey, signature, signedMessageBytes) // if it needs to check multiple signatures, the app can do so here
const receivedOffchainMessage = getOffchainMessageDecoder().decode(signedMessageBytes)
if (receivedOffchainMessage.version === 1) {
assertV1OffchainMessagesEqual(receivedOffchainMessage, myInputOffchainMessage)
} else {
// throw, not the version the app expected
}Now if the spec moves on and the dapp starts sending version 2 messages to the wallet and we add a v2 verify function, it just needs to change:
if (receivedOffchainMessage.version === 2) {
assertV2OffchainMessagesEqual(receivedOffchainMessage, myInputOffchainMessage)
} else {
// throw, not the version the app expected
}On another note, thinking about this API in the context of multiple requiredSigners makes the feature as proposed feel quite limited. An app would need to piece together the envelope with multiple signatures itself. We'll be able to hide that in the signer API (as we currently do with signMessage) though.
Adds a
verifyOffchainMessagehelper to@solana/offchain-messagesthat verifies a signed version 1 offchain message returned by an untrusted signer (eg. a wallet) in a single call.When you ask a wallet to sign an offchain message, it hands back the bytes it signed plus a signature. Verifying the signature alone is not enough — a compromised signer could return a perfectly valid signature over data that has nothing to do with what you asked it to sign. From the
messageandrequiredSignersyou requested,verifyOffchainMessagereconstructs the version 1 message you expected the signer to sign, asserts it matches the bytes the signer reports having signed, then asserts the signature is a valid Ed25519 signature of those bytes by the signer.This is distinct from the existing
verifyOffchainMessageEnvelope, which trusts the envelope'scontentas-is and has no notion of an expected message to compare against.solana:signOffchainMessagewallet feature.requiredSignersis optional and defaults to[signer].SOLANA_ERROR__OFFCHAIN_MESSAGE__CONTENT_DOES_NOT_MATCH_EXPECTED(5607018), thrown when the signed bytes don't match the expected message.Test plan
verifyOffchainMessagetest suite covering: happy path,requiredSignersdefault, content mismatch, short-circuit before signature verification, invalid signature, signer-not-required guard, out-of-order signers, version tampering, truncated bytes, and the structural-validation cases.pnpm test:lint,test:typecheck,test:unit:node,test:unit:browserall pass.