Prepare 0.9.6-SNAPSHOT with DH input validation and distinct signer checks

This commit is contained in:
sergeych committed 2026-10-21 23:17:13 +04:00
1 parent 66254f8fa6
commit 70d7a13fc3
7 files changed
+175 -7

No files matched your search

@@ -0,0 +1,57 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.bipack.BipackDecoder
import net.sergeych.crypto2.*
import kotlin.test.*
class DuplicateSignerTest {
@Test fun thresholdsCountDistinctAuthorizedKeys() = runTest {
initCrypto()
val keys = List(4) { SigningSecretKey.new().verifyingKey }
val outsider = SigningSecretKey.new().verifyingKey
// Exhaust all subsets of authorized keys, thresholds, and repeated inputs.
for (threshold in 1..keys.size) {
val policy = Multikey.someOf(threshold, keys)
for (mask in 0 until (1 shl keys.size)) {
val selected = keys.filterIndexed { index, _ -> mask and (1 shl index) != 0 }
for (copies in 1..5) {
val signers = selected.flatMap { key -> List(copies) { key } } + List(5) { outsider }
assertEquals(selected.size >= threshold, policy.check(signers))
assertEquals(selected.size >= threshold, policy.check(signers.reversed()))
}
}
}
assertFalse(Multikey.allOf(keys).check(List(10) { keys.first() }))
assertTrue(Multikey.allOf(keys).check(keys + keys))
}
@Test fun nestedPoliciesKeepBooleanSemantics() = runTest {
initCrypto()
val a = SigningSecretKey.new().verifyingKey
val b = SigningSecretKey.new().verifyingKey
val c = SigningSecretKey.new().verifyingKey
val d = SigningSecretKey.new().verifyingKey
val threshold = Multikey.someOf(2, a, b)
val policy = (threshold and c) or d
assertFalse(policy.check(a, a, c))
assertTrue(policy.check(a, a, b, c))
assertTrue(policy.check(d))
// Overlapping branches remain logical expressions, not globally consumed signatures.
assertTrue(((a or b) and (a or c)).check(a))
}
@Test fun repeatedValidSealCannotSatisfyThresholdAfterDecoding() = runTest {
initCrypto()
val a = SigningSecretKey.new()
val b = SigningSecretKey.new()
val policy = Multikey.someOf(2, a.verifyingKey, b.verifyingKey)
val original = SealedBox.create("threshold regression".encodeToByteArray().toUByteArray(), a)
val repeated = SealedBox(original.message, List(3) { original.seals.single() })
val decoded = BipackDecoder.decode<SealedBox>(repeated.encoded.asByteArray())
assertEquals(3, decoded.seals.size)
assertEquals(1, decoded.signedByKeys.toSet().size)
assertFalse(decoded.isSealedBy(policy))
assertTrue(decoded.isSealedBy(Multikey(a)))
val authorized = BipackDecoder.decode<SealedBox>((original + b).encoded.asByteArray())
assertTrue(authorized.isSealedBy(policy))
}
}
@@ -0,0 +1,60 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto2.SafeKeyExchange
import net.sergeych.crypto2.initCrypto
import kotlin.test.*
class KeyExchangeValidationTest {
@Test
fun rejectsLowOrderPeerKeysOnBothSides() = runTest {
initCrypto()
// u = 0, 1, p-1, p, p+1, including aliases with the ignored high bit set.
val points = listOf(
UByteArray(32),
UByteArray(32).also { it[0] = 1u },
fieldEncoding(0xecu),
fieldEncoding(0xedu),
fieldEncoding(0xeeu),
)
for ((index, point) in points.withIndex()) {
for (highBit in listOf(false, true)) {
val bytes = point.copyOf()
if (highBit) bytes[31] = (bytes[31].toInt() or 0x80).toUByte()
val peer = SafeKeyExchange.PublicKey(bytes)
assertFails("client accepted low-order point $index, highBit=$highBit") {
SafeKeyExchange().clientSessionKey(peer)
}
assertFails("server accepted low-order point $index, highBit=$highBit") {
SafeKeyExchange().serverSessionKey(peer)
}
}
}
}
@Test
fun rejectsWrongLengthBeforeCallingNativeCode() = runTest {
initCrypto()
val valid = SafeKeyExchange().publicKey.keyBytes
for (size in listOf(0, 1, 31, 33, 64)) {
val peer = SafeKeyExchange.PublicKey(valid.copyOf(size))
assertFailsWith<IllegalArgumentException> { SafeKeyExchange().clientSessionKey(peer) }
assertFailsWith<IllegalArgumentException> { SafeKeyExchange().serverSessionKey(peer) }
}
}
@Test
fun validPeersStillAgreeInBothDirections() = runTest {
initCrypto()
val client = SafeKeyExchange()
val server = SafeKeyExchange()
val c = client.clientSessionKey(server.publicKey)
val s = server.serverSessionKey(client.publicKey)
assertContentEquals(c.sessionTag, s.sessionTag)
assertEquals("client", s.decryptString(c.encrypt("client")))
assertEquals("server", c.decryptString(s.encrypt("server")))
}
private fun fieldEncoding(first: UByte) = UByteArray(32) { 0xffu }.also {
it[0] = first
it[31] = 0x7fu
}
}