Skip to content

Commit eef61c2

Browse files
authored
feat: implement message signing and verification according to SEP-53. (#698)
1 parent 2989e9b commit eef61c2

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Update:
66
- feat: add `pollTransaction` method to `SorobanServer` to poll transaction status with retry strategy. ([#696](https://github.com/stellar/java-stellar-sdk/pull/696))
7+
- feat: implement message signing and verification according to [SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md), check `KeyPair.signMessage` and `KeyPair.verifyMessage` for more details. ([#698](https://github.com/stellar/java-stellar-sdk/pull/698))
78

89
## 1.5.0
910

src/main/java/org/stellar/sdk/KeyPair.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.stellar.sdk;
22

33
import static java.lang.System.arraycopy;
4+
import static java.nio.charset.StandardCharsets.UTF_8;
45

56
import java.io.ByteArrayOutputStream;
67
import java.io.IOException;
@@ -354,4 +355,72 @@ public int hashCode() {
354355
Arrays.hashCode(publicKey.getEncoded()),
355356
privateKey == null ? null : Arrays.hashCode(privateKey.getEncoded()));
356357
}
358+
359+
/**
360+
* Calculate the hash of a message according to <a
361+
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
362+
* target="_blank">SEP-53</a>.
363+
*
364+
* @param message The message to hash
365+
* @return The SHA-256 hash of the prefixed message.
366+
*/
367+
private static byte[] calculateMessageHash(byte[] message) {
368+
final byte[] messagePrefix = "Stellar Signed Message:\n".getBytes(UTF_8);
369+
byte[] signedMessageBase = new byte[messagePrefix.length + message.length];
370+
System.arraycopy(messagePrefix, 0, signedMessageBase, 0, messagePrefix.length);
371+
System.arraycopy(message, 0, signedMessageBase, messagePrefix.length, message.length);
372+
return Util.hash(signedMessageBase);
373+
}
374+
375+
/**
376+
* Sign a message according to <a
377+
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
378+
* target="_blank">SEP-53</a>.
379+
*
380+
* @param message The message to sign.
381+
* @return The signature bytes.
382+
*/
383+
public byte[] signMessage(String message) {
384+
return signMessage(message.getBytes(UTF_8));
385+
}
386+
387+
/**
388+
* Sign a message according to <a
389+
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
390+
* target="_blank">SEP-53</a>.
391+
*
392+
* @param message The message to sign.
393+
* @return The signature bytes.
394+
*/
395+
public byte[] signMessage(byte[] message) {
396+
byte[] messageHash = calculateMessageHash(message);
397+
return sign(messageHash);
398+
}
399+
400+
/**
401+
* Verify a <a
402+
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
403+
* target="_blank">SEP-53</a> signed message.
404+
*
405+
* @param message The original message.
406+
* @param signature The signature to verify.
407+
* @return True if the signature is valid for the given message, false otherwise.
408+
*/
409+
public boolean verifyMessage(byte[] message, byte[] signature) {
410+
byte[] messageHash = calculateMessageHash(message);
411+
return verify(messageHash, signature);
412+
}
413+
414+
/**
415+
* Verify a <a
416+
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
417+
* target="_blank">SEP-53</a> signed message.
418+
*
419+
* @param message The original message.
420+
* @param signature The signature to verify.
421+
* @return True if the signature is valid for the given message, false otherwise.
422+
*/
423+
public boolean verifyMessage(String message, byte[] signature) {
424+
return verifyMessage(message.getBytes(UTF_8), signature);
425+
}
357426
}

src/test/java/org/stellar/sdk/KeyPairTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.junit.Assert.fail;
99

1010
import java.util.Arrays;
11+
import java.util.Base64;
1112
import java.util.HashMap;
1213
import java.util.List;
1314
import java.util.Map;
@@ -212,4 +213,48 @@ public void testFromSecretSeedThrowsWithBytes() {
212213
assertThrows(
213214
IllegalArgumentException.class, () -> KeyPair.fromSecretSeed(Util.hexToBytes("00")));
214215
}
216+
217+
@Test
218+
public void testSignAndVerifyMessage() {
219+
KeyPair kp = KeyPair.fromSecretSeed("SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW");
220+
221+
String inputEnglish = "Hello, World!";
222+
byte[] expectedEnglishSig =
223+
Base64.getDecoder()
224+
.decode(
225+
"fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==");
226+
String inputJapanese = "こんにちは、世界!";
227+
byte[] expectedJapaneseSig =
228+
Base64.getDecoder()
229+
.decode(
230+
"CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA==");
231+
byte[] inputBytes = Base64.getDecoder().decode("2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=");
232+
byte[] expectedBytesSig =
233+
Base64.getDecoder()
234+
.decode(
235+
"VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ==");
236+
237+
assertArrayEquals(expectedEnglishSig, kp.signMessage(inputEnglish));
238+
assertArrayEquals(expectedJapaneseSig, kp.signMessage(inputJapanese));
239+
assertArrayEquals(expectedBytesSig, kp.signMessage(inputBytes));
240+
241+
assertTrue(kp.verifyMessage(inputEnglish, expectedEnglishSig));
242+
assertTrue(kp.verifyMessage(inputJapanese, expectedJapaneseSig));
243+
assertTrue(kp.verifyMessage(inputBytes, expectedBytesSig));
244+
}
245+
246+
@Test
247+
public void testVerifyMessageWithInvalidSignature() {
248+
KeyPair kp = KeyPair.fromAccountId("GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L");
249+
String message = "Hello, World!";
250+
String[] invalidSigs =
251+
new String[] {
252+
"VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ==",
253+
"MTI="
254+
};
255+
for (String sigB64 : invalidSigs) {
256+
byte[] sig = Base64.getDecoder().decode(sigB64);
257+
assertFalse(kp.verifyMessage(message, sig));
258+
}
259+
}
215260
}

0 commit comments

Comments
 (0)