비트코인과 이더리움은 디지털 서명을 위해서 Secp256k1
타원곡선을 사용하지만, 솔라나(Solana)네트워크는 Ed25519를 사용하는것을 알게되었습니다.
Ed25519는 Curve25519
타원곡선을 기반으로 하고 있습니다.
그래서 기존에 의존성으로 사용중이던 바운시캐슬을 사용하여 키페어 생성과 서명을 수행하는 간단한 헬퍼 메소드를 만들어보았습니다.
소스는 자바 1.8 버전으로 작성되었습니다.
public static List<byte[]> generateKeypair() {
final Ed25519KeyGenerationParameters parameters = new Ed25519KeyGenerationParameters(new SecureRandom());
final Ed25519KeyPairGenerator generator = new Ed25519KeyPairGenerator();
generator.init(parameters);
final AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
final Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters) keyPair.getPrivate();
final Ed25519PublicKeyParameters publicKey = (Ed25519PublicKeyParameters) keyPair.getPublic();
return Arrays.asList(publicKey.getEncoded(), privateKey.getEncoded());
}
publicKey
는 말그대로 외부에 공개할 수 있는 공개키를 의미하고, privateKey
는 공개해선 안되는 비밀키를 의미합니다.
public static byte[] sign(byte[] privateKey, byte[] message) {
final Ed25519PrivateKeyParameters priKey = new Ed25519PrivateKeyParameters(privateKey);
Ed25519Signer signer = new Ed25519Signer();
signer.init(true, priKey);
signer.update(message, 0, message.length);
return signer.generateSignature();
}
privateKey
는 generateKeypair
메소드에서 생성한 비밀키를 의미하고, message
는 서명의 대상이되는 값으로써, 거래(transaction)의 해시값을 의미할 수 도 있습니다.
이 메소드가 반환하는 값은 서명 그 자체의 값입니다.
public static boolean verify(byte[] signature, byte[] publicKey, byte[] message) {
final Ed25519PublicKeyParameters keyParameters = new Ed25519PublicKeyParameters(publicKey);
final Ed25519Signer signer = new Ed25519Signer();
signer.init(false, keyParameters);
signer.update(message, 0, message.length);
return signer.verifySignature(signature);
}
signature
는 sign
메소드에서 생성한 서명값을 의미하고, publicKey
는 generateKeypair
메소드에서 생성한 공개키를 의미합니다.
전달받은 message
, signature
그리고 publicKey
로 서명이 유효한지 검증합니다.
@Test
void verifySuccessTest() {
// given
List<byte[]> keypair = Ed25519Helper.generateKeypair();
byte[] pubKey = keypair.get(0);
byte[] privKey = keypair.get(1);
String transaction = "send 10 coins to Bob";
byte[] message = transaction.getBytes();
// when
byte[] sig = sign(privKey, message);
// then
boolean verify = verify(sig, pubKey, message);
assertTrue(verify);
}
키페어 생성 후, 메시지를 서명하였습니다. 그리고 그 메시지와, 서명 그리고 공개키를 전달 받은 사람이 해당 서명이 유효한지 검증하여 유효한 서명과 메시지로 테스트를
통과했습니다.
@Test
void wrongSignerTest() {
// given
List<byte[]> keypair = Ed25519Helper.generateKeypair();
byte[] pubKey = keypair.get(0);
String transaction = "send 10 coins to Bob";
byte[] message = transaction.getBytes();
// when
byte[] anotherPrivKey = Ed25519Helper.generateKeypair().get(1);
byte[] sig = sign(anotherPrivKey, message);
// then
boolean verify = verify(sig, pubKey, message);
assertFalse(verify);
}
위의 경우에는, 전달받은 서명값과 메시지는 기존에 알고있던 공개키를 생성한 사람이 서명한 것이 아닙니다. 따라서 폐기합니다.
@Test
void modifiedMessageTest() {
// given
List<byte[]> keypair = Ed25519Helper.generateKeypair();
byte[] pubKey = keypair.get(0);
byte[] privKey = keypair.get(1);
String transaction = "send 10 coins to Bob";
byte[] message = transaction.getBytes();
byte[] sig = sign(privKey, message);
// when
byte[] corruptedMessage = "send 10 coins to bob".getBytes();
// then
boolean verify = verify(sig, pubKey, corruptedMessage);
assertFalse(verify);
}
위의 경우에는, 전달받은 메시지가 서명자가 서명할 당시의 메시지 값과 다릅니다(Bob -> bob). 따라서 폐기합니다.
상기 예제에서는 키페어 생성을 위해 java.security.SecureRandom
를 사용하였습니다. 이는 java.util.Random
보다 보안성이 높은 난수 생성기이지만,
실무에서 사용하실때는 이것이 보안 요구사항을 만족하는 엔트로피를 생성할 수 있는지 충분한 검토가 필요합니다.