From f4454c96bbe492856958eac042f12db076a2d903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 9 Oct 2024 10:39:01 +0200 Subject: [PATCH] Fix/oid (#158) * add BigInt ASN.1 VarInt encoding * make OIDs work with BigIntegers * directly support UUID-based OID creation * thorough OID testing --- CHANGELOG.md | 2 + docs/docs/indispensable.md | 19 + .../indispensable/asn1/ObjectIdentifier.kt | 174 ++++++--- .../asn1/encoding/NumberEncoding.kt | 87 +++++ .../asitplus/signum/indispensable/OidTest.kt | 368 +++++++++++++++++- .../signum/indispensable/UVarIntTest.kt | 27 ++ 6 files changed, 615 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dacaafe0..9f60ef6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ * Introduce shorthand to create certificate from TbsCertificate * Remove requirement from CSR to have certificate extensions * Fix CoseSigned equals +* Base OIDs on BigInteger instead of UInt +* Directly support UUID-based OID creation ### 3.9.0 (Supreme 0.4.0) diff --git a/docs/docs/indispensable.md b/docs/docs/indispensable.md index 50aa3ef2..5181e7e3 100644 --- a/docs/docs/indispensable.md +++ b/docs/docs/indispensable.md @@ -342,6 +342,25 @@ Asn1.Sequence { +Asn1.Int(42) } withImplicitTag (0x5EUL without CONSTRUCTED) Just blame the mess you created only on yourself and nobody else! +### Object Identifiers +Signum's _Indispensable_ module comes with an expressive, convenient, and efficient ASN.1 `ObjectIdentifier` class. +It can be constructed by either parsing a `ByteArray` containing ASN.1-encoded representation of an OID, +or constructing it from a humanly-readable string representation (`"1.2.96"`, `"1 2 96"`). +In addition, it is possible to pass OID node components as either `UInt` or `BigInteger` to construct an OID: `ObjectIdentifier(1u, 3u, 6u, 1u)`. + +The OID class exposes a `nodes` property, corresponding to the individual components that make up an OID node for convenience, +as well as a `bytes` property, corresponding to its ASN.1-encoded `ByteArray` representation. +One peculiar characteristic of the `ObjectIdentifier` class is that both `nodes` and `bytes` properties are lazily evaluated. +This means that if the OID was constructed from raw bytes, accessing `bytes` is a NOOP, but operating on `nodes` is initially +quite expensive, since the bytes have yet to be parsed. +Conversely, if an OID was constructed from `BigInteger` components, accessing `bytes` is slow. +If, however, an OID was constructed from `UInt` components, those are eagerly encoded into bytes and the `nodes` property +is not immediately initialized. + +This behaviour boils down to performance: Only very rarely, will you want to create an OID with components exceeding `UInt.MAX_VALUE`, +but you will almost certainly want to encode a OID you created to ASN.1. +On the other hand, parsing an OID from ASN.1-encoded bytes and re-encoding it are both close to a NOOP (object creation aside). + ### ASN.1 Builder DSL So far, custom high-level types and manually constructing low-level types was discussed. When actually constructing ASN.1 structures, a far more streamlined and intuitive approach exists. diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/ObjectIdentifier.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/ObjectIdentifier.kt index 8faae1b8..95d971d7 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/ObjectIdentifier.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/ObjectIdentifier.kt @@ -1,15 +1,20 @@ package at.asitplus.signum.indispensable.asn1 import at.asitplus.signum.indispensable.asn1.encoding.decode -import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarUInt +import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarBigInt import at.asitplus.signum.indispensable.asn1.encoding.toAsn1VarInt +import at.asitplus.signum.indispensable.asn1.encoding.toBigInteger +import com.ionspin.kotlin.bignum.integer.BigInteger import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +private val BIGINT_40 = BigInteger.fromUByte(40u) /** * ASN.1 OBJECT IDENTIFIER featuring the most cursed encoding of numbers known to man, which probably surfaced due to an ungodly combination @@ -19,48 +24,125 @@ import kotlinx.serialization.encoding.Encoder * @throws Asn1Exception if less than two nodes are supplied, the first node is >2 or the second node is >39 */ @Serializable(with = ObjectIdSerializer::class) -class ObjectIdentifier @Throws(Asn1Exception::class) constructor(@Transient vararg val nodes: UInt) : +class ObjectIdentifier @Throws(Asn1Exception::class) private constructor( + bytes: ByteArray?, + nodes: List? +) : Asn1Encodable { - init { - if (nodes.size < 2) throw Asn1StructuralException("at least two nodes required!") - if (nodes[0] * 40u > UByte.MAX_VALUE.toUInt()) throw Asn1Exception("first node too lage!") - //TODO more sanity checks + if ((bytes == null) && (nodes == null)) { + //we're not even declaring this, since this is an implementation error on our end + throw IllegalArgumentException("either nodes or bytes required") + } + if (bytes?.isEmpty() == true || nodes?.isEmpty() == true) + throw Asn1Exception("Empty OIDs are not supported") + + bytes?.apply { + if(first().toUByte()>127u) throw Asn1Exception("OID top-level arc can only be number 0, 1 or 2") + } + nodes?.apply { + if (size < 2) throw Asn1StructuralException("at least two nodes required!") + if (first() > 2u) throw Asn1Exception("OID top-level arc can only be number 0, 1 or 2") + if(first()<2u) { + if (get(1) > 39u) throw Asn1Exception("Second segment must be <40") + }else { + if (get(1) > 47u) throw Asn1Exception("Second segment must be <48") + } + forEach { if (it.isNegative) throw Asn1Exception("Negative Number encountered: $it") } + } + } + + + /** + * Efficient, but cursed encoding of OID nodes, see [Microsoft's KB entry on OIDs](https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier) + * for details. + * Lazily evaluated. + */ + val bytes: ByteArray by if (bytes != null) lazyOf(bytes) else lazy { + this.nodes.toOidBytes() + } - if (nodes.first() > 2u) throw Asn1Exception("OID must start with either 1 or 2") - if (nodes[1] > 39u) throw Asn1Exception("Second segment must be <40") + /** + * Lazily evaluated list of OID nodes (e.g. `[1, 2, 35, 4654]`) + */ + val nodes by if (nodes != null) lazyOf(nodes) else lazy { + val (first, second) = + if (this.bytes[0] >= 80) { + BigInteger.fromUByte(2u) to BigInteger.fromUInt(this.bytes[0].toUByte() - 80u) + } else { + BigInteger.fromUInt(this.bytes[0].toUByte() / 40u) to BigInteger.fromUInt(this.bytes[0].toUByte() % 40u) + } + var index = 1 + val collected = mutableListOf(first, second) + while (index < this.bytes.size) { + if (this.bytes[index] >= 0) { + collected += BigInteger.fromUInt(this.bytes[index].toUInt()) + index++ + } else { + val currentNode = mutableListOf() + while (this.bytes[index] < 0) { + currentNode += this.bytes[index] //+= parsed + index++ + } + currentNode += this.bytes[index] + index++ + collected += currentNode.decodeAsn1VarBigInt().first + } + } + collected } /** - * @param oid in human-readable format (e.g. "1.2.96") + * Creates an OID in the 2.25 subtree that requires no formal registration. + * E.g. the UUID `550e8400-e29b-41d4-a716-446655440000` results in the OID + * `2.25.113059749145936325402354257176981405696` */ - constructor(oid: String) : this(*(oid.split(if (oid.contains('.')) '.' else ' ')).map { it.toUInt() }.toUIntArray()) + @OptIn(ExperimentalUuidApi::class) + constructor(uuid: Uuid) : this( + bytes = byteArrayOf((2 * 40 + 25).toUByte().toByte(), *uuid.toBigInteger().toAsn1VarInt()), + nodes = null + ) + + /** + * @param nodes OID Tree nodes passed in order (e.g. 1u, 2u, 96u, …) + * @throws Asn1Exception if less than two nodes are supplied, the first node is >2 or the second node is >39 + */ + constructor(vararg nodes: UInt) : this( + bytes = nodes.toOidBytes(), + nodes = null + ) + + /** + * @param nodes OID Tree nodes passed in order (e.g. 1, 2, 96, …) + * @throws Asn1Exception if less than two nodes are supplied, the first node is >2, the second node is >39 or any node is negative + */ + constructor(vararg nodes: BigInteger) : this( + bytes = null, + nodes = nodes.asList() + ) + + /** + * @param oid OID string in human-readable format (e.g. "1.2.96" or "1 2 96") + */ + constructor(oid: String) : this(*(oid.split(if (oid.contains('.')) '.' else ' ')).map { BigInteger.parseString(it) } + .toTypedArray()) + /** * @return human-readable format (e.g. "1.2.96") */ - override fun toString() = nodes.joinToString(separator = ".") { it.toString() } + override fun toString(): String { + return nodes.joinToString(".") + } override fun equals(other: Any?): Boolean { if (other == null) return false if (other !is ObjectIdentifier) return false - return nodes contentEquals other.nodes + return bytes contentEquals other.bytes } override fun hashCode(): Int { - return nodes.hashCode() - } - - - /** - * Cursed encoding of OID nodes. A sacrifice of pristine numbers requested by past gods of the netherrealm - */ - val bytes: ByteArray by lazy { - nodes.slice(2.. acc + bytes } + return bytes.contentHashCode() } /** @@ -88,33 +170,19 @@ class ObjectIdentifier @Throws(Asn1Exception::class) constructor(@Transient vara * @throws Asn1Exception all sorts of errors on invalid input */ @Throws(Asn1Exception::class) - fun parse(rawValue: ByteArray): ObjectIdentifier = runRethrowing { - if (rawValue.isEmpty()) throw Asn1Exception("Empty OIDs are not supported") - val (first, second) = - if (rawValue[0] >= 80) { - 2u to rawValue[0].toUByte() - 80u - } else { - rawValue[0].toUByte() / 40u to rawValue[0].toUByte() % 40u - } + fun parse(rawValue: ByteArray): ObjectIdentifier = ObjectIdentifier(bytes = rawValue, nodes = null) - var index = 1 - val collected = mutableListOf(first, second) - while (index < rawValue.size) { - if (rawValue[index] >= 0) { - collected += rawValue[index].toUInt() - index++ - } else { - val currentNode = mutableListOf() - while (rawValue[index] < 0) { - currentNode += rawValue[index] //+= parsed - index++ - } - currentNode += rawValue[index] - index++ - collected += currentNode.decodeAsn1VarUInt().first - } - } - return ObjectIdentifier(*collected.toUIntArray()) + private fun UIntArray.toOidBytes(): ByteArray { + return slice(2.. acc + bytes } + } + + private fun List.toOidBytes(): ByteArray { + return slice(2.. acc + bytes } } } } @@ -125,7 +193,7 @@ object ObjectIdSerializer : KSerializer { override fun deserialize(decoder: Decoder): ObjectIdentifier = ObjectIdentifier(decoder.decodeString()) override fun serialize(encoder: Encoder, value: ObjectIdentifier) { - encoder.encodeString(value.nodes.joinToString(separator = ".") { it.toString() }) + encoder.encodeString(toString()) } } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt index e0565b2c..c1a1f638 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt @@ -1,8 +1,14 @@ package at.asitplus.signum.indispensable.asn1.encoding +import at.asitplus.catching import at.asitplus.signum.indispensable.asn1.ObjectIdentifier +import at.asitplus.signum.indispensable.io.ensureSize +import com.ionspin.kotlin.bignum.integer.BigInteger +import com.ionspin.kotlin.bignum.integer.Sign import kotlin.experimental.or import kotlin.math.ceil +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid /** @@ -247,6 +253,30 @@ fun ULong.toAsn1VarInt(): ByteArray { if (offset > (ULong.SIZE_BITS - 1)) break //End of Fahnenstange b0 = (this shr offset and 0x7FuL).toByte() } + return with(result) { + ByteArray(size) { fromBack(it) or asn1VarIntByteMask(it) } + } +} + +/** + * Encodes this number using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come + */ +fun BigInteger.toAsn1VarInt(): ByteArray { + if (isZero()) return byteArrayOf(0) + require(isPositive) { "Only positive Numbers are supported" } + if (this < 128) return byteArrayOf(this.byteValue(exactRequired = true)) //Fast case + var offset = 0 + var result = mutableListOf() + + val mask = BigInteger.fromUByte(0x7Fu) + var b0 = ((this shr offset) and mask).byteValue(exactRequired = false) + while ((this shr offset > 0uL) || offset == 0) { + result += b0 + offset += 7 + if (offset > (this.bitLength() - 1)) break //End of Fahnenstange + b0 = ((this shr offset) and mask).byteValue(exactRequired = false) + } return with(result) { ByteArray(size) { fromBack(it) or asn1VarIntByteMask(it) } @@ -329,6 +359,46 @@ fun Iterator.decodeAsn1VarULong(): Pair { } +/** + * Decodes an unsigned BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded unsigned BigInteger and the underlying varint-encoded bytes as `ByteArray` + */ +inline fun Iterable.decodeAsn1VarBigInt(): Pair = iterator().decodeAsn1VarBigInt() + +/** + * Decodes an unsigned BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded unsigned BigInteger and the underlying varint-encoded bytes as `ByteArray` + */ +inline fun ByteArray.decodeAsn1VarBigInt(): Pair = iterator().decodeAsn1VarBigInt() + + +/** + * Decodes an BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded BigInteger and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + */ +fun Iterator.decodeAsn1VarBigInt(): Pair { + var result = BigInteger.ZERO + val mask = BigInteger.fromUByte(0x7Fu) + val accumulator = mutableListOf() + while (hasNext()) { + val curByte = next() + val current = BigInteger(curByte.toUByte().toInt()) + accumulator += curByte + result = (current and mask) or (result shl 7) + if (current < 0x80.toUByte()) break + } + + return result to accumulator.toByteArray() +} + + //TOOD: how to not duplicate all this??? /** * Decodes an UInt from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, @@ -373,3 +443,20 @@ fun Iterator.decodeAsn1VarUInt(): Pair { return result to accumulator.toByteArray() } + +/** + * Converts this UUID to a BigInteger representation + */ +@OptIn(ExperimentalUuidApi::class) +fun Uuid.toBigInteger(): BigInteger = BigInteger.fromByteArray(toByteArray(), Sign.POSITIVE) + +/** + * Tries to convert a BigInteger to a UUID. Only guaranteed to work with BigIntegers that contain the unsigned (positive) + * integer representation of a UUID, chances are high, though, that it works with random positive BigIntegers between + * 16 and 14 bytes large. + * + * Returns `null` if conversion fails. Never throws. + */ +@OptIn(ExperimentalUuidApi::class) +fun Uuid.Companion.fromBigintOrNull(bigInteger: BigInteger): Uuid? = + catching { fromByteArray(bigInteger.toByteArray().ensureSize(16)) }.getOrNull() diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/OidTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/OidTest.kt index 83f9e258..ceb2c1ea 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/OidTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/OidTest.kt @@ -1,19 +1,27 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.asn1.ObjectIdentifier +import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.* +import com.ionspin.kotlin.bignum.integer.BigInteger +import com.ionspin.kotlin.bignum.integer.Sign +import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue import io.kotest.core.spec.style.FreeSpec +import io.kotest.datatest.withData +import io.kotest.matchers.comparables.shouldBeLessThan import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.property.Arb -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.intArray -import io.kotest.property.arbitrary.positiveInt +import io.kotest.property.arbitrary.* import io.kotest.property.checkAll -import org.bouncycastle.asn1.ASN1Integer +import kotlinx.datetime.Clock import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.DERTaggedObject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid +@OptIn(ExperimentalUuidApi::class, ExperimentalStdlibApi::class) class OidTest : FreeSpec({ "OID test" - { @@ -29,10 +37,87 @@ class OidTest : FreeSpec({ oid.hashCode() shouldNotBe oid2.hashCode() } - "Automated" - { + "Full Root Arc" - { + withData(nameFn = { "Byte $it" }, List(127) { it }) { + val oid = ObjectIdentifier.parse(byteArrayOf(it.toUByte().toByte())) + val fromBC = ASN1ObjectIdentifier.fromContents(byteArrayOf(it.toByte())) + oid.encodeToDer() shouldBe fromBC.encoded + ObjectIdentifier(oid.toString()).let { + it shouldBe oid + it.encodeToDer() shouldBe fromBC.encoded + } + ObjectIdentifier(*(oid.toString().split(".").map { it.toUInt() }.toUIntArray())).let { + it shouldBe oid + it.encodeToDer() shouldBe fromBC.encoded + } + ObjectIdentifier(*(oid.toString().split(".").map { BigInteger.parseString(it) }.toTypedArray())).let { + it shouldBe oid + it.encodeToDer() shouldBe fromBC.encoded + } + } + + val stringRepesentations = mutableListOf() + repeat(39) { stringRepesentations += "0.$it" } + repeat(39) { stringRepesentations += "1.$it" } + repeat(47) { stringRepesentations += "2.$it" } + withData(nameFn = { "String $it" }, stringRepesentations) { + val oid = ObjectIdentifier(it) + val fromBC = ASN1ObjectIdentifier(it) + oid.encodeToDer() shouldBe fromBC.encoded + ObjectIdentifier(oid.toString()).let { + it shouldBe oid + it.encodeToDer() shouldBe fromBC.encoded + } + ObjectIdentifier(*(oid.toString().split(".").map { it.toUInt() }.toUIntArray())).let { + it shouldBe oid + it.encodeToDer() shouldBe fromBC.encoded + } + ObjectIdentifier(*(oid.toString().split(".").map { BigInteger.parseString(it) }.toTypedArray())).let { + it shouldBe oid + it.encodeToDer() shouldBe fromBC.encoded + } + } + + } + "Failing Root Arc" - { + withData(nameFn = { "Byte $it" }, List(128) { it + 128 }) { + shouldThrow { + ObjectIdentifier.parse(byteArrayOf(it.toUByte().toByte())) + } + } + val stringRepesentations = mutableListOf() + + repeat(255-40) { stringRepesentations += "0.${it + 40}" } + repeat(255-40) { stringRepesentations += "1.${it + 40}" } + repeat(255-48) { stringRepesentations += "2.${it + 48}" } + repeat(255-3) { stringRepesentations += "${3 + it}.${it % 40}" } + + withData(nameFn = { "String $it" }, stringRepesentations) { + shouldThrow { + ObjectIdentifier(it) + } + } + + } + + "Failing negative Bigints" - { + checkAll(iterations = 50, Arb.negativeInt()) { negativeInt -> + checkAll(iterations = 15, Arb.positiveInt(39)) { second -> + checkAll(iterations = 100, Arb.intArray(Arb.int(0..128), Arb.positiveInt(Int.MAX_VALUE))) { rest -> + listOf(0, 1, 2).forEach { first -> + val withNegative = intArrayOf(negativeInt, *rest).apply { shuffle() }.map { BigInteger(it) }.toTypedArray() + shouldThrow { + ObjectIdentifier(BigInteger(first), BigInteger(second), *withNegative) + } + } + } + } + } + } + "Automated UInt Capped" - { checkAll(iterations = 15, Arb.positiveInt(39)) { second -> checkAll(iterations = 5000, Arb.intArray(Arb.int(0..128), Arb.positiveInt(Int.MAX_VALUE))) { - listOf(1, 2).forEach { first -> + listOf(0, 1, 2).forEach { first -> val oid = ObjectIdentifier( first.toUInt(), second.toUInt(), @@ -42,6 +127,9 @@ class OidTest : FreeSpec({ val stringRepresentation = "$first.$second" + if (it.isEmpty()) "" else ("." + it.joinToString(".")) + + oid.toString() shouldBe stringRepresentation + val second1 = if (second > 1) second - 1 else second + 1 val oid1 = ObjectIdentifier( @@ -73,5 +161,267 @@ class OidTest : FreeSpec({ } } } + + "Benchmarking fast case" - { + val optimized = mutableListOf() + val repetitions= 10 + + "Optimized" - { + repeat(repetitions) { + val before = Clock.System.now() + checkAll(iterations = 15, Arb.uInt(max = 39u)) { second -> + checkAll(iterations = 5000, Arb.uIntArray(Arb.int(0..256), Arb.uInt(UInt.MAX_VALUE))) { + listOf(1u, 2u).forEach { first -> + val oid = ObjectIdentifier(first, second, *it.toUIntArray()) + ObjectIdentifier.decodeFromTlv(oid.encodeToTlv()) + } + } + } + val duration = Clock.System.now() - before + optimized += duration + println("Optimized: $duration") + } + } + + val avgOpt = (optimized.sorted().subList(1, optimized.size - 1) + .sumOf { it.inWholeMilliseconds } / optimized.size - 2).milliseconds + println("AvgOpt: $avgOpt") + val simple = mutableListOf() + "Simple" - { + repeat(repetitions) { + val before = Clock.System.now() + checkAll(iterations = 15, Arb.uInt(max = 39u)) { second -> + checkAll(iterations = 5000, Arb.uIntArray(Arb.int(0..256), Arb.uInt(UInt.MAX_VALUE))) { + listOf(1u, 2u).forEach { first -> + val oid = OldOIDObjectIdentifier(first, second, *it.toUIntArray()) + OldOIDObjectIdentifier.decodeFromTlv(oid.encodeToTlv()) + } + } + } + val duration = Clock.System.now() - before + simple += duration + println("Simple $duration") + } + } + + val avgSimple = (simple.sorted().subList(1, simple.size - 1) + .sumOf { it.inWholeMilliseconds } / simple.size - 2).milliseconds + println("AvgSimple: $avgSimple") + + avgOpt shouldBeLessThan avgSimple + + } + + "Automated BigInt" - { + checkAll(iterations = 15, Arb.positiveInt(39)) { second -> + checkAll(iterations = 500, Arb.bigInt(1, 358)) { + listOf(1, 2).forEach { first -> + val third = BigInteger.fromByteArray(it.toByteArray(), Sign.POSITIVE) + val oid = ObjectIdentifier( + BigInteger.fromUInt(first.toUInt()), + BigInteger.fromUInt(second.toUInt()), + third + ) + + val stringRepresentation = + "$first.$second.$third" + + oid.toString() shouldBe stringRepresentation + + val second1 = if (second > 1) second - 1 else second + 1 + + val oid1 = ObjectIdentifier( + BigInteger.fromUInt(first.toUInt()), + BigInteger.fromUInt(second1.toUInt()), + ) + val parsed = ObjectIdentifier.decodeFromTlv(oid.encodeToTlv()) + val fromBC = ASN1ObjectIdentifier(stringRepresentation) + + val bcEncoded = fromBC.encoded + val ownEncoded = oid.encodeToDer() + + @OptIn(ExperimentalStdlibApi::class) + withClue( + "Expected: ${bcEncoded.toHexString(HexFormat.UpperCase)}\nActual: ${ + ownEncoded.toHexString( + HexFormat.UpperCase + ) + }" + ) { + bcEncoded shouldBe ownEncoded + } + parsed shouldBe oid + parsed.hashCode() shouldBe oid.hashCode() + parsed shouldNotBe oid1 + parsed.hashCode() shouldNotBe oid1.hashCode() + } + } + } + } + + "UUID" - { + "550e8400-e29b-41d4-a716-446655440000" { + val uuid = Uuid.parse("550e8400-e29b-41d4-a716-446655440000") + val bigint = uuid.toBigInteger() + bigint.toString() shouldBe "113059749145936325402354257176981405696" + Uuid.fromBigintOrNull(bigint) shouldBe uuid + } + + withData(nameFn = { it.toString() }, List(1000) { Uuid.random() }) { + val bigint = it.toBigInteger() + bigint shouldBe BigInteger.parseString(it.toHexString(), 16) + Uuid.fromBigintOrNull(bigint) shouldBe it + + val oid = ObjectIdentifier(it) + oid.nodes.size shouldBe 3 + oid.nodes.first() shouldBe BigInteger(2) + oid.nodes[1] shouldBe BigInteger(25) + oid.nodes.last() shouldBe bigint + + oid.toString() shouldBe "2.25.$bigint" + } + } } -}) \ No newline at end of file +}) + + +// old implementation for benchmarking +private val BIGINT_40 = BigInteger.fromUByte(40u) + +class OldOIDObjectIdentifier @Throws(Asn1Exception::class) constructor(@Transient vararg val nodes: BigInteger) : + Asn1Encodable { + + init { + if (nodes.size < 2) throw Asn1StructuralException("at least two nodes required!") + if ((nodes[0] * BIGINT_40) > UByte.MAX_VALUE.toUInt()) throw Asn1Exception("first node too lage!") + //TODO more sanity checks + + if (nodes.first() > 2u) throw Asn1Exception("OID must start with either 1 or 2") + if (nodes[1] > 39u) throw Asn1Exception("Second segment must be <40") + } + + /** + * Creates an OID in the 2.25 subtree that requires no formal registration. + * E.g. the UUID `550e8400-e29b-41d4-a716-446655440000` results in the OID + * `2.25.113059749145936325402354257176981405696` + */ + @OptIn(ExperimentalUuidApi::class) + constructor(uuid: Uuid) : this( + BigInteger.fromByte(2), + BigInteger.fromByte(25), + uuid.toBigInteger() + ) + + /** + * @param nodes OID Tree nodes passed in order (e.g. 1u, 2u, 96u, …) + * @throws Asn1Exception if less than two nodes are supplied, the first node is >2 or the second node is >39 + */ + constructor(vararg ints: UInt) : this(*(ints.map { BigInteger.fromUInt(it) }.toTypedArray())) + + + /** + * @param oid in human-readable format (e.g. "1.2.96") + */ + constructor(oid: String) : this(*(oid.split(if (oid.contains('.')) '.' else ' ')).map { BigInteger.parseString(it) } + .toTypedArray()) + + /** + * @return human-readable format (e.g. "1.2.96") + */ + override fun toString() = nodes.joinToString(separator = ".") { it.toString() } + + override fun equals(other: Any?): Boolean { + if (other == null) return false + if (other !is OldOIDObjectIdentifier) return false + return bytes contentEquals other.bytes + } + + override fun hashCode(): Int { + return bytes.contentHashCode() + } + + + /** + * Cursed encoding of OID nodes. A sacrifice of pristine numbers requested by past gods of the netherrealm + */ + val bytes: ByteArray by lazy { + nodes.slice(2.. acc + bytes } + } + + /** + * @return an OBJECT IDENTIFIER [Asn1Primitive] + */ + override fun encodeToTlv() = Asn1Primitive(Asn1Element.Tag.OID, bytes) + + companion object : Asn1Decodable { + + /** + * Parses an OBJECT IDENTIFIER contained in [src] to an [ObjectIdentifier] + * @throws Asn1Exception all sorts of errors on invalid input + */ + @Throws(Asn1Exception::class) + override fun doDecode(src: Asn1Primitive): ObjectIdentifier { + if (src.length < 1) throw Asn1StructuralException("Empty OIDs are not supported") + + return parse(src.content) + + } + + /** + * Casts out the evil demons that haunt OID components encoded into [rawValue] + * @return ObjectIdentifier if decoding succeeded + * @throws Asn1Exception all sorts of errors on invalid input + */ + @Throws(Asn1Exception::class) + fun parse(rawValue: ByteArray): ObjectIdentifier = runRethrowing { + if (rawValue.isEmpty()) throw Asn1Exception("Empty OIDs are not supported") + val (first, second) = + if (rawValue[0] >= 80) { + BigInteger.fromUByte(2u) to BigInteger.fromUInt(rawValue[0].toUByte() - 80u) + } else { + BigInteger.fromUInt(rawValue[0].toUByte() / 40u) to BigInteger.fromUInt(rawValue[0].toUByte() % 40u) + } + + var index = 1 + val collected = mutableListOf(first, second) + while (index < rawValue.size) { + if (rawValue[index] >= 0) { + collected += BigInteger.fromUInt(rawValue[index].toUInt()) + index++ + } else { + val currentNode = mutableListOf() + while (rawValue[index] < 0) { + currentNode += rawValue[index] //+= parsed + index++ + } + currentNode += rawValue[index] + index++ + collected += currentNode.decodeAsn1VarBigInt().first + } + } + return ObjectIdentifier(*collected.toTypedArray()) + } + } +} + + +/** + * Adds [oid] to the implementing class + */ +interface Identifiable { + val oid: ObjectIdentifier +} + +/** + * decodes this [Asn1Primitive]'s content into an [ObjectIdentifier] + * + * @throws Asn1Exception on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.readOid() = runRethrowing { + decode(Asn1Element.Tag.OID) { OldOIDObjectIdentifier.parse(it) } +} \ No newline at end of file diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UVarIntTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UVarIntTest.kt index a191e4c2..ff25b2d0 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UVarIntTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UVarIntTest.kt @@ -1,11 +1,15 @@ package at.asitplus.signum.indispensable +import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarBigInt import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarUInt import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarULong import at.asitplus.signum.indispensable.asn1.encoding.toAsn1VarInt +import com.ionspin.kotlin.bignum.integer.BigInteger +import com.ionspin.kotlin.bignum.integer.Sign import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe import io.kotest.property.Arb +import io.kotest.property.arbitrary.bigInt import io.kotest.property.arbitrary.uInt import io.kotest.property.arbitrary.uLong import io.kotest.property.checkAll @@ -37,4 +41,27 @@ class UVarIntTest : FreeSpec({ } } + "BigInts" - { + "long-capped" - { + checkAll(Arb.uLong()) { long -> + val uLongVarInt = long.toAsn1VarInt() + val bigInteger = BigInteger.fromULong(long) + val bigIntVarInt = bigInteger.toAsn1VarInt() + + bigIntVarInt shouldBe uLongVarInt + + (uLongVarInt.asList() + Random.nextBytes(8).asList()).decodeAsn1VarBigInt().first shouldBe bigInteger + + } + } + + "larger" - { + checkAll(Arb.bigInt(1, 1024 * 32)) { javaBigInt -> + val bigInt = BigInteger.fromByteArray(javaBigInt.toByteArray(), Sign.POSITIVE) + (bigInt.toAsn1VarInt().asList() + Random.nextBytes(33) + .asList()).decodeAsn1VarBigInt().first shouldBe bigInt + } + } + } + }) \ No newline at end of file