WIP: Fix lingering S < L issue
This commit is contained in:
@@ -32,13 +32,18 @@ import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
// Verified against the canonical Erlang ec_utils (see test/README.md for how).
|
||||
//
|
||||
// SAFETY NOTE:
|
||||
// It turns out that many JVM environments completely lack a way to allocate pinned memory
|
||||
// that lives outside the domain of the garbage collector. While the techniques used in this
|
||||
// code are pretty darn safe, the underlying byte arrays can still get copied on GC before
|
||||
// being zeroed if they are allocated in memory that isn't pinned. It turns out Android is
|
||||
// one such platform, and the solution there is to vendor out a C implementation of Ed25519
|
||||
// and interact with it over a JNI boundary with off-heap memory via java.nio.ByteBuffer.
|
||||
// In cases where this code can run in a fully isolated memory environment it is sufficient.
|
||||
// Despite the effort at zeroing sensitive memory in this module, Java heap memory does not
|
||||
// provide reliable secret-memory erasure or even really permit it. If protection against
|
||||
// copies of secret material in managed memory is a requirement of the deployment environment,
|
||||
// don't use this implementation; use explicitly managed native memory instead.
|
||||
//
|
||||
// This implementation is intended for isolated environments where the Java heap is an
|
||||
// acceptable trust boundary. It is not intended to provide secure-memory guarantees
|
||||
// against a hostile or inspecting runtime.
|
||||
//
|
||||
// Android is one such platform where a vendor-provided C implementation of Ed25519
|
||||
// interacting over a JNI boundary with off-heap memory (via java.nio.ByteBuffer)
|
||||
// and aggressive memory sanitation is necessary.
|
||||
//
|
||||
// References:
|
||||
// I don't even know where to start with references, but the tink-java library and pretty much
|
||||
@@ -72,9 +77,26 @@ public final class Ed25519 {
|
||||
private static final long[] D2 = {45281625L, 27714825L, 36363642L, 13898781L, 229458L, 15978800L, 54557047L, 27058993L, 29715967L, 9444199L};
|
||||
private static final long[] I = {34513072L, 25610706L, 9377949L, 3500415L, 12389472L, 33281959L, 41962654L, 31548777L, 326685L, 11406482L};
|
||||
|
||||
// Group order L (little-endian)
|
||||
private static final byte[] L = {
|
||||
(byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
|
||||
(byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10
|
||||
};
|
||||
|
||||
// Field prime P = 2^255 - 19 (little-endian, 255 bits used)
|
||||
private static final byte[] P = {
|
||||
(byte) 0xed, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f
|
||||
};
|
||||
|
||||
public static final class Ge {
|
||||
public final long[] X = new long[10], Y = new long[10], Z = new long[10], T = new long[10];
|
||||
|
||||
// Internal: Wipes the coordinates.
|
||||
public void wipe() {
|
||||
CryptoUtils.wipe(X);
|
||||
CryptoUtils.wipe(Y);
|
||||
@@ -91,6 +113,7 @@ public final class Ed25519 {
|
||||
public final long[][] stack = new long[10][10];
|
||||
public final Ge geTmp = new Ge();
|
||||
|
||||
// Internal: Wipes the scratch space.
|
||||
public void wipe() {
|
||||
CryptoUtils.wipe(a);
|
||||
CryptoUtils.wipe(b);
|
||||
@@ -165,7 +188,7 @@ public final class Ed25519 {
|
||||
if (signature.length != 64) return false;
|
||||
byte[] R_bytes = Arrays.copyOfRange(signature, 0, 32);
|
||||
byte[] S_bytes = Arrays.copyOfRange(signature, 32, 64);
|
||||
if ((S_bytes[31] & 0xe0) != 0) return false;
|
||||
if (!isLessThan(S_bytes, L)) return false;
|
||||
Scratch sc = new Scratch();
|
||||
Ge A = decompress(publicKey, sc);
|
||||
if (A == null) return false;
|
||||
@@ -197,6 +220,11 @@ public final class Ed25519 {
|
||||
return ok;
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// Internal curve and field arithmetic machinery.
|
||||
// These are public only for parity testing against reference implementations.
|
||||
|
||||
// Internal: Scalar multiplication by base point.
|
||||
public static Ge scalarMulBase(byte[] scalar, Scratch s) {
|
||||
Ge res = new Ge();
|
||||
fe_0(res.X);
|
||||
@@ -218,16 +246,7 @@ public final class Ed25519 {
|
||||
return res;
|
||||
}
|
||||
|
||||
public static void ge_double_scalarmul_vartime(Ge r, byte[] s, Ge a, byte[] k, Scratch sc) {
|
||||
Ge sB = scalarMulBase(s, sc);
|
||||
Ge kA = scalarMul(a, k, sc);
|
||||
fe_neg(kA.X, kA.X);
|
||||
fe_neg(kA.T, kA.T);
|
||||
ge_add(r, sB, kA, sc);
|
||||
sB.wipe();
|
||||
kA.wipe();
|
||||
}
|
||||
|
||||
// Internal: General scalar multiplication.
|
||||
public static Ge scalarMul(Ge p, byte[] scalar, Scratch s) {
|
||||
Ge res = new Ge();
|
||||
fe_0(res.X);
|
||||
@@ -249,6 +268,7 @@ public final class Ed25519 {
|
||||
return res;
|
||||
}
|
||||
|
||||
// Internal: Point addition.
|
||||
public static void ge_add(Ge r, Ge p1, Ge p2, Scratch s) {
|
||||
long[] yMinusX1 = s.stack[0], yPlusX1 = s.stack[1], yMinusX2 = s.stack[2], yPlusX2 = s.stack[3];
|
||||
long[] A = s.stack[4], B = s.stack[5], C = s.stack[6], D = s.stack[7];
|
||||
@@ -273,6 +293,7 @@ public final class Ed25519 {
|
||||
fe_mul(r.T, E, H, s.t19);
|
||||
}
|
||||
|
||||
// Internal: Point doubling.
|
||||
public static void ge_double(Ge r, Ge p, Scratch s) {
|
||||
long[] A = s.stack[0], B = s.stack[1], C = s.stack[2], D = s.stack[3];
|
||||
long[] E = s.stack[4], F = s.stack[5], G = s.stack[6], H = s.stack[7];
|
||||
@@ -309,8 +330,15 @@ public final class Ed25519 {
|
||||
}
|
||||
}
|
||||
|
||||
// Internal: Point decompression.
|
||||
public static Ge decompress(byte[] b, Scratch s) {
|
||||
if (b.length != 32) return null;
|
||||
|
||||
// Ensure y is canonical (y < P)
|
||||
byte[] y_check = Arrays.copyOf(b, 32);
|
||||
y_check[31] &= 0x7F;
|
||||
if (!isLessThan(y_check, P)) return null;
|
||||
|
||||
Ge p = new Ge();
|
||||
fe_frombytes(p.Y, b);
|
||||
fe_1(p.Z);
|
||||
@@ -347,6 +375,7 @@ public final class Ed25519 {
|
||||
return p;
|
||||
}
|
||||
|
||||
// Internal: Point compression.
|
||||
public static byte[] compress(Ge p, Scratch s) {
|
||||
fe_invert(s.a, p.Z, s);
|
||||
fe_mul(s.b, p.X, s.a, s.t19);
|
||||
@@ -381,6 +410,17 @@ public final class Ed25519 {
|
||||
for (int i = 0; i < 10; i++) h[i] = -f[i];
|
||||
}
|
||||
|
||||
private static boolean isLessThan(byte[] a, byte[] b) {
|
||||
for (int i = 31; i >= 0; i--) {
|
||||
int ai = a[i] & 0xff;
|
||||
int bi = b[i] & 0xff;
|
||||
if (ai < bi) return true;
|
||||
if (ai > bi) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Internal: Field multiplication.
|
||||
public static void fe_mul(long[] out, long[] f, long[] g, long[] t) {
|
||||
t[0] = f[0] * g[0];
|
||||
t[1] = f[0] * g[1] + f[1] * g[0];
|
||||
@@ -404,6 +444,7 @@ public final class Ed25519 {
|
||||
fe_reduce(out, t);
|
||||
}
|
||||
|
||||
/// Internal: Field squaring.
|
||||
public static void fe_sq(long[] out, long[] f, long[] t) {
|
||||
t[0] = f[0] * f[0];
|
||||
t[1] = 2 * f[0] * f[1];
|
||||
@@ -427,6 +468,7 @@ public final class Ed25519 {
|
||||
fe_reduce(out, t);
|
||||
}
|
||||
|
||||
// Internal: Field reduction.
|
||||
public static void fe_reduce(long[] h, long[] t) {
|
||||
t[0] += t[10] * 19;
|
||||
t[1] += t[11] * 19;
|
||||
@@ -450,6 +492,7 @@ public final class Ed25519 {
|
||||
for (int i = 0; i < 10; i++) h[i] = t[i];
|
||||
}
|
||||
|
||||
// Internal: Load field element from bytes.
|
||||
public static void fe_frombytes(long[] h, byte[] s) {
|
||||
for (int i = 0; i < 10; i++) h[i] = 0;
|
||||
int bitIdx = 0;
|
||||
@@ -464,6 +507,7 @@ public final class Ed25519 {
|
||||
}
|
||||
}
|
||||
|
||||
// Internal: Contract field element to bytes.
|
||||
public static byte[] fe_contract(long[] h) {
|
||||
long[] val = Arrays.copyOf(h, 10);
|
||||
for (int p = 0; p < 2; p++) {
|
||||
@@ -597,6 +641,7 @@ public final class Ed25519 {
|
||||
return s[0] & 1;
|
||||
}
|
||||
|
||||
// Internal: Scalar reduction.
|
||||
public static void reduceScalar(byte[] s) {
|
||||
long s0 = 2097151 & load3(s, 0);
|
||||
long s1 = 2097151 & (load4(s, 2) >> 5);
|
||||
|
||||
@@ -44,6 +44,7 @@ public class Testinator {
|
||||
case "blake2b" -> { System.out.print(blake2b(args[1])); }
|
||||
case "ed25519" -> { System.out.print(ed25519(args[1])); }
|
||||
case "ed25519_verify" -> { System.out.print(ed25519_verify(args[1])); }
|
||||
case "ed25519_malleability" -> { System.out.print(ed25519_malleability()); }
|
||||
case "api_encode" -> { System.out.print(api_encode(args[1])); }
|
||||
case "id_serialization" -> { System.out.print(id_serialization(args[1])); }
|
||||
case "spend_tx" -> { System.out.print(spend_tx(args[1])); }
|
||||
@@ -391,6 +392,20 @@ public class Testinator {
|
||||
return CryptoUtils.binToHex(pub) + "|" + CryptoUtils.binToHex(sig) + "|" + verify;
|
||||
}
|
||||
|
||||
private static String ed25519_malleability() {
|
||||
// L in little-endian
|
||||
byte[] L_bytes = {
|
||||
(byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
|
||||
(byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10
|
||||
};
|
||||
byte[] sig = new byte[64];
|
||||
System.arraycopy(L_bytes, 0, sig, 32, 32); // S = L
|
||||
boolean result = Ed25519.verify(new byte[32], new byte[0], sig);
|
||||
return Boolean.toString(result);
|
||||
}
|
||||
|
||||
private static String api_encode(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "api_encode.test");
|
||||
Path resPath = Path.of(workingPath, "api_encode.java.txt");
|
||||
|
||||
Reference in New Issue
Block a user