Skip to content

Commit

Permalink
Fix/oid (#158)
Browse files Browse the repository at this point in the history
* add BigInt ASN.1 VarInt encoding
* make OIDs work with BigIntegers
* directly support UUID-based OID creation
* thorough OID testing
  • Loading branch information
JesusMcCloud authored Oct 9, 2024
1 parent c874b5a commit f4454c9
Show file tree
Hide file tree
Showing 6 changed files with 615 additions and 62 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
19 changes: 19 additions & 0 deletions docs/docs/indispensable.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<BigInteger>?
) :
Asn1Encodable<Asn1Primitive> {

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<Byte>()
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..<nodes.size).map { it.toAsn1VarInt() }.fold(
byteArrayOf(
(nodes[0] * 40u + nodes[1]).toUByte().toByte()
)
) { acc, bytes -> acc + bytes }
return bytes.contentHashCode()
}

/**
Expand Down Expand Up @@ -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<Byte>()
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..<size).map { it.toAsn1VarInt() }.fold(
byteArrayOf((first() * 40u + get(1)).toUByte().toByte())
) { acc, bytes -> acc + bytes }
}

private fun List<out BigInteger>.toOidBytes(): ByteArray {
return slice(2..<size).map { it.toAsn1VarInt() }
.fold(
byteArrayOf((first().intValue() * 40 + get(1).intValue()).toUByte().toByte())
) { acc, bytes -> acc + bytes }
}
}
}
Expand All @@ -125,7 +193,7 @@ object ObjectIdSerializer : KSerializer<ObjectIdentifier> {
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())
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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


/**
Expand Down Expand Up @@ -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<Byte>()

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) }
Expand Down Expand Up @@ -329,6 +359,46 @@ fun Iterator<Byte>.decodeAsn1VarULong(): Pair<ULong, ByteArray> {
}


/**
* 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<Byte>.decodeAsn1VarBigInt(): Pair<BigInteger, ByteArray> = 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<BigInteger, ByteArray> = 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<Byte>.decodeAsn1VarBigInt(): Pair<BigInteger, ByteArray> {
var result = BigInteger.ZERO
val mask = BigInteger.fromUByte(0x7Fu)
val accumulator = mutableListOf<Byte>()
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,
Expand Down Expand Up @@ -373,3 +443,20 @@ fun Iterator<Byte>.decodeAsn1VarUInt(): Pair<UInt, ByteArray> {

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()
Loading

0 comments on commit f4454c9

Please sign in to comment.