Skip to content

Commit becb237

Browse files
authored
Add SEP-53 message signing/verification to Keypair (#1513)
* Add SEP-53 message signing/verification to Keypair * Updated changelog * Add section to guide * Update for consistency * Updated docs
1 parent 6a7898e commit becb237

5 files changed

Lines changed: 230 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A breaking change will get clearly marked in this log.
1212
- `rpc.Server.getContractInstance(contractId)`: returns a contract's `xdr.ScContractInstance` (its executable and instance storage) ([#1501](https://github.com/stellar/js-stellar-sdk/pull/1501)).
1313
- `contract.Client.from`, `fromWasm`, and `fromWasmHash` are now generic (`<T>`) and return `Client & T`, giving typed, autocompleted contract methods without code generation. The type parameter defaults to `unknown`, so existing untyped calls are unchanged ([#1502](https://github.com/stellar/js-stellar-sdk/pull/1502)).
1414
- `ClientOptions.server`: pass an existing `rpc.Server` to `contract.Client.from` to reuse its transport (headers, interceptors, `allowHttp`) instead of constructing a new one ([#1502](https://github.com/stellar/js-stellar-sdk/pull/1502)).
15+
- `Keypair.signMessage(message)` and `Keypair.verifyMessage(message, signature)`: sign and verify arbitrary messages per [SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md), for proving Stellar address ownership off-chain. The message (a UTF-8 string or `Buffer`) is prefixed with `"Stellar Signed Message:\n"`, SHA-256 hashed, and signed/verified with the keypair's ed25519 key — parity with the Python/Java SDKs and stellar-cli ([#1513](https://github.com/stellar/js-stellar-sdk/pull/1513)).
1516

1617
### Changed
1718
- `contract.Client.from` now supports built-in Stellar Asset Contracts (SACs): when the contract's executable is a SAC, the client is built from the embedded SAC spec (lazily imported so bundlers can code-split it out of the common path) instead of downloading Wasm, which a SAC has none of on-chain ([#1501](https://github.com/stellar/js-stellar-sdk/pull/1501)).

docs/guides/01-connect-and-fund.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,29 @@ Store the secret, not the keypair object. Rebuild the keypair when you need it:
3535
const keypair = Keypair.fromSecret(secret);
3636
```
3737

38+
## Sign and verify a message
39+
40+
A keypair can also sign an arbitrary message — not a transaction — to prove it
41+
controls an address, entirely off-chain. This follows
42+
[SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md),
43+
so signatures interoperate with the other Stellar SDKs and the CLI, and it is the
44+
basis for proof-of-ownership flows like "Sign in with Stellar".
45+
46+
```ts
47+
// Sign a string (or raw bytes); returns a 64-byte signature.
48+
const signature = keypair.signMessage("Hello, World!");
49+
50+
// Anyone holding the public key can verify it — no secret key needed.
51+
const verifier = Keypair.fromPublicKey(keypair.publicKey());
52+
verifier.verifyMessage("Hello, World!", signature); // true
53+
```
54+
55+
Only the secret-key holder can produce a valid signature; verification needs just
56+
the public key. See
57+
[`signMessage`](/reference/core-keys/#keypairsignmessagemessage) and
58+
[`verifyMessage`](/reference/core-keys/#keypairverifymessagemessage-signature) in
59+
the reference.
60+
3861
## Choose a network
3962

4063
Every transaction is signed for exactly one

docs/reference/core-keys.md

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,17 @@ class Keypair {
3434
sign(data: Buffer): Buffer;
3535
signatureHint(): Buffer;
3636
signDecorated(data: Buffer): DecoratedSignature;
37+
signMessage(message: string | Buffer<ArrayBufferLike>): Buffer;
3738
signPayloadDecorated(data: Buffer): DecoratedSignature;
3839
verify(data: Buffer, signature: Buffer): boolean;
40+
verifyMessage(message: string | Buffer<ArrayBufferLike>, signature: Buffer): boolean;
3941
xdrAccountId(): PublicKey;
4042
xdrMuxedAccount(id?: string): MuxedAccount;
4143
xdrPublicKey(): PublicKey;
4244
}
4345
```
4446

45-
**Source:** [src/base/keypair.ts:21](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L21)
47+
**Source:** [src/base/keypair.ts:25](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L25)
4648

4749
### `new Keypair(keys)`
4850

@@ -57,7 +59,7 @@ constructor(keys: { publicKey?: string | Buffer<ArrayBufferLike>; secretKey: str
5759
- `publicKey`: raw public key
5860
- `secretKey`: raw secret key (32-byte secret seed in ed25519)
5961

60-
**Source:** [src/base/keypair.ts:33](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L33)
62+
**Source:** [src/base/keypair.ts:37](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L37)
6163

6264
### `Keypair.fromPublicKey(publicKey)`
6365

@@ -71,7 +73,7 @@ static fromPublicKey(publicKey: string): Keypair;
7173

7274
- **`publicKey`**`string` (required) — public key (ex. `GB3KJPLFUYN5VL6R3GU3EGCGVCKFDSD7BEDX42HWG5BWFKB3KQGJJRMA`)
7375

74-
**Source:** [src/base/keypair.ts:115](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L115)
76+
**Source:** [src/base/keypair.ts:119](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L119)
7577

7678
### `Keypair.fromRawEd25519Seed(rawSeed)`
7779

@@ -85,7 +87,7 @@ static fromRawEd25519Seed(rawSeed: Buffer): Keypair;
8587

8688
- **`rawSeed`**`Buffer` (required) — raw 32-byte ed25519 secret key seed
8789

88-
**Source:** [src/base/keypair.ts:93](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L93)
90+
**Source:** [src/base/keypair.ts:97](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L97)
8991

9092
### `Keypair.fromSecret(secret)`
9193

@@ -100,7 +102,7 @@ static fromSecret(secret: string): Keypair;
100102

101103
- **`secret`**`string` (required) — secret key (ex. `SDAK....`)
102104

103-
**Source:** [src/base/keypair.ts:83](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L83)
105+
**Source:** [src/base/keypair.ts:87](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L87)
104106

105107
### `Keypair.master(networkPassphrase)`
106108

@@ -114,7 +116,7 @@ static master(networkPassphrase: string): Keypair;
114116

115117
- **`networkPassphrase`**`string` (required) — passphrase of the target stellar network (e.g. "Public Global Stellar Network ; September 2015")
116118

117-
**Source:** [src/base/keypair.ts:101](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L101)
119+
**Source:** [src/base/keypair.ts:105](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L105)
118120

119121
### `Keypair.random()`
120122

@@ -124,15 +126,15 @@ Create a random `Keypair` object.
124126
static random(): Keypair;
125127
```
126128

127-
**Source:** [src/base/keypair.ts:127](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L127)
129+
**Source:** [src/base/keypair.ts:131](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L131)
128130

129131
### `keypair.type`
130132

131133
```ts
132134
readonly type: "ed25519";
133135
```
134136

135-
**Source:** [src/base/keypair.ts:22](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L22)
137+
**Source:** [src/base/keypair.ts:26](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L26)
136138

137139
### `keypair.canSign()`
138140

@@ -142,7 +144,7 @@ Returns `true` if this `Keypair` object contains secret key and can sign.
142144
canSign(): boolean;
143145
```
144146

145-
**Source:** [src/base/keypair.ts:226](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L226)
147+
**Source:** [src/base/keypair.ts:230](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L230)
146148

147149
### `keypair.publicKey()`
148150

@@ -152,7 +154,7 @@ Returns public key associated with this `Keypair` object.
152154
publicKey(): string;
153155
```
154156

155-
**Source:** [src/base/keypair.ts:188](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L188)
157+
**Source:** [src/base/keypair.ts:192](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L192)
156158

157159
### `keypair.rawPublicKey()`
158160

@@ -162,7 +164,7 @@ Returns raw public key bytes
162164
rawPublicKey(): Buffer;
163165
```
164166

165-
**Source:** [src/base/keypair.ts:171](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L171)
167+
**Source:** [src/base/keypair.ts:175](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L175)
166168

167169
### `keypair.rawSecretKey()`
168170

@@ -176,7 +178,7 @@ rawSecretKey(): Buffer;
176178

177179
- if no secret seed is available
178180

179-
**Source:** [src/base/keypair.ts:216](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L216)
181+
**Source:** [src/base/keypair.ts:220](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L220)
180182

181183
### `keypair.secret()`
182184

@@ -192,7 +194,7 @@ secret(): string;
192194

193195
- if no secret key is available
194196

195-
**Source:** [src/base/keypair.ts:199](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L199)
197+
**Source:** [src/base/keypair.ts:203](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L203)
196198

197199
### `keypair.sign(data)`
198200

@@ -210,7 +212,7 @@ sign(data: Buffer): Buffer;
210212

211213
- if no secret key is available
212214

213-
**Source:** [src/base/keypair.ts:236](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L236)
215+
**Source:** [src/base/keypair.ts:240](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L240)
214216

215217
### `keypair.signatureHint()`
216218

@@ -221,7 +223,7 @@ The hint is the last 4 bytes of the account ID XDR representation.
221223
signatureHint(): Buffer;
222224
```
223225

224-
**Source:** [src/base/keypair.ts:179](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L179)
226+
**Source:** [src/base/keypair.ts:183](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L183)
225227

226228
### `keypair.signDecorated(data)`
227229

@@ -241,7 +243,37 @@ signDecorated(data: Buffer): DecoratedSignature;
241243

242244
- TransactionBase.addDecoratedSignature
243245

244-
**Source:** [src/base/keypair.ts:267](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L267)
246+
**Source:** [src/base/keypair.ts:314](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L314)
247+
248+
### `keypair.signMessage(message)`
249+
250+
Signs an arbitrary message per SEP-53.
251+
252+
The message is UTF-8 encoded (if a string), prefixed with the fixed
253+
`"Stellar Signed Message:\n"` marker, hashed with SHA-256, and that hash is
254+
signed with this keypair's ed25519 secret key.
255+
256+
```ts
257+
signMessage(message: string | Buffer<ArrayBufferLike>): Buffer;
258+
```
259+
260+
**Parameters**
261+
262+
- **`message`**`string | Buffer<ArrayBufferLike>` (required) — the message to sign (a UTF-8 string or raw bytes)
263+
264+
**Returns**
265+
266+
the 64-byte ed25519 signature
267+
268+
**Throws**
269+
270+
- if no secret key is available
271+
272+
**See also**
273+
274+
- https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md
275+
276+
**Source:** [src/base/keypair.ts:274](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L274)
245277

246278
### `keypair.signPayloadDecorated(data)`
247279

@@ -263,7 +295,7 @@ signPayloadDecorated(data: Buffer): DecoratedSignature;
263295
- - https://github.com/stellar/stellar-protocol/blob/master/core/cap-0040.md#signature-hint
264296
- TransactionBase.addDecoratedSignature
265297

266-
**Source:** [src/base/keypair.ts:285](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L285)
298+
**Source:** [src/base/keypair.ts:332](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L332)
267299

268300
### `keypair.verify(data, signature)`
269301

@@ -278,7 +310,30 @@ verify(data: Buffer, signature: Buffer): boolean;
278310
- **`data`**`Buffer` (required) — signed data
279311
- **`signature`**`Buffer` (required) — signature to verify
280312

281-
**Source:** [src/base/keypair.ts:250](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L250)
313+
**Source:** [src/base/keypair.ts:254](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L254)
314+
315+
### `keypair.verifyMessage(message, signature)`
316+
317+
Verifies a SEP-53 signed message against this keypair's public key.
318+
319+
```ts
320+
verifyMessage(message: string | Buffer<ArrayBufferLike>, signature: Buffer): boolean;
321+
```
322+
323+
**Parameters**
324+
325+
- **`message`**`string | Buffer<ArrayBufferLike>` (required) — the original message (a UTF-8 string or raw bytes)
326+
- **`signature`**`Buffer` (required) — the 64-byte signature to verify
327+
328+
**Returns**
329+
330+
`true` if `signature` is valid for `message` and this key
331+
332+
**See also**
333+
334+
- https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md
335+
336+
**Source:** [src/base/keypair.ts:286](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L286)
282337

283338
### `keypair.xdrAccountId()`
284339

@@ -288,7 +343,7 @@ Returns this public key as an xdr.AccountId.
288343
xdrAccountId(): PublicKey;
289344
```
290345

291-
**Source:** [src/base/keypair.ts:133](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L133)
346+
**Source:** [src/base/keypair.ts:137](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L137)
292347

293348
### `keypair.xdrMuxedAccount(id)`
294349

@@ -306,7 +361,7 @@ xdrMuxedAccount(id?: string): MuxedAccount;
306361
- **`id`**`string` (optional) — (optional) stringified integer indicating the underlying muxed
307362
ID of the new account object
308363

309-
**Source:** [src/base/keypair.ts:151](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L151)
364+
**Source:** [src/base/keypair.ts:155](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L155)
310365

311366
### `keypair.xdrPublicKey()`
312367

@@ -316,7 +371,7 @@ Returns this public key as an xdr.PublicKey.
316371
xdrPublicKey(): PublicKey;
317372
```
318373

319-
**Source:** [src/base/keypair.ts:138](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L138)
374+
**Source:** [src/base/keypair.ts:142](https://github.com/stellar/js-stellar-sdk/blob/main/src/base/keypair.ts#L142)
320375

321376
## SignerKey
322377

src/base/keypair.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { hash } from "./hashing.js";
77
import xdr from "./xdr.js";
88

99
ed.hashes.sha512 = sha512;
10+
11+
// SEP-53: fixed prefix prepended to a message before hashing and signing.
12+
const MESSAGE_PREFIX = Buffer.from("Stellar Signed Message:\n", "utf8");
13+
1014
/**
1115
* `Keypair` represents public (and secret) keys of the account.
1216
*
@@ -255,6 +259,49 @@ export class Keypair {
255259
}
256260
}
257261

262+
/**
263+
* Signs an arbitrary message per SEP-53.
264+
*
265+
* The message is UTF-8 encoded (if a string), prefixed with the fixed
266+
* `"Stellar Signed Message:\n"` marker, hashed with SHA-256, and that hash is
267+
* signed with this keypair's ed25519 secret key.
268+
*
269+
* @param message - the message to sign (a UTF-8 string or raw bytes)
270+
* @returns the 64-byte ed25519 signature
271+
* @throws if no secret key is available
272+
* @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md
273+
*/
274+
signMessage(message: string | Buffer): Buffer {
275+
return this.sign(this._hashMessage(message));
276+
}
277+
278+
/**
279+
* Verifies a SEP-53 signed message against this keypair's public key.
280+
*
281+
* @param message - the original message (a UTF-8 string or raw bytes)
282+
* @param signature - the 64-byte signature to verify
283+
* @returns `true` if `signature` is valid for `message` and this key
284+
* @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md
285+
*/
286+
verifyMessage(message: string | Buffer, signature: Buffer): boolean {
287+
try {
288+
return this.verify(this._hashMessage(message), signature);
289+
} catch {
290+
// Mirror `verify`: never throw on bad input, just report invalid.
291+
return false;
292+
}
293+
}
294+
295+
/**
296+
* Computes the SEP-53 message hash:
297+
* `SHA-256("Stellar Signed Message:\n" + message)`.
298+
*/
299+
private _hashMessage(message: string | Buffer): Buffer {
300+
const messageBytes =
301+
typeof message === "string" ? Buffer.from(message, "utf8") : message;
302+
return hash(Buffer.concat([MESSAGE_PREFIX, messageBytes]));
303+
}
304+
258305
/**
259306
* Returns the decorated signature (hint+sig) for arbitrary data.
260307
*

0 commit comments

Comments
 (0)