Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 4 additions & 28 deletions app/src/main/java/io/github/aedev/flow/sync2/crypto/Curves.kt
Original file line number Diff line number Diff line change
@@ -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) }

Expand All @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/test/resources/sync2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 2<sup>53</sup> − 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 |
Expand Down
Loading
Loading