From 0803852bf53220b7ce30d947c37583126be86dbe Mon Sep 17 00:00:00 2001
From: AE <98699388+A-EDev@users.noreply.github.com>
Date: Sat, 7 Nov 2026 20:04:54 +0200
Subject: [PATCH 1/2] fix(sync): accept only signatures whose key and R have
order L
---
.../github/aedev/flow/sync2/crypto/Curves.kt | 32 +--
.../io/github/aedev/flow/sync2/VectorsTest.kt | 2 +-
app/src/test/resources/sync2/README.md | 4 +-
app/src/test/resources/sync2/ed25519.json | 237 +++++++++++++++++-
app/src/test/resources/sync2/roster.json | 44 ++++
5 files changed, 287 insertions(+), 32 deletions(-)
diff --git a/app/src/main/java/io/github/aedev/flow/sync2/crypto/Curves.kt b/app/src/main/java/io/github/aedev/flow/sync2/crypto/Curves.kt
index 8d3d4b932..efa9b31de 100644
--- a/app/src/main/java/io/github/aedev/flow/sync2/crypto/Curves.kt
+++ b/app/src/main/java/io/github/aedev/flow/sync2/crypto/Curves.kt
@@ -1,40 +1,15 @@
package io.github.aedev.flow.sync2.crypto
-import io.github.aedev.flow.sync.crypto.SyncBytes
import org.bouncycastle.math.ec.rfc7748.X25519
import org.bouncycastle.math.ec.rfc8032.Ed25519
-import java.math.BigInteger
private const val KEY_SIZE = 32
private const val SIGNATURE_SIZE = 64
-/** 2^255 - 19; an encoded y at or above it is non-canonical. */
-private val FIELD_PRIME = BigInteger.ONE.shiftLeft(255).subtract(BigInteger.valueOf(19))
+/** A canonical encoding of a point of order exactly L (spec/crypto.md, Ed25519 checks). */
+private fun startsWithPointOfOrderL(bytes: ByteArray): Boolean = Ed25519.validatePublicKeyFull(bytes, 0)
-/** The eight canonical encodings of small-order points (spec/crypto.md, Ed25519 checks). */
-private val SMALL_ORDER_KEYS =
- listOf(
- "0100000000000000000000000000000000000000000000000000000000000000",
- "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
- "0000000000000000000000000000000000000000000000000000000000000000",
- "0000000000000000000000000000000000000000000000000000000000000080",
- "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
- "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85",
- "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
- "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
- )
-
-private fun canonicalY(key: ByteArray): Boolean {
- val y = key.reversedArray().also { it[0] = (it[0].toInt() and 0x7f).toByte() }
- return BigInteger(1, y) < FIELD_PRIME
-}
-
-/** Rejects non-canonical encodings, small-order keys, and bytes that aren't a curve point. */
-fun isValidSigningKey(publicKey: ByteArray): Boolean =
- publicKey.size == KEY_SIZE &&
- canonicalY(publicKey) &&
- SyncBytes.toHex(publicKey) !in SMALL_ORDER_KEYS &&
- Ed25519.validatePublicKeyPartial(publicKey, 0)
+fun isValidSigningKey(publicKey: ByteArray): Boolean = publicKey.size == KEY_SIZE && startsWithPointOfOrderL(publicKey)
fun signingPublicKey(seed: ByteArray): ByteArray = ByteArray(KEY_SIZE).also { Ed25519.generatePublicKey(seed, 0, it, 0) }
@@ -50,6 +25,7 @@ fun verify(
): Boolean =
signature.size == SIGNATURE_SIZE &&
isValidSigningKey(publicKey) &&
+ startsWithPointOfOrderL(signature) &&
Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.size)
fun agreementPublicKey(secret: ByteArray): ByteArray = ByteArray(KEY_SIZE).also { X25519.scalarMultBase(secret, 0, it, 0) }
diff --git a/app/src/test/java/io/github/aedev/flow/sync2/VectorsTest.kt b/app/src/test/java/io/github/aedev/flow/sync2/VectorsTest.kt
index 7555a6ab2..29a971a9c 100644
--- a/app/src/test/java/io/github/aedev/flow/sync2/VectorsTest.kt
+++ b/app/src/test/java/io/github/aedev/flow/sync2/VectorsTest.kt
@@ -131,7 +131,7 @@ class VectorsTest {
val valid = verify(c["publicKey"].bytes, c["message"].bytes, c["signature"].bytes)
assertEquals(c["name"].text, c["valid"].jsonPrimitive.boolean, valid)
}
- for (key in v["weakKeys"].items + v["nonCanonicalKeys"].items) {
+ for (key in v["weakKeys"].items + v["nonCanonicalKeys"].items + v["mixedOrderKeys"].items) {
assertFalse(key.text, isValidSigningKey(key.bytes))
}
}
diff --git a/app/src/test/resources/sync2/README.md b/app/src/test/resources/sync2/README.md
index e4df2893a..7d9c9224d 100644
--- a/app/src/test/resources/sync2/README.md
+++ b/app/src/test/resources/sync2/README.md
@@ -8,13 +8,13 @@ All byte strings are lowercase hex, except inside roster entries, which use the
|---|---|
| `canonical.json` | Canonical JSON serialization, including keys outside the Basic Multilingual Plane, which sort by UTF-16 code unit, not by code point |
| `derive.json` | `Derive` and `Hash` ([crypto.md](../crypto.md#labelled-derivation)) |
-| `ed25519.json` | Strict verification: valid, tampered, wrong key, non-canonical `S`, small-order key; the eight small-order encodings; a non-canonical key encoding |
+| `ed25519.json` | Strict verification: valid, tampered, wrong key, non-canonical `S`, small-order key; every small-order encoding of `R`, canonical or not; `R` and keys with a small-order part under either equation; the [ed25519-speccheck](https://github.com/novifinancial/ed25519-speccheck) set; the eight small-order encodings (`weakKeys`), non-canonical key encodings (`nonCanonicalKeys`) and keys with a small-order part (`mixedOrderKeys`), none of which may enter a roster |
| `box.json` | Box seal and open with fixed salts; tampered ciphertext, tag, commitment and salt, wrong key, wrong AAD, truncation |
| `payload.json` | PADMÉ sizes; payload decoding; bad length, non-zero padding, corrupt gzip |
| `keys.json` | Member ids, epoch commitment, purpose keys, HPKE wrap with a fixed ephemeral key, wrong-epoch and wrong-member unwraps, history chain, wrap and chain paths |
| `recovery.json` | Recovery secret, 24 words and derived keys; words with a bad checksum, 23 words, and a valid 12-word phrase |
| `envelope.json` | Log objects of each type, including a store login, opened in stream order, a live message, a snapshot blob; truncation, magic, suite, group, unknown sender, signature, tampering, replay, gap, a log read from the middle, wrong `prev`, swapped epoch, `seq` above 253 − 1, blob hash |
-| `roster.json` | A chain through genesis, add, rename, remove, restore, recovery change, add, leave and rotate, with the state after each entry (including whether a rotate is due) and the device names it carries; bad and missing signatures, unknown fields, wrong `seq`, truncated chain, removed signer, weak key, re-adding a removed device, wrong epoch sets and numbers, self-removal, renaming the recovery key, a non-member signer, a used recovery key; a leave by a non-member, by the recovery member and by the last device, a leave removing another device or carrying an epoch, a rotate without an epoch, with a removal, or wrapped to a device that left; six forks and their winners, including leave against remove and rotate, and concurrent rotates; a genesis whose group id isn't the one its keys commit to |
+| `roster.json` | A chain through genesis, add, rename, remove, restore, recovery change, add, leave and rotate, with the state after each entry (including whether a rotate is due, and each member's epochs and `cut`) and the device names it carries; bad and missing signatures, unknown fields, wrong `seq`, truncated chain, removed signer, weak key, a key with a small-order part, an identity `R`, re-adding a removed device, wrong epoch sets and numbers, self-removal, renaming the recovery key, a non-member signer, a used recovery key; a leave by a non-member, by the recovery member and by the last device, a leave removing another device or carrying an epoch, a rotate without an epoch, with a removal, or wrapped to a device that left; six forks and their winners, including leave against remove and rotate, and concurrent rotates; a genesis whose group id isn't the one its keys commit to; a `cut` on the wrong op, for a device not removed, empty, or at `seq` 0; `replacements`, entries on top of the chain where the device that set the recovery key is removed or has left |
| `forks.json` | Named entries, and cases that settle a store's view of them: `applied` is what the device had applied, `view` is the store's answer, `pins` the chosen entries, `linear` a Flow server. Each case gives the outcome and, in any order of `view`, the same chain, standing entries that lost and start an epoch, contested set and sides |
| `pairing.json` | Pairing keys, SAS, mailbox slot, and a sealed `join` message |
| `lan.json` | Discovery tag, hello AAD, transcripts, session keys, frame AAD |
diff --git a/app/src/test/resources/sync2/ed25519.json b/app/src/test/resources/sync2/ed25519.json
index a581db1e5..09469b151 100644
--- a/app/src/test/resources/sync2/ed25519.json
+++ b/app/src/test/resources/sync2/ed25519.json
@@ -10,7 +10,18 @@
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa"
],
"nonCanonicalKeys": [
- "f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"
+ "f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
+ "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
+ "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
+ "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "0100000000000000000000000000000000000000000000000000000000000080",
+ "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
+ ],
+ "mixedOrderKeys": [
+ "5886ca37f41cdb6d9863e08d6ad4d6e86c8347315943fa20e5dbff0fd6698fac",
+ "5a8f610c2188f1238be160112874bb81bbd85e7c75b358ea45633e61506c3bf4",
+ "6bcc03dc957b26dacac0c994a2813a4c1a88c7f1f17df76dcc4aed337751987e"
],
"cases": [
{
@@ -47,6 +58,230 @@
"message": "464c4f572d53594e432f3220766563746f72206d657373616765",
"signature": "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"valid": false
+ },
+ {
+ "name": "valid-chosen-nonce",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "3e05294c61d6b33963744952abb10c3460d0b0c9f9f79ffe2416e539d3118da7ca0f2e9eca79af0bd79c28a8b74bef97921782de5c4b9c055f2666e46e719009",
+ "valid": true
+ },
+ {
+ "name": "identity-r",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "0100000000000000000000000000000000000000000000000000000000000000941d68395ad14540ae38a328b69d89b8bc65c85c46dac58e55338894f476a904",
+ "valid": false
+ },
+ {
+ "name": "small-order-r-1",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037af639566709d510ad43b15e62caa8514e28bc4195fa764e220414afdff9dd930a",
+ "valid": false
+ },
+ {
+ "name": "small-order-r-2",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "0000000000000000000000000000000000000000000000000000000000000080f023e6598d91c7db494a933cfcab9108d7781a93e2a4e67debcbfaccfb4a5802",
+ "valid": false
+ },
+ {
+ "name": "small-order-r-3",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05d078ee6ee10ceba83cd54d1bbe503bf8f1cc855b29a49375f6a526a953a29304",
+ "valid": false
+ },
+ {
+ "name": "small-order-r-4",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f7ae8fa2eee23758ba2fba4961516d67bcff2d1f71315daf7fdcdfba1c058f608",
+ "valid": false
+ },
+ {
+ "name": "small-order-r-5",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85cb0cd18267c1681d4408bf32745c962b141e884390cde3503e8824cb904f2f01",
+ "valid": false
+ },
+ {
+ "name": "small-order-r-6",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "0000000000000000000000000000000000000000000000000000000000000000140e32a756abad31fbb5501017fc388e4cf78e627b1db0cc9cc04b7dc7f17c08",
+ "valid": false
+ },
+ {
+ "name": "small-order-r-7",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fada37166c9fd2bb0ac494a8a56a5ebc0d113002a4c76f3c8d7b510175bdd8a802",
+ "valid": false
+ },
+ {
+ "name": "non-canonical-r-0",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7fc69d72943886b495aa551b7971e309360ef9b030ede89df271e6e127b0cf7500",
+ "valid": false
+ },
+ {
+ "name": "non-canonical-r-1",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "edfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa030fe3400b016e080205429115012e0e3e3ecc36123f58850c814cfc4f8d0e",
+ "valid": false
+ },
+ {
+ "name": "non-canonical-r-2",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7fbb7b303b5200a2e16e8e1feb3b9c5c13ea36684667ccc1a8881358094a480407",
+ "valid": false
+ },
+ {
+ "name": "non-canonical-r-3",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3236e31e55596a6d3d88cdba7ce4416ab3cd2b14020be2f7fa716fa4de691a0b",
+ "valid": false
+ },
+ {
+ "name": "non-canonical-r-4",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "01000000000000000000000000000000000000000000000000000000000000800059436b9f1f6bf61e9204d05d27f3e8ff0d41b4d6eef81e7b856080bb41a602",
+ "valid": false
+ },
+ {
+ "name": "non-canonical-r-5",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1e0acb2856103aff9a4b01cfc85ef33a29cdc0bfce95a7cfd80699583f975f06",
+ "valid": false
+ },
+ {
+ "name": "mixed-order-r-8",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "2e93e683fdba4ce21af4180584303713bae5afe91f52adf537aec8b63cccc9811d2785ab4f07943286668eb249d3a40ddda9d97ded32ebd382b0226fdd44ee0e",
+ "valid": false
+ },
+ {
+ "name": "mixed-order-r-4",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "42eb113403260f8938b4544a60226758ca1c20efd3171c8b468a29e418d646ce10c077db60e2d766597488a97f8fe70b647ba7b68caa3072f00f577480410d06",
+ "valid": false
+ },
+ {
+ "name": "mixed-order-r-2",
+ "publicKey": "8233fc236a84d925353f366b5d7ec5b3e577380e0e82089233b512cc88ae6781",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "affad6b39e294cc69c8bb6ad544ef3cb9f2f4f3606086001dbe91ac62cee72587296fa6a2038d0ad19e03cb0ab2ae38b09647e8604e610628e8cdaca57f5460d",
+ "valid": false
+ },
+ {
+ "name": "mixed-order-key-both-equations",
+ "publicKey": "5886ca37f41cdb6d9863e08d6ad4d6e86c8347315943fa20e5dbff0fd6698fac",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "c54e869091a9f45cc52b107d3a7007df9137556431da677d88e9536c67f9f042ad497de73a88e76951b4a4f36d5079240498328b9b6f1ec20f16bd4182a6b104",
+ "valid": false
+ },
+ {
+ "name": "mixed-order-key-cofactored-only",
+ "publicKey": "5886ca37f41cdb6d9863e08d6ad4d6e86c8347315943fa20e5dbff0fd6698fac",
+ "message": "464c4f572d53594e432f3220766563746f72206d657373616765",
+ "signature": "3e05294c61d6b33963744952abb10c3460d0b0c9f9f79ffe2416e539d3118da78e95f1ac1af239bbaeb9d830e0bb14b5775e84b9a0274c5b715e57177bc5640a",
+ "valid": false
+ },
+ {
+ "name": "speccheck-0",
+ "publicKey": "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
+ "message": "8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6",
+ "signature": "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a0000000000000000000000000000000000000000000000000000000000000000",
+ "valid": false
+ },
+ {
+ "name": "speccheck-1",
+ "publicKey": "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
+ "message": "9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79",
+ "signature": "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43a5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
+ "valid": false
+ },
+ {
+ "name": "speccheck-2",
+ "publicKey": "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
+ "message": "aebf3f2601a0c8c5d39cc7d8911642f740b78168218da8471772b35f9d35b9ab",
+ "signature": "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa8c4bd45aecaca5b24fb97bc10ac27ac8751a7dfe1baff8b953ec9f5833ca260e",
+ "valid": false
+ },
+ {
+ "name": "speccheck-3",
+ "publicKey": "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
+ "message": "9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79",
+ "signature": "9046a64750444938de19f227bb80485e92b83fdb4b6506c160484c016cc1852f87909e14428a7a1d62e9f22f3d3ad7802db02eb2e688b6c52fcd6648a98bd009",
+ "valid": false
+ },
+ {
+ "name": "speccheck-4",
+ "publicKey": "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
+ "message": "e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c",
+ "signature": "160a1cb0dc9c0258cd0a7d23e94d8fa878bcb1925f2c64246b2dee1796bed5125ec6bc982a269b723e0668e540911a9a6a58921d6925e434ab10aa7940551a09",
+ "valid": false
+ },
+ {
+ "name": "speccheck-5",
+ "publicKey": "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
+ "message": "e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c",
+ "signature": "21122a84e0b5fca4052f5b1235c80a537878b38f3142356b2c2384ebad4668b7e40bc836dac0f71076f9abe3a53f9c03c1ceeeddb658d0030494ace586687405",
+ "valid": false
+ },
+ {
+ "name": "speccheck-6",
+ "publicKey": "442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623",
+ "message": "85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40",
+ "signature": "e96f66be976d82e60150baecff9906684aebb1ef181f67a7189ac78ea23b6c0e547f7690a0e2ddcd04d87dbc3490dc19b3b3052f7ff0538cb68afb369ba3a514",
+ "valid": false
+ },
+ {
+ "name": "speccheck-7",
+ "publicKey": "442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623",
+ "message": "85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40",
+ "signature": "8ce5b96c8f26d0ab6c47958c9e68b937104cd36e13c33566acd2fe8d38aa19427e71f98a473474f2f13f06f97c20d58cc3f54b8bd0d272f42b695dd7e89a8c22",
+ "valid": false
+ },
+ {
+ "name": "speccheck-8",
+ "publicKey": "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
+ "message": "9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41",
+ "signature": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03be9678ac102edcd92b0210bb34d7428d12ffc5df5f37e359941266a4e35f0f",
+ "valid": false
+ },
+ {
+ "name": "speccheck-9",
+ "publicKey": "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
+ "message": "9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41",
+ "signature": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffca8c5b64cd208982aa38d4936621a4775aa233aa0505711d8fdcfdaa943d4908",
+ "valid": false
+ },
+ {
+ "name": "speccheck-10",
+ "publicKey": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "message": "e96b7021eb39c1a163b6da4e3093dcd3f21387da4cc4572be588fafae23c155b",
+ "signature": "a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
+ "valid": false
+ },
+ {
+ "name": "speccheck-11",
+ "publicKey": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "message": "39a591f5321bbe07fd5a23dc2f39d025d74526615746727ceefd6e82ae65c06f",
+ "signature": "a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
+ "valid": false
}
]
}
diff --git a/app/src/test/resources/sync2/roster.json b/app/src/test/resources/sync2/roster.json
index 819e13785..12d902368 100644
--- a/app/src/test/resources/sync2/roster.json
+++ b/app/src/test/resources/sync2/roster.json
@@ -1044,6 +1044,50 @@
},
"error": "roster-key"
},
+ {
+ "name": "mixed-order-key",
+ "base": 1,
+ "entry": {
+ "v": 2,
+ "group": "669a44456c08e4a12e4ceb3538860317",
+ "seq": 1,
+ "prev": "c2d714b286d2cc8b4f11911a6024e65e916302ec7a0c1ecbef9db9bdcac76143",
+ "op": "add",
+ "signer": "d8576f2de42861ceb8030cc722b4c669",
+ "add": [
+ {
+ "did": "6897b8ae78f4e31d1857d19cc710c530",
+ "sig": "jqX8f1GFi-6NFSK350izBUek5RJFkLDXyejOqc-iwTE",
+ "kem": "bmiQy52XcNEUqT_nj29df8adxbK84jlUCWdacit50FA",
+ "role": "device"
+ }
+ ],
+ "sigs": {
+ "d8576f2de42861ceb8030cc722b4c669": "cfc7J2MoV9mkD0eXBIVG07Y5h3cljavwUlhPdc20aYIkQVTXwXf_4t5RxAnqiHOhRoth5B9fAA7jsYGO1-q-AQ",
+ "6897b8ae78f4e31d1857d19cc710c530": "qZHjkrOraxl-bCPXtzYu8YoM1gBFAOjiP7_vmHEci1-Dv22zfElOC_Cpjf_ID7A_hrNJ3pLnzyZy73SYoURIDQ"
+ }
+ },
+ "error": "roster-key"
+ },
+ {
+ "name": "small-order-r",
+ "base": 1,
+ "entry": {
+ "v": 2,
+ "group": "669a44456c08e4a12e4ceb3538860317",
+ "seq": 1,
+ "prev": "c2d714b286d2cc8b4f11911a6024e65e916302ec7a0c1ecbef9db9bdcac76143",
+ "op": "rename",
+ "signer": "d8576f2de42861ceb8030cc722b4c669",
+ "meta": {
+ "d8576f2de42861ceb8030cc722b4c669": "AA"
+ },
+ "sigs": {
+ "d8576f2de42861ceb8030cc722b4c669": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAlGb6FsDbfEQIixXdF8sVRpJW52nc-MG7Xb6LA8ugCw"
+ }
+ },
+ "error": "roster-signature"
+ },
{
"name": "readd-removed-device",
"base": 4,
From 1f0c05525a6a8a870050a940e178a42cb1cdd028 Mon Sep 17 00:00:00 2001
From: AE <98699388+A-EDev@users.noreply.github.com>
Date: Mon, 9 Nov 2026 00:16:22 +0200
Subject: [PATCH 2/2] test(sync): take the vectors from the matching protocol
change
---
app/src/test/resources/sync2/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/src/test/resources/sync2/README.md b/app/src/test/resources/sync2/README.md
index 88a128d1b..d3ab65369 100644
--- a/app/src/test/resources/sync2/README.md
+++ b/app/src/test/resources/sync2/README.md
@@ -8,13 +8,13 @@ All byte strings are lowercase hex, except inside roster entries, which use the
|---|---|
| `canonical.json` | Canonical JSON serialization, including keys outside the Basic Multilingual Plane, which sort by UTF-16 code unit, not by code point |
| `derive.json` | `Derive` and `Hash` ([crypto.md](../crypto.md#labelled-derivation)) |
-| `ed25519.json` | Strict verification: valid, tampered, wrong key, non-canonical `S`, small-order key; the eight small-order encodings; a non-canonical key encoding |
+| `ed25519.json` | Strict verification: valid, tampered, wrong key, non-canonical `S`, small-order key; every small-order encoding of `R`, canonical or not; `R` and keys with a small-order part under either equation; the [ed25519-speccheck](https://github.com/novifinancial/ed25519-speccheck) set; the eight small-order encodings (`weakKeys`), non-canonical key encodings (`nonCanonicalKeys`) and keys with a small-order part (`mixedOrderKeys`), none of which may enter a roster |
| `box.json` | Box seal and open with fixed salts; tampered ciphertext, tag, commitment and salt, wrong key, wrong AAD, truncation |
| `payload.json` | PADMÉ sizes; payload decoding; bad length, non-zero padding, corrupt gzip |
| `keys.json` | Member ids, epoch commitment, purpose keys, HPKE wrap with a fixed ephemeral key, wrong-epoch and wrong-member unwraps, history chain, wrap and chain paths |
| `recovery.json` | Recovery secret, 24 words and derived keys; words with a bad checksum, 23 words, and a valid 12-word phrase |
| `envelope.json` | Log objects of each type, including a store login, opened in stream order, a live message, a snapshot blob; truncation, magic, suite, group, unknown sender, signature, tampering, replay, gap, a log read from the middle, wrong `prev`, swapped epoch, `seq` above 253 − 1, blob hash |
-| `roster.json` | A chain through genesis, add, rename, remove, restore, recovery change, add, leave and rotate, with the state after each entry (including whether a rotate is due, and each member's epochs and `cut`) and the device names it carries; bad and missing signatures, unknown fields, wrong `seq`, truncated chain, removed signer, weak key, re-adding a removed device, wrong epoch sets and numbers, self-removal, renaming the recovery key, a non-member signer, a used recovery key; a leave by a non-member, by the recovery member and by the last device, a leave removing another device or carrying an epoch, a rotate without an epoch, with a removal, or wrapped to a device that left; six forks and their winners, including leave against remove and rotate, and concurrent rotates; a genesis whose group id isn't the one its keys commit to; a `cut` on the wrong op, for a device not removed, empty, or at `seq` 0; `replacements`, entries on top of the chain where the device that set the recovery key is removed or has left |
+| `roster.json` | A chain through genesis, add, rename, remove, restore, recovery change, add, leave and rotate, with the state after each entry (including whether a rotate is due, and each member's epochs and `cut`) and the device names it carries; bad and missing signatures, unknown fields, wrong `seq`, truncated chain, removed signer, weak key, a key with a small-order part, an identity `R`, re-adding a removed device, wrong epoch sets and numbers, self-removal, renaming the recovery key, a non-member signer, a used recovery key; a leave by a non-member, by the recovery member and by the last device, a leave removing another device or carrying an epoch, a rotate without an epoch, with a removal, or wrapped to a device that left; six forks and their winners, including leave against remove and rotate, and concurrent rotates; a genesis whose group id isn't the one its keys commit to; a `cut` on the wrong op, for a device not removed, empty, or at `seq` 0; `replacements`, entries on top of the chain where the device that set the recovery key is removed or has left |
| `forks.json` | Named entries, and cases that settle a store's view of them: `applied` is what the device had applied, `view` is the store's answer, `pins` the chosen entries, `linear` a Flow server. Each case gives the outcome and, in any order of `view`, the same chain, standing entries that lost and start an epoch, contested set and sides |
| `senders.json` | Step 2 of [reading an envelope](../store.md#reading-an-envelope): a roster, then `senders` with who may write under which epoch and how far along its log, then `walks` of a log down from a `cut`, with the hashes found from the lowest `seq` up or the check that refuses it. An empty string in `log` is an object the store doesn't have |
| `pairing.json` | Commitment, proof, the scanner's first message, pairing keys, SAS, mailbox slot, and a sealed `join` message; `refused` holds a shower key that doesn't match the commitment and a proof made with another token |