499 lines
26 KiB
Java
499 lines
26 KiB
Java
/*
|
|
* AereCandidacy.java - the signing helper of the Aere Network validator kit (chain 2800).
|
|
*
|
|
* It runs on the CANDIDATE's machine, against the lib/ directory of the candidate's OWN build of the
|
|
* Aere fork (run-node.sh --no-start builds it from the published patch series), with the JDK's
|
|
* single-file source launcher:
|
|
*
|
|
* java -cp '<build>/build/install/besu/lib/*' AereCandidacy.java <command> [options]
|
|
*
|
|
* It uses the classes of that build and nothing else: the registry v3 forms (PqRegistryV3, patch 0008),
|
|
* the Falcon-512 and SLH-DSA-SHA2-128s schemes the node signs with, Besu's secp256k1 and Besu's own
|
|
* KeyPairUtil (the code path that creates data/key when a node starts for the first time).
|
|
*
|
|
* WHAT IT NEVER DOES: print, log, copy or transmit any private key, or any part of one. It reads the
|
|
* key files, signs, verifies its own signatures before writing anything, and writes PUBLIC data only.
|
|
* The shell scripts next to it check that again on their own output (validator-candidacy.sh).
|
|
*
|
|
* Commands:
|
|
* params --params <admission-params.json> prints "chainId bindHeight count index"
|
|
* node-key --data-path <dir> creates <dir>/key with Besu's KeyPairUtil if
|
|
* (and only if) it does not exist
|
|
* ids --node-key-file <key> --falcon-key <f> --slh-key <s> prints the public identity (JSON)
|
|
* candidacy --params <p> --row <row.json> --falcon-key <f> --slh-key <s> --node-key-file <key>
|
|
* --enode-host <host> --p2p-port <n> --issued <YYYY-MM-DD> --clock-measured-at <unix>
|
|
* --clock-head <n> --clock-samples <n> --clock-dmin <s> --clock-dmax <s> --out <file>
|
|
* clock-report --chain-id <c> --node-key-file <key> --enode-host <host> --p2p-port <n>
|
|
* --clock-measured-at <unix> --clock-head <n> --clock-samples <n> --clock-dmin <s>
|
|
* --clock-dmax <s> --out <file>
|
|
*
|
|
* The two signed messages (also written out in validator/README.md, "The candidacy file", so that
|
|
* anyone can verify a candidacy without this file):
|
|
*
|
|
* candidacy = keccak256("AERE-CANDIDACY-1" || u64 chainId || u64 bindHeight || u32 count || u32 index
|
|
* || u32 len(issued) || issued (ASCII, YYYY-MM-DD) || id (20 bytes) || rowHash (32 bytes))
|
|
* clock = keccak256("AERE-CLOCK-1" || u64 chainId || u64 measuredAt || u64 headNumber || u32 samples
|
|
* || i32 dMin || i32 dMax)
|
|
*
|
|
* signed by: candidacy -> the node key (secp256k1, 65 bytes r||s||v), the Falcon-512 key and the
|
|
* SLH-DSA-SHA2-128s key of the row; clock -> the node key. Integers are big-endian; i32 is two's
|
|
* complement. rowHash is the registry v3 row hash (AIP-22 section 3.2, PqRegistryV3.rowHash). The node
|
|
* key is bound through its ADDRESS: the secp256k1 signatures must recover to the row's ecdsa address,
|
|
* which is also its transport address, and the enode's public key must hash to that same address.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import org.hyperledger.besu.consensus.common.bft.FalconSealScheme;
|
|
import org.hyperledger.besu.consensus.common.bft.PqRegistryV3;
|
|
import org.hyperledger.besu.consensus.common.bft.SealScheme;
|
|
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
|
|
import org.hyperledger.besu.crypto.Hash;
|
|
import org.hyperledger.besu.crypto.KeyPair;
|
|
import org.hyperledger.besu.crypto.KeyPairUtil;
|
|
import org.hyperledger.besu.crypto.SECPPublicKey;
|
|
import org.hyperledger.besu.crypto.SECPSignature;
|
|
import org.hyperledger.besu.crypto.SignatureAlgorithm;
|
|
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
|
|
import org.hyperledger.besu.ethereum.core.Util;
|
|
|
|
import java.io.IOException;
|
|
import java.io.StringReader;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.StandardOpenOption;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Properties;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
import java.util.regex.Pattern;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
import org.apache.tuweni.bytes.Bytes;
|
|
import org.apache.tuweni.bytes.Bytes32;
|
|
import org.bouncycastle.pqc.crypto.falcon.FalconParameters;
|
|
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
|
|
|
|
public class AereCandidacy {
|
|
|
|
static final String CANDIDACY_DOMAIN = "AERE-CANDIDACY-1";
|
|
static final String CLOCK_DOMAIN = "AERE-CLOCK-1";
|
|
static final String CANDIDACY_FORMAT = "aere-validator-candidacy/1";
|
|
static final String CLOCK_FORMAT = "aere-validator-clock/1";
|
|
static final String PARAMS_FORMAT = "aere-admission-params/1";
|
|
static final ObjectMapper M = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
|
static final SignatureAlgorithm EC = SignatureAlgorithmFactory.getInstance();
|
|
static final Pattern HOST = Pattern.compile("^([A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?|\\[[0-9A-Fa-f:.]{2,45}\\])$");
|
|
static final Pattern DATE = Pattern.compile("^20[0-9]{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$");
|
|
|
|
public static void main(final String[] args) throws IOException {
|
|
if (args.length == 0) {
|
|
die(2, "usage: AereCandidacy.java params|node-key|ids|candidacy|clock-report [options] (see the header)");
|
|
}
|
|
final Map<String, String> o = new HashMap<>();
|
|
for (int i = 1; i < args.length; i++) {
|
|
if (!args[i].startsWith("--") || i + 1 >= args.length) {
|
|
die(2, "options are --name value pairs; bad argument: " + args[i]);
|
|
}
|
|
o.put(args[i].substring(2), args[++i]);
|
|
}
|
|
try {
|
|
switch (args[0]) {
|
|
case "params" -> {
|
|
final long[] p = params(o);
|
|
System.out.println(p[0] + " " + p[1] + " " + p[2] + " " + p[3]);
|
|
}
|
|
case "node-key" -> nodeKey(o);
|
|
case "ids" -> ids(o);
|
|
case "candidacy" -> candidacy(o);
|
|
case "clock-report" -> clockReport(o);
|
|
default -> die(2, "unknown command " + args[0]);
|
|
}
|
|
} catch (final IllegalArgumentException | IllegalStateException e) {
|
|
// messages here never carry key material: every one of them is written below by hand
|
|
die(1, "REFUSED: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// ---- the admission parameters the Foundation sends (public) ----------------------------------
|
|
static long[] params(final Map<String, String> o) throws IOException {
|
|
final JsonNode p = M.readTree(Files.readString(Path.of(need(o, "params")), StandardCharsets.UTF_8));
|
|
if (p == null || !p.isObject()) {
|
|
throw new IllegalArgumentException("the admission parameters are not a JSON object");
|
|
}
|
|
final Set<String> allowed = Set.of("format", "chainId", "bindHeight", "count", "index", "note");
|
|
for (final Iterator<String> it = p.fieldNames(); it.hasNext(); ) {
|
|
final String f = it.next();
|
|
if (!allowed.contains(f)) {
|
|
throw new IllegalArgumentException("the admission parameters carry an unknown field: " + f);
|
|
}
|
|
}
|
|
if (!PARAMS_FORMAT.equals(p.path("format").asText())) {
|
|
throw new IllegalArgumentException("the admission parameters are not of format " + PARAMS_FORMAT);
|
|
}
|
|
final long chain = integral(p, "chainId");
|
|
final long bind = integral(p, "bindHeight");
|
|
final long count = integral(p, "count");
|
|
final long index = integral(p, "index");
|
|
if (chain <= 0 || bind <= 0 || count < 1 || count > 64 || index < 0 || index >= count) {
|
|
throw new IllegalArgumentException("the admission parameters are out of range (chainId, bindHeight > 0; 1 <= count <= 64; 0 <= index < count)");
|
|
}
|
|
return new long[] {chain, bind, count, index};
|
|
}
|
|
|
|
// ---- the node key: Besu's own path, never a second implementation -----------------------------
|
|
static void nodeKey(final Map<String, String> o) {
|
|
final Path dir = Path.of(need(o, "data-path"));
|
|
final Path key = dir.resolve("key");
|
|
if (Files.exists(key)) {
|
|
throw new IllegalStateException(key + " already exists; nothing was created (the node key is never replaced)");
|
|
}
|
|
if (!Files.isDirectory(dir)) {
|
|
throw new IllegalStateException(dir + " is not a directory");
|
|
}
|
|
final KeyPair kp = KeyPairUtil.loadKeyPair(dir);
|
|
final ObjectNode out = M.createObjectNode();
|
|
out.put("created", key.toString());
|
|
out.put("address", hx(Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe()));
|
|
out.put("nodePublicKey", kp.getPublicKey().getEncodedBytes().toHexString());
|
|
print(out);
|
|
}
|
|
|
|
// ---- the public identity ------------------------------------------------------------------------
|
|
static void ids(final Map<String, String> o) throws IOException {
|
|
final KeyPair kp = nodeKeyPair(need(o, "node-key-file"));
|
|
final Falcon f = falcon(need(o, "falcon-key"), -1);
|
|
final Slh s = slh(need(o, "slh-key"), -1);
|
|
if (f.index != s.index) {
|
|
throw new IllegalArgumentException("the Falcon key file is for index " + f.index + ", the SLH-DSA key file for index " + s.index);
|
|
}
|
|
final ObjectNode out = M.createObjectNode();
|
|
out.put("address", hx(Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe()));
|
|
out.put("nodePublicKey", kp.getPublicKey().getEncodedBytes().toHexString());
|
|
out.put("index", f.index);
|
|
out.put("id", hx(PqRegistryV3.idOf(f.std)));
|
|
out.put("falcon512PublicKeySha256", Hash.sha256(Bytes.wrap(f.std)).toHexString());
|
|
out.put("slhDsaSha2128sPublicKey", hx(s.pk));
|
|
print(out);
|
|
}
|
|
|
|
// ---- the candidacy --------------------------------------------------------------------------------
|
|
static void candidacy(final Map<String, String> o) throws IOException {
|
|
final long[] p = params(o);
|
|
final long chain = p[0], bind = p[1];
|
|
final int count = (int) p[2], index = (int) p[3];
|
|
final String issued = need(o, "issued");
|
|
if (!DATE.matcher(issued).matches()) {
|
|
throw new IllegalArgumentException("--issued must be a UTC date YYYY-MM-DD");
|
|
}
|
|
final KeyPair kp = nodeKeyPair(need(o, "node-key-file"));
|
|
final byte[] addr = Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe();
|
|
final byte[] nodePub = kp.getPublicKey().getEncodedBytes().toArrayUnsafe();
|
|
final Falcon f = falcon(need(o, "falcon-key"), index);
|
|
final Slh s = slh(need(o, "slh-key"), index);
|
|
final String enode = enode(nodePub, o);
|
|
final Path out = Path.of(need(o, "out"));
|
|
if (Files.exists(out)) {
|
|
throw new IllegalStateException(out + " exists; a candidacy file is never overwritten");
|
|
}
|
|
|
|
// the row, as PqRegistryV3Tool wrote it: it must be THIS node's keys, bound to THESE parameters
|
|
final JsonNode rf = M.readTree(Files.readString(Path.of(need(o, "row")), StandardCharsets.UTF_8));
|
|
if (rf == null || rf.path("index").asInt(-1) != index || !rf.has("row")) {
|
|
throw new IllegalArgumentException("the row file is not a PqRegistryV3Tool row for index " + index);
|
|
}
|
|
final JsonNode row = rf.get("row");
|
|
final byte[] id = hex(text(row, "id"));
|
|
if (!Arrays.equals(id, PqRegistryV3.idOf(f.std))) {
|
|
throw new IllegalArgumentException("the row's id is not the derivation of this Falcon key (a new id must be)");
|
|
}
|
|
if (!Arrays.equals(hex(text(row.path("keys"), "falcon-512")), f.std)
|
|
|| !Arrays.equals(hex(text(row.path("keys"), "slh-dsa-sha2-128s")), s.pk)) {
|
|
throw new IllegalArgumentException("the row carries other public keys than the key files given");
|
|
}
|
|
if (row.has("rotation")) {
|
|
throw new IllegalArgumentException("the row carries a rotation; a new validator's row never does");
|
|
}
|
|
final byte[] transport = hex(text(row.path("transport"), "secp256k1Address"));
|
|
final byte[] ecAddr = hex(text(row.path("ecdsa"), "addr"));
|
|
if (!Arrays.equals(transport, addr) || !Arrays.equals(ecAddr, addr)) {
|
|
throw new IllegalArgumentException("the row's transport and ecdsa addresses must both be the address of the NODE key"
|
|
+ " (the consensus key is the node key; a validator whose node key differs receives no consensus message)");
|
|
}
|
|
final byte[] falconPop = hex(text(row.path("pop"), "falcon-512"));
|
|
final byte[] slhPop = hex(text(row.path("pop"), "slh-dsa-sha2-128s"));
|
|
final TreeMap<Integer, PqRegistryV3.Key> keys = new TreeMap<>();
|
|
keys.put(SealSchemes.FALCON_512.wireId() & 0xff, new PqRegistryV3.Key(SealSchemes.FALCON_512, f.std, falconPop));
|
|
keys.put(SealSchemes.SLH_DSA_128S.wireId() & 0xff, new PqRegistryV3.Key(SealSchemes.SLH_DSA_128S, s.pk, slhPop));
|
|
final Bytes32 rowHash =
|
|
PqRegistryV3.rowHash(chain, bind, count, new PqRegistryV3.Row(index, id, keys, transport, Map.of(), null, null));
|
|
// the row's own proofs, checked again here before this node signs anything about it
|
|
if (!SealSchemes.FALCON_512.verifyRaw(f.raw896,
|
|
PqRegistryV3.popMessage(chain, bind, count, index, id, SealSchemes.FALCON_512.wireId(), f.std).toArrayUnsafe(), falconPop)
|
|
|| !SealSchemes.SLH_DSA_128S.verifyRaw(s.pk,
|
|
PqRegistryV3.popMessage(chain, bind, count, index, id, SealSchemes.SLH_DSA_128S.wireId(), s.pk).toArrayUnsafe(), slhPop)) {
|
|
throw new IllegalStateException("a proof of possession in the row does not verify for these parameters");
|
|
}
|
|
final SECPSignature claim = EC.decodeSignature(Bytes.wrap(hex(text(row.path("ecdsa"), "claim"))));
|
|
final Optional<SECPPublicKey> claimer = EC.recoverPublicKeyFromSignature(PqRegistryV3.claimDigest(chain, rowHash), claim);
|
|
if (claimer.isEmpty() || !Arrays.equals(Util.publicKeyToAddress(claimer.get()).getBytes().toArrayUnsafe(), addr)) {
|
|
throw new IllegalStateException("the row's ecdsa claim does not recover to the node key's address");
|
|
}
|
|
|
|
// the candidacy proof: three signatures over a message bound to the chain, the epoch, the date and the row
|
|
final Bytes32 msg = candidacyMessage(chain, bind, count, index, issued, id, rowHash);
|
|
final SECPSignature es = EC.sign(msg, kp);
|
|
final byte[] fs = ((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(f.sk, msg.toArrayUnsafe())
|
|
.orElseThrow(() -> new IllegalStateException("the Falcon key did not sign"));
|
|
final byte[] ss = SealSchemes.SLH_DSA_128S.sign(s.sk, msg.toArrayUnsafe())
|
|
.orElseThrow(() -> new IllegalStateException("the SLH-DSA key did not sign"));
|
|
selfCheck(msg, es, nodePub, f, fs, s, ss);
|
|
final ObjectNode clock = clock(o, chain, nodePub, kp);
|
|
|
|
final ObjectNode c = M.createObjectNode();
|
|
c.put("format", CANDIDACY_FORMAT);
|
|
c.put("chainId", chain);
|
|
c.put("bindHeight", bind);
|
|
c.put("count", count);
|
|
c.put("index", index);
|
|
c.put("issued", issued);
|
|
c.put("enode", enode);
|
|
c.put("address", hx(addr));
|
|
c.put("id", hx(id));
|
|
c.set("row", row);
|
|
c.put("rowHash", rowHash.toHexString());
|
|
final ObjectNode pr = c.putObject("proof");
|
|
pr.put("domain", CANDIDACY_DOMAIN);
|
|
pr.put("message", msg.toHexString());
|
|
pr.put("secp256k1", es.encodedBytes().toHexString());
|
|
pr.put("falcon-512", hx(fs));
|
|
pr.put("slh-dsa-sha2-128s", hx(ss));
|
|
c.set("clock", clock);
|
|
Files.writeString(out, M.writeValueAsString(c) + "\n", StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
|
|
final ObjectNode sum = M.createObjectNode();
|
|
sum.put("written", out.toString());
|
|
sum.put("enode", enode);
|
|
sum.put("address", hx(addr));
|
|
sum.put("id", hx(id));
|
|
sum.put("rowHash", rowHash.toHexString());
|
|
print(sum);
|
|
}
|
|
|
|
static void clockReport(final Map<String, String> o) throws IOException {
|
|
final long chain = Long.parseLong(need(o, "chain-id"));
|
|
final KeyPair kp = nodeKeyPair(need(o, "node-key-file"));
|
|
final byte[] nodePub = kp.getPublicKey().getEncodedBytes().toArrayUnsafe();
|
|
final Path out = Path.of(need(o, "out"));
|
|
if (Files.exists(out)) {
|
|
throw new IllegalStateException(out + " exists; a clock report is never overwritten");
|
|
}
|
|
final ObjectNode c = M.createObjectNode();
|
|
c.put("format", CLOCK_FORMAT);
|
|
c.put("chainId", chain);
|
|
c.put("enode", enode(nodePub, o));
|
|
c.put("address", hx(Util.publicKeyToAddress(kp.getPublicKey()).getBytes().toArrayUnsafe()));
|
|
c.set("clock", clock(o, chain, nodePub, kp));
|
|
Files.writeString(out, M.writeValueAsString(c) + "\n", StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
|
|
final ObjectNode sum = M.createObjectNode();
|
|
sum.put("written", out.toString());
|
|
sum.set("clock", c.get("clock"));
|
|
print(sum);
|
|
}
|
|
|
|
// ---- messages -------------------------------------------------------------------------------------
|
|
static Bytes32 candidacyMessage(final long chain, final long bind, final int count, final int index, final String issued,
|
|
final byte[] id, final Bytes32 rowHash) {
|
|
final byte[] d = CANDIDACY_DOMAIN.getBytes(StandardCharsets.US_ASCII);
|
|
final byte[] is = issued.getBytes(StandardCharsets.US_ASCII);
|
|
final ByteBuffer b = ByteBuffer.allocate(d.length + 8 + 8 + 4 + 4 + 4 + is.length + 20 + 32);
|
|
b.put(d).putLong(chain).putLong(bind).putInt(count).putInt(index).putInt(is.length).put(is).put(id)
|
|
.put(rowHash.toArrayUnsafe());
|
|
return Hash.keccak256(Bytes.wrap(b.array()));
|
|
}
|
|
|
|
static Bytes32 clockMessage(final long chain, final long measuredAt, final long head, final int samples, final int dMin,
|
|
final int dMax) {
|
|
final byte[] d = CLOCK_DOMAIN.getBytes(StandardCharsets.US_ASCII);
|
|
final ByteBuffer b = ByteBuffer.allocate(d.length + 8 + 8 + 8 + 4 + 4 + 4);
|
|
b.put(d).putLong(chain).putLong(measuredAt).putLong(head).putInt(samples).putInt(dMin).putInt(dMax);
|
|
return Hash.keccak256(Bytes.wrap(b.array()));
|
|
}
|
|
|
|
static ObjectNode clock(final Map<String, String> o, final long chain, final byte[] nodePub, final KeyPair kp) {
|
|
final long at = Long.parseLong(need(o, "clock-measured-at"));
|
|
final long head = Long.parseLong(need(o, "clock-head"));
|
|
final int samples = Integer.parseInt(need(o, "clock-samples"));
|
|
final int dMin = Integer.parseInt(need(o, "clock-dmin"));
|
|
final int dMax = Integer.parseInt(need(o, "clock-dmax"));
|
|
if (samples < 1 || dMin > dMax || head < 1 || at < 1) {
|
|
throw new IllegalArgumentException("the clock measurement is malformed");
|
|
}
|
|
final Bytes32 cm = clockMessage(chain, at, head, samples, dMin, dMax);
|
|
final SECPSignature cs = EC.sign(cm, kp);
|
|
final Optional<SECPPublicKey> back = EC.recoverPublicKeyFromSignature(cm, cs);
|
|
if (back.isEmpty() || !Arrays.equals(back.get().getEncodedBytes().toArrayUnsafe(), nodePub)) {
|
|
throw new IllegalStateException("the clock signature does not recover to the node key; nothing written");
|
|
}
|
|
final ObjectNode c = M.createObjectNode();
|
|
c.put("domain", CLOCK_DOMAIN);
|
|
c.put("measuredAt", at);
|
|
c.put("headNumber", head);
|
|
c.put("samples", samples);
|
|
c.put("dMin", dMin);
|
|
c.put("dMax", dMax);
|
|
c.put("secp256k1", cs.encodedBytes().toHexString());
|
|
return c;
|
|
}
|
|
|
|
static void selfCheck(final Bytes32 msg, final SECPSignature es, final byte[] nodePub, final Falcon f, final byte[] fs,
|
|
final Slh s, final byte[] ss) {
|
|
final Optional<SECPPublicKey> back = EC.recoverPublicKeyFromSignature(msg, es);
|
|
if (back.isEmpty() || !Arrays.equals(back.get().getEncodedBytes().toArrayUnsafe(), nodePub)) {
|
|
throw new IllegalStateException("the secp256k1 signature does not recover to the node key; nothing written");
|
|
}
|
|
if (!SealSchemes.FALCON_512.verifyRaw(f.raw896, msg.toArrayUnsafe(), fs)) {
|
|
throw new IllegalStateException("the Falcon-512 signature does not verify; nothing written");
|
|
}
|
|
if (!SealSchemes.SLH_DSA_128S.verifyRaw(s.pk, msg.toArrayUnsafe(), ss)) {
|
|
throw new IllegalStateException("the SLH-DSA signature does not verify; nothing written");
|
|
}
|
|
}
|
|
|
|
// ---- key files (read, never printed) -----------------------------------------------------------
|
|
record Falcon(int index, FalconPrivateKeyParameters sk, byte[] raw896, byte[] std) {}
|
|
|
|
record Slh(int index, SealScheme.PrivateHandle sk, byte[] pk) {}
|
|
|
|
static Falcon falcon(final String file, final int wantIndex) throws IOException {
|
|
final Properties fp = new Properties();
|
|
fp.load(new StringReader(Files.readString(Path.of(file), StandardCharsets.UTF_8)));
|
|
final int index = Integer.parseInt(prop(fp, "index", "the Falcon key file"));
|
|
if (wantIndex >= 0 && index != wantIndex) {
|
|
throw new IllegalArgumentException("the Falcon key file is for index " + index + ", the admission parameters say " + wantIndex);
|
|
}
|
|
final byte[] h = hex(prop(fp, "pk", "the Falcon key file"));
|
|
if (h.length != PqRegistryV3.FALCON_512_PK_LENGTH - 1) {
|
|
throw new IllegalArgumentException("the Falcon key file's public key is not 896 bytes");
|
|
}
|
|
final FalconPrivateKeyParameters sk = new FalconPrivateKeyParameters(FalconParameters.falcon_512,
|
|
hex(prop(fp, "f", "the Falcon key file")), hex(prop(fp, "g", "the Falcon key file")), hex(prop(fp, "F", "the Falcon key file")), h);
|
|
final byte[] std = new byte[PqRegistryV3.FALCON_512_PK_LENGTH];
|
|
std[0] = PqRegistryV3.FALCON_512_PK_HEADER;
|
|
System.arraycopy(h, 0, std, 1, h.length);
|
|
// the private half must match the public half: sign a probe and verify it under the public key
|
|
final byte[] probe = Hash.keccak256(Bytes.wrap("AERE-KIT-PROBE".getBytes(StandardCharsets.US_ASCII))).toArrayUnsafe();
|
|
final byte[] sig = ((FalconSealScheme) SealSchemes.FALCON_512).signWithParams(sk, probe)
|
|
.orElseThrow(() -> new IllegalArgumentException("the Falcon key does not sign"));
|
|
if (!SealSchemes.FALCON_512.verifyRaw(h, probe, sig)) {
|
|
throw new IllegalArgumentException("the Falcon key file's private and public halves do not match");
|
|
}
|
|
return new Falcon(index, sk, h, std);
|
|
}
|
|
|
|
static Slh slh(final String file, final int wantIndex) throws IOException {
|
|
final Properties sp = new Properties();
|
|
sp.load(new StringReader(Files.readString(Path.of(file), StandardCharsets.UTF_8)));
|
|
final int index = Integer.parseInt(prop(sp, "index", "the SLH-DSA key file"));
|
|
if (wantIndex >= 0 && index != wantIndex) {
|
|
throw new IllegalArgumentException("the SLH-DSA key file is for index " + index + ", the admission parameters say " + wantIndex);
|
|
}
|
|
if (!"slh-dsa-sha2-128s".equals(sp.getProperty("alg", "").trim())) {
|
|
throw new IllegalArgumentException("the SLH-DSA key file does not name alg=slh-dsa-sha2-128s");
|
|
}
|
|
final SealScheme.PrivateHandle sk = SealSchemes.SLH_DSA_128S.parsePrivateKey(hex(prop(sp, "sk", "the SLH-DSA key file")))
|
|
.orElseThrow(() -> new IllegalArgumentException("the SLH-DSA private key does not parse"));
|
|
final byte[] pk = hex(prop(sp, "pk", "the SLH-DSA key file"));
|
|
final byte[] probe = Hash.keccak256(Bytes.wrap("AERE-KIT-PROBE".getBytes(StandardCharsets.US_ASCII))).toArrayUnsafe();
|
|
final byte[] sig = SealSchemes.SLH_DSA_128S.sign(sk, probe).orElseThrow(() -> new IllegalArgumentException("the SLH-DSA key does not sign"));
|
|
if (!SealSchemes.SLH_DSA_128S.verifyRaw(pk, probe, sig)) {
|
|
throw new IllegalArgumentException("the SLH-DSA key file's private and public halves do not match");
|
|
}
|
|
return new Slh(index, sk, pk);
|
|
}
|
|
|
|
static KeyPair nodeKeyPair(final String file) throws IOException {
|
|
final String t = Files.readString(Path.of(file), StandardCharsets.UTF_8).trim();
|
|
if (!t.matches("^(0x)?[0-9a-fA-F]{64}$")) {
|
|
throw new IllegalArgumentException("the node key file does not hold a 32-byte hex key");
|
|
}
|
|
return EC.createKeyPair(EC.createPrivateKey(Bytes32.fromHexString(t)));
|
|
}
|
|
|
|
static String enode(final byte[] nodePub, final Map<String, String> o) {
|
|
final String host = need(o, "enode-host");
|
|
final int port = Integer.parseInt(need(o, "p2p-port"));
|
|
if (!HOST.matcher(host).matches() || port < 1 || port > 65535) {
|
|
throw new IllegalArgumentException("--enode-host must be a host name or an IP address and --p2p-port a port number");
|
|
}
|
|
return "enode://" + hx(nodePub).substring(2) + "@" + host + ":" + port;
|
|
}
|
|
|
|
// ---- small helpers -----------------------------------------------------------------------------
|
|
static String prop(final Properties p, final String k, final String what) {
|
|
final String v = p.getProperty(k);
|
|
if (v == null || v.isBlank()) {
|
|
throw new IllegalArgumentException(what + " has no '" + k + "' entry");
|
|
}
|
|
return v.trim();
|
|
}
|
|
|
|
static long integral(final JsonNode p, final String k) {
|
|
final JsonNode v = p.get(k);
|
|
if (v == null || !v.isIntegralNumber()) {
|
|
throw new IllegalArgumentException("the admission parameters need the integer field " + k);
|
|
}
|
|
return v.asLong();
|
|
}
|
|
|
|
static String text(final JsonNode n, final String k) {
|
|
final JsonNode v = n == null ? null : n.get(k);
|
|
if (v == null || !v.isTextual()) {
|
|
throw new IllegalArgumentException("the row has no text field " + k);
|
|
}
|
|
return v.asText();
|
|
}
|
|
|
|
static String need(final Map<String, String> o, final String k) {
|
|
final String v = o.get(k);
|
|
if (v == null || v.isBlank()) {
|
|
throw new IllegalArgumentException("missing --" + k);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
static byte[] hex(final String s) {
|
|
return Bytes.fromHexStringLenient(s.trim()).toArrayUnsafe();
|
|
}
|
|
|
|
static String hx(final byte[] b) {
|
|
final StringBuilder sb = new StringBuilder("0x");
|
|
for (final byte x : b) {
|
|
sb.append(String.format(Locale.ROOT, "%02x", x & 0xff));
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
static void print(final ObjectNode n) {
|
|
try {
|
|
System.out.println(M.writeValueAsString(n));
|
|
} catch (final IOException e) {
|
|
die(2, "cannot write output");
|
|
}
|
|
}
|
|
|
|
static void die(final int code, final String msg) {
|
|
System.err.println(msg);
|
|
System.exit(code);
|
|
}
|
|
}
|