diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d72dc1..4ecc3338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,21 @@ * `Iterable.decodeAsn1VarUInt()` * `ByteArray.decodeAsn1VarUInt()` * Revamp implicit tagging +* Revamp `Asn1Element.parse()`, introducing new variants. This yields: + * `Asn1Element.parse()` with the same semantics as before + * `Asn1Element.parse()` alternative introduced, which takes a `ByteIterator` instead of a `ByteArray` + * `Asn1Element.parseAll()` introduced, which consumes all bytes and returns a list of all ASN.1 elements (if parsing works) + * Variant 1 takes a `ByteIterator` + * Variant 2 takes a `ByteArray` + * `Asn1Element.parseFirst()` introduced, which tries to only parse a single ASN.1 element from the input and leaves the rest untouched. + * Variant 1 takes a `ByteIterator` and returns the element; the `ByteIterator` is advanced accordingly + * Variant 2 takes a `ByteArray` and returns a `Pair` of `(element, remainingBytes)` +* More consistent low-level encoding and decoding function names: + * `encodeToAsn1Primitive` to produce an `Asn1Primitive` that can directly be DER-encoded + * `encodeToAsn1ContentBytes` to produce the content bytes of a TLV primitive (the _V_ in TLV) + * `decodeToXXX` to be invoked on an `Asn1Primitive` to decode a DER-encoded primitive into the target type + * `decodeFromAsn1ContentBytes` to be invoked on the companion of the target type to decode the content bytes of a TLV primitive (the _V_ in TLV) +* Update conventions -> Coroutines 1.0.9 ## 3.0 diff --git a/README.md b/README.md index 495fcde6..5ab8cd46 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,11 @@ [![Kotlin](https://img.shields.io/badge/kotlin-multiplatform-orange.svg?logo=kotlin)](http://kotlinlang.org) [![Kotlin](https://img.shields.io/badge/kotlin-2.0.20-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Java](https://img.shields.io/badge/java-17+-blue.svg?logo=OPENJDK)](https://www.oracle.com/java/technologies/downloads/#java11) + [![Maven Central (indispensable)](https://img.shields.io/maven-central/v/at.asitplus.signum/indispensable?label=maven-central%20%28indispensable%29)](https://mvnrepository.com/artifact/at.asitplus.signum/) +[![Maven SNAPSHOT (indispensable)](https://img.shields.io/nexus/snapshots/https/s01.oss.sonatype.org/at.asitplus.signum/indispensable?label=SNAPSHOT%20%28indispensable%29)](https://s01.oss.sonatype.org/content/repositories/snapshots/at/asitplus/signum/indispensable/) [![Maven Central (Supreme)](https://img.shields.io/maven-central/v/at.asitplus.signum/supreme?label=maven-central%20%28Supreme%29)](https://mvnrepository.com/artifact/at.asitplus.signum/supreme) +[![Maven SNAPSHOT (Supreme)](https://img.shields.io/nexus/snapshots/https/s01.oss.sonatype.org/at.asitplus.signum/supreme?label=SNAPSHOT%20%28Supreme%29)](https://s01.oss.sonatype.org/content/repositories/snapshots/at/asitplus/signum/supreme/) @@ -383,47 +386,76 @@ Which results in the following output: ### Working with Generic ASN.1 Structures The magic shown above is based on a from-scratch 100% KMP implementation of an ASN.1 encoder and parser. -To parse any DER-encoded ASN.1 structure, call `Asn1Element.parse(derBytes)`, which will result in exactly a single -`Asn1Element`. -It can be re-encoded (and yes, it is a true re-encoding, since the original bytes are discarded after decoding) by -accessing the lazily evaluated `.derEncoded` property. +To parse any DER-encoded ASN.1 structure, call either: + +* `Asn1Element.parse()`, which will consume all bytes and return the first parsed ASN.1 element. +This method throws if parsing errors occur or any trailing bytes are left after parsing the first element. +* `Asn1Element.parseFirst()`, which will try to parse a single toplevel ASN.1 element. +Any remaining bytes can still be consumed from the iterator, as it will only be advanced to right after the first parsed element. +* `Asn1Element.parseAll()`, wich consumes all bytes, parses all toplevel ASN.1 elements, and returns them as list. +Throws on any parsing error. + +`Asn1Element`s can encoded by accessing the lazily evaluated `.derEncoded` property. +Even for parsed elements, this is a true re-encoding. The original bytes are discarded after decoding. **Note that decoding operations will throw exceptions if invalid data is provided!** A parsed `Asn1Element` can either be a primitive (whose tag and value can be read) or a structure (like a set or -sequence) whose child -nodes can be processed as desired. Subclasses of `Asn1Element` reflect this: +sequence) whose child nodes can be processed as desired. Subclasses of `Asn1Element` reflect this: * `Asn1Primitive` + * `Asn1BitString` (for convenience) + * `Asn1PrimitiveOctetString` (for convenience) * `Asn1Structure` - * `Asn1Set` - * `Asn1Sequence` - + * `Asn1Sequence` and `Asn1SequenceOf` + * `Asn1Set` and `Asn1SetOf` (sorting children by default) + * `Asn1EncapsulatingOctetString` (tagged as OCTET STRING, containing a valid ASN.1 structure or primitive) + * `Asn1ExplicitlyTagged` (user-specified tag + CONTEXT_SPECIFIC + CONSTRUCTED) + * `Asn1CustomStructure` (any other CONSTRUCTED tag not fitting the above options. CONSTRUCTED bit may be overridden) + +Convenience wrappers exist, to cast to any subtype (e.g. `.asSequence()`). These shorthand functions throw an `Asn1Exception` +if a cast is not possible. Any complex data structure (such as CSR, public key, certificate, …) implements `Asn1Encodable`, which means you can: * encapsulate it into an ASN.1 Tree by calling `.encodeToTlv()` * directly get a DER-encoded byte array through the `.encodetoDer()` function -To also suport going the other way, the companion objects of these complex classes implement `Asn1Decodable`, which -allows for +A tandem of helper functions is available for primitives (numbers, booleans, string, bigints): + +* `encodeToAsn1Primitive` to produce an `Asn1Primitive` that can directly be DER-encoded +* `encodeToAsn1ContentBytes` to produce the content bytes of a TLV primitive (the _V_ in TLV) -* directly parsing DER-encoded byte arrays by calling `.decodeFromDer(bytes)` -* processing an `Asn1Element` by calling `.fromTlv(src)` +Variations of these exist for `Instant` and `ByteArray`. + +Check out [Asn1Encoding.kt](indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Encoding.kt) for a full +list of helper functions. #### Decoding Values -Various helper functions exist to facilitate decoding the values contained in `Asn1Primitives`, such as `decodeInt()`, -for example. +Various helper functions exist to facilitate decoding the values contained in `Asn1Primitives`, such as `readInt()`, +for example. To also support decoding more complex structures, the companion objects of complex classes (such as certificates, CSRs, …) +implement `Asn1Decodable`, which allows for: + +* directly parsing DER-encoded byte arrays by calling `.decodeFromDer(bytes)` and `.decodeFromDerHexString` +* processing an `Asn1Element` by calling `.decodefromTlv(src)` + +Both encoding and decoding functions come in two _safe_ (i.e. non-throwing) variants: +* `…Safe()` which returns a [KmmResult](https://github.com/a-sit-plus/kmmresult) +* `…orNull()` which returns null on error + +Similarly to encoding, a tandem of decoding functions exists for primitives: +* `decodeToXXX` to be invoked on an `Asn1Primitive` to decode a DER-encoded primitive into the target type +* `decodeFromAsn1ContentBytes` to be invoked on the companion of the target type to decode the content bytes of a TLV primitive (the _V_ in TLV) + However, anything can be decoded and tagged at will. Therefore, a generic decoding function exists, which has the following signature: ```kotlin -inline fun Asn1Primitive.decode(tag: UByte, decode: (content: ByteArray) -> T) +inline fun Asn1Primitive.decode(assertTag: Asn1Element.Tag, decode: (content: ByteArray) -> T) ``` -Check out [Asn1Reader.kt](datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Reader.kt) for a full -list -of helper functions. +Check out [Asn1Decoding.kt](indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt) for a full +list of helper functions. #### ASN1 DSL for Creating ASN.1 Structures diff --git a/build.gradle.kts b/build.gradle.kts index e7270272..fa0544d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,16 @@ import org.jetbrains.dokka.gradle.DokkaMultiModuleTask plugins { - id("at.asitplus.gradle.conventions") version "2.0.20+20240829" + id("at.asitplus.gradle.conventions") version "2.0.20+20240920" id("com.android.library") version "8.2.2" apply (false) } group = "at.asitplus.signum" +//work around nexus publish bug +val artifactVersion: String by extra +version = artifactVersion +//end work around nexus publish bug + //access dokka plugin from conventions plugin's classpath in root project → no need to specify version apply(plugin = "org.jetbrains.dokka") @@ -26,7 +31,7 @@ tasks.getByName("dokkaHtmlMultiModule") { "josef-light.png", "signum-light-large.png", "signum-dark-large.png", - ).files.forEach { it.copyTo(File("build/dokka/${it.name}"), overwrite = true) } + ).files.forEach { it.copyTo(File("build/dokka/${it.name}"), overwrite = true) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 18a76c81..fb3cf6c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ bignum = "0.3.10" jose = "9.31" kotlinpoet = "1.16.0" runner = "1.5.2" +kotest-plugin = "20240918.002009-71" [libraries] bignum = { group = "com.ionspin.kotlin", name = "bignum", version.ref = "bignum" } diff --git a/indispensable-cosef/build.gradle.kts b/indispensable-cosef/build.gradle.kts index f2be9fb0..1ecad2af 100644 --- a/indispensable-cosef/build.gradle.kts +++ b/indispensable-cosef/build.gradle.kts @@ -39,6 +39,7 @@ kotlin { exportIosFramework( "IndispensableCosef", + transitiveExports=false, serialization("cbor"), datetime(), kmmresult(), diff --git a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseKey.kt b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseKey.kt index e57bf0e6..525915fa 100644 --- a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseKey.kt +++ b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseKey.kt @@ -6,7 +6,7 @@ import at.asitplus.catching import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.SignatureAlgorithm import at.asitplus.signum.indispensable.SpecializedCryptoPublicKey -import at.asitplus.signum.indispensable.asn1.toTwosComplementByteArray +import at.asitplus.signum.indispensable.asn1.encoding.toTwosComplementByteArray import at.asitplus.signum.indispensable.cosef.CoseKey.Companion.deserialize import at.asitplus.signum.indispensable.cosef.CoseKeySerializer.CompressedCompoundCoseKeySerialContainer import at.asitplus.signum.indispensable.cosef.CoseKeySerializer.UncompressedCompoundCoseKeySerialContainer diff --git a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseKeyParams.kt b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseKeyParams.kt index 6558a9d9..39d90934 100644 --- a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseKeyParams.kt +++ b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseKeyParams.kt @@ -5,7 +5,7 @@ import at.asitplus.KmmResult.Companion.failure import at.asitplus.catching import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.SpecializedCryptoPublicKey -import at.asitplus.signum.indispensable.asn1.decodeFromDerValue +import at.asitplus.signum.indispensable.asn1.encoding.decodeFromAsn1ContentBytes /** * Wrapper to handle parameters for different COSE public key types. @@ -164,7 +164,7 @@ sealed class CoseKeyParams : SpecializedCryptoPublicKey { override fun toCryptoPublicKey(): KmmResult = catching { CryptoPublicKey.Rsa( n = n ?: throw IllegalArgumentException("Missing modulus n"), - e = Int.decodeFromDerValue(e ?: + e = Int.decodeFromAsn1ContentBytes(e ?: throw IllegalArgumentException("Missing or invalid exponent e")) ) } diff --git a/indispensable-josef/build.gradle.kts b/indispensable-josef/build.gradle.kts index 41484642..e57d3e9d 100644 --- a/indispensable-josef/build.gradle.kts +++ b/indispensable-josef/build.gradle.kts @@ -46,6 +46,7 @@ kotlin { exportIosFramework( "IndispensableJosef", + transitiveExports=false, serialization("json"), datetime(), kmmresult(), diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JsonWebKey.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JsonWebKey.kt index 281cbba5..ebd14f8d 100644 --- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JsonWebKey.kt +++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JsonWebKey.kt @@ -8,8 +8,8 @@ import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.CryptoPublicKey.EC.Companion.fromUncompressed import at.asitplus.signum.indispensable.ECCurve import at.asitplus.signum.indispensable.SpecializedCryptoPublicKey -import at.asitplus.signum.indispensable.asn1.decodeFromDerValue -import at.asitplus.signum.indispensable.asn1.toTwosComplementByteArray +import at.asitplus.signum.indispensable.asn1.encoding.decodeFromAsn1ContentBytes +import at.asitplus.signum.indispensable.asn1.encoding.toTwosComplementByteArray import at.asitplus.signum.indispensable.io.Base64UrlStrict import at.asitplus.signum.indispensable.io.ByteArrayBase64UrlSerializer import at.asitplus.signum.indispensable.josef.io.JwsCertificateSerializer @@ -303,7 +303,7 @@ data class JsonWebKey( JwkType.RSA -> { CryptoPublicKey.Rsa( n = n ?: throw IllegalArgumentException("Missing modulus n"), - e = e?.let { bytes -> Int.decodeFromDerValue(bytes) } + e = e?.let { bytes -> Int.decodeFromAsn1ContentBytes(bytes) } ?: throw IllegalArgumentException("Missing or invalid exponent e") ).apply { jwkId = keyId } } diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsExtensions.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsExtensions.kt index 07f78dd3..ecb5c7a3 100644 --- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsExtensions.kt +++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsExtensions.kt @@ -1,20 +1,9 @@ package at.asitplus.signum.indispensable.josef -import at.asitplus.signum.indispensable.asn1.encodeTo4Bytes - - -//TODO a lot of this can now be streamlined thanks to our various helpers and ASN.1 Foo +import at.asitplus.signum.indispensable.asn1.encoding.encodeTo4Bytes object JwsExtensions { - /** - * ASN.1 encoding about encoding of integers: - * Bits of first octet and bit 8 of the second octet - * shall not be all ones; and shall not be all zeros - */ - private fun ByteArray.toAsn1Integer() = if (this[0] < 0) byteArrayOf(0) + this else - if (this[0] == 0x00.toByte() && this[1] > 0) drop(1).toByteArray() else this - /** * Prepend `this` with the size as four bytes */ diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/io/JwsCertificateSerializer.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/io/JwsCertificateSerializer.kt index 2f0b0cd2..21e849a3 100644 --- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/io/JwsCertificateSerializer.kt +++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/io/JwsCertificateSerializer.kt @@ -7,5 +7,5 @@ import at.asitplus.signum.indispensable.pki.X509Certificate object JwsCertificateSerializer : TransformingSerializerTemplate( parent = ByteArrayBase64Serializer, encodeAs = X509Certificate::encodeToDer, - decodeAs = X509Certificate::decodeFromDer + decodeAs = { X509Certificate.decodeFromDer(it) } //workaround iOS compilation bug KT-71498 ) diff --git a/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/JsonWebKeyJvmTest.kt b/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/JsonWebKeyJvmTest.kt index 14f7da0f..55ff93e5 100644 --- a/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/JsonWebKeyJvmTest.kt +++ b/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/JsonWebKeyJvmTest.kt @@ -3,8 +3,8 @@ package at.asitplus.signum.indispensable.josef import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.CryptoPublicKey.EC.Companion.fromUncompressed import at.asitplus.signum.indispensable.ECCurve -import at.asitplus.signum.indispensable.asn1.ensureSize -import at.asitplus.signum.indispensable.asn1.toTwosComplementByteArray +import at.asitplus.signum.indispensable.io.ensureSize +import at.asitplus.signum.indispensable.asn1.encoding.toTwosComplementByteArray import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe diff --git a/indispensable/build.gradle.kts b/indispensable/build.gradle.kts index 32879141..a3d1ba24 100644 --- a/indispensable/build.gradle.kts +++ b/indispensable/build.gradle.kts @@ -197,6 +197,7 @@ kotlin { exportIosFramework( "Indispensable", + transitiveExports=false, serialization("json"), datetime(), kmmresult(), diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt index 5fff0e63..ff4d4fe6 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt @@ -3,8 +3,8 @@ package at.asitplus.signum.indispensable import at.asitplus.KmmResult import at.asitplus.catching import at.asitplus.signum.indispensable.asn1.* -import at.asitplus.signum.indispensable.asn1.Asn1.BitString -import at.asitplus.signum.indispensable.asn1.Asn1.Null +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.BitString +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Null import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer import at.asitplus.signum.indispensable.misc.ANSIECPrefix import at.asitplus.signum.indispensable.misc.ANSIECPrefix.Companion.hasPrefix @@ -13,6 +13,8 @@ import at.asitplus.io.MultiBase import at.asitplus.io.UVarInt import at.asitplus.io.multibaseDecode import at.asitplus.io.multibaseEncode +import at.asitplus.signum.indispensable.asn1.encoding.* +import at.asitplus.signum.indispensable.io.ensureSize import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.Sign import kotlinx.serialization.SerialName @@ -115,7 +117,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { val curve = ECCurve.entries.find { it.oid == curveOid } ?: throw Asn1Exception("Curve not supported: $curveOid") - val bitString = (src.nextChild() as Asn1Primitive).readBitString() + val bitString = (src.nextChild() as Asn1Primitive).asAsn1BitString() if (!bitString.rawBytes.hasPrefix(ANSIECPrefix.UNCOMPRESSED)) throw Asn1Exception("EC key not prefixed with 0x04") val xAndY = bitString.rawBytes.drop(1) val coordLen = curve.coordinateLength.bytes.toInt() @@ -126,10 +128,10 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { Rsa.oid -> { (keyInfo.nextChild() as Asn1Primitive).readNull() - val bitString = (src.nextChild() as Asn1Primitive).readBitString() + val bitString = (src.nextChild() as Asn1Primitive).asAsn1BitString() val rsaSequence = Asn1Element.parse(bitString.rawBytes) as Asn1Sequence val n = (rsaSequence.nextChild() as Asn1Primitive).decode(Asn1Element.Tag.INT) { it } - val e = (rsaSequence.nextChild() as Asn1Primitive).readInt() + val e = (rsaSequence.nextChild() as Asn1Primitive).decodeToInt() if (rsaSequence.hasMoreChildren()) throw Asn1StructuralException("Superfluous data in SPKI!") return Rsa(n, e) } @@ -283,7 +285,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { fun fromPKCS1encoded(input: ByteArray): Rsa = runRethrowing { val conv = Asn1Element.parse(input) as Asn1Sequence val n = (conv.nextChild() as Asn1Primitive).decode(Asn1Element.Tag.INT) { it } - val e = (conv.nextChild() as Asn1Primitive).readInt() + val e = (conv.nextChild() as Asn1Primitive).decodeToInt() if (conv.hasMoreChildren()) throw Asn1StructuralException("Superfluous bytes") return Rsa(Size.of(n), n, e) } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt index 6c464865..c469f63e 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt @@ -1,7 +1,9 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.* import at.asitplus.signum.indispensable.io.Base64Strict +import at.asitplus.signum.indispensable.io.ensureSize import at.asitplus.signum.indispensable.misc.BitLength import at.asitplus.signum.indispensable.misc.max import at.asitplus.signum.indispensable.pki.X509Certificate @@ -91,9 +93,9 @@ sealed interface CryptoSignature : Asn1Encodable { require(s.isPositive) { "s must be positive" } } - override val signature: Asn1Element = Asn1.Sequence { +r.encodeToTlv(); +s.encodeToTlv() } + override val signature: Asn1Element = Asn1.Sequence { +r.encodeToAsn1Primitive(); +s.encodeToAsn1Primitive() } - override fun encodeToTlvBitString(): Asn1Element = encodeToDer().encodeToTlvBitString() + override fun encodeToTlvBitString(): Asn1Element = encodeToDer().encodeToAsn1BitStringPrimitive() /** * Two signatures are considered equal if `r` and `s` are equal. @@ -209,13 +211,13 @@ sealed interface CryptoSignature : Asn1Encodable { @Throws(Asn1Exception::class) fun decodeFromTlvBitString(src: Asn1Primitive): EC.IndefiniteLength = runRethrowing { - decodeFromDer(src.readBitString().rawBytes) + decodeFromDer(src.asAsn1BitString().rawBytes) } override fun doDecode(src: Asn1Element): EC.IndefiniteLength { src as Asn1Sequence - val r = (src.nextChild() as Asn1Primitive).readBigInteger() - val s = (src.nextChild() as Asn1Primitive).readBigInteger() + val r = (src.nextChild() as Asn1Primitive).decodeToBigInteger() + val s = (src.nextChild() as Asn1Primitive).decodeToBigInteger() if (src.hasMoreChildren()) throw Asn1Exception("Illegal Signature Format") return fromRS(r, s) } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/ECPoint.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/ECPoint.kt index 819e381e..e9e43cf5 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/ECPoint.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/ECPoint.kt @@ -1,6 +1,6 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.asn1.ensureSize +import at.asitplus.signum.indispensable.io.ensureSize import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer import at.asitplus.signum.indispensable.misc.compressY import at.asitplus.signum.indispensable.misc.decompressY diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt index 04fbf775..7b2c8d1b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt @@ -2,8 +2,10 @@ package at.asitplus.signum.indispensable import at.asitplus.catching import at.asitplus.signum.indispensable.asn1.* -import at.asitplus.signum.indispensable.asn1.Asn1.Null -import at.asitplus.signum.indispensable.asn1.Asn1.ExplicitlyTagged +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Null +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.ExplicitlyTagged +import at.asitplus.signum.indispensable.asn1.encoding.decodeToInt import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -150,7 +152,7 @@ enum class X509SignatureAlgorithm( ) val last = (seq.nextChild() as Asn1ExplicitlyTagged).verifyTag(2u).single() as Asn1Primitive - val saltLen = last.readInt() + val saltLen = last.decodeToInt() return sigAlg.let { when (it) { diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Decoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Decoding.kt deleted file mode 100644 index 8bb3e1e2..00000000 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Decoding.kt +++ /dev/null @@ -1,433 +0,0 @@ -package at.asitplus.signum.indispensable.asn1 - -import at.asitplus.catching -import at.asitplus.signum.indispensable.asn1.BERTags.BMP_STRING -import at.asitplus.signum.indispensable.asn1.BERTags.IA5_STRING -import at.asitplus.signum.indispensable.asn1.BERTags.NUMERIC_STRING -import at.asitplus.signum.indispensable.asn1.BERTags.PRINTABLE_STRING -import at.asitplus.signum.indispensable.asn1.BERTags.T61_STRING -import at.asitplus.signum.indispensable.asn1.BERTags.UNIVERSAL_STRING -import at.asitplus.signum.indispensable.asn1.BERTags.UTF8_STRING -import at.asitplus.signum.indispensable.asn1.BERTags.VISIBLE_STRING -import com.ionspin.kotlin.bignum.integer.BigInteger -import com.ionspin.kotlin.bignum.integer.util.fromTwosComplementByteArray -import kotlinx.datetime.Instant -import kotlin.experimental.and -import kotlin.math.ceil - - -/** - * Parses the provides [input] into a single [Asn1Element] - * @return the parsed [Asn1Element] - * - * @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input] - */ -@Throws(Asn1Exception::class) -fun Asn1Element.Companion.parse(input: ByteArray) = Asn1Reader(input).doParse().let { - if (it.size != 1) throw Asn1StructuralException("Multiple ASN.1 structures found") - it.first() -} - -private class Asn1Reader(input: ByteArray) { - - private var rest = input - - @Throws(Asn1Exception::class) - fun doParse(): List = runRethrowing { - val result = mutableListOf() - while (rest.isNotEmpty()) { - val tlv = read() - if (tlv.isSequence()) result.add(Asn1Sequence(Asn1Reader(tlv.content).doParse())) - else if (tlv.isSet()) result.add(Asn1Set.fromPresorted(Asn1Reader(tlv.content).doParse())) - else if (tlv.isExplicitlyTagged()) result.add( - Asn1ExplicitlyTagged( - tlv.tag.tagValue, - Asn1Reader(tlv.content).doParse() - ) - ) - else if (tlv.tag == Asn1Element.Tag.OCTET_STRING) { - catching { - result.add(Asn1EncapsulatingOctetString(Asn1Reader(tlv.content).doParse())) - }.getOrElse { - result.add(Asn1PrimitiveOctetString(tlv.content)) - } - } else if (tlv.tag.isConstructed) { //custom tags, we don't know if it is a SET OF, SET, SEQUENCE,… so we default to sequence semantics - result.add(Asn1CustomStructure(Asn1Reader(tlv.content).doParse(), tlv.tag.tagValue, tlv.tagClass)) - } else result.add(Asn1Primitive(tlv.tag, tlv.content)) - - } - return result - } - - private fun TLV.isSet() = tag == Asn1Element.Tag.SET - private fun TLV.isSequence() = (tag == Asn1Element.Tag.SEQUENCE) - private fun TLV.isExplicitlyTagged() = tag.isExplicitlyTagged - - @Throws(Asn1Exception::class) - private fun read(): TLV = runRethrowing { - val tlv = rest.readTlv() - if (tlv.overallLength > rest.size) - throw Asn1Exception("Out of bytes") - rest = rest.drop(tlv.overallLength).toByteArray() - return tlv - } -} - -/** - * decodes this [Asn1Primitive]'s content into an [Boolean] - * @throws [Asn1Exception] all sorts of exceptions on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readBool() = runRethrowing { - decode(Asn1Element.Tag.BOOL) { - if (it.size != 1) throw Asn1Exception("Not a Boolean!") - when (it.first().toUByte()) { - 0.toUByte() -> false - 0xff.toUByte() -> true - else -> throw Asn1Exception("${it.first().toString(16).uppercase()} is not a value!") - } - } -} - -/** - * decodes this [Asn1Primitive]'s content into an [Int] - * @throws [Asn1Exception] on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readInt() = runRethrowing { decode(Asn1Element.Tag.INT) { Int.decodeFromDerValue(it) } } - -/** Exception-free version of [readInt] */ -fun Asn1Primitive.readIntOrNull() = runCatching { readInt() }.getOrNull() - -/** - * decodes this [Asn1Primitive]'s content into a [Long] - * @throws [Asn1Exception] on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readLong() = runRethrowing { decode(Asn1Element.Tag.INT) { Long.decodeFromDerValue(it) } } - -/** Exception-free version of [readLong] */ -inline fun Asn1Primitive.readLongOrNull() = runCatching { readLong() }.getOrNull() - -/** - * decodes this [Asn1Primitive]'s content into an [UInt] - * @throws [Asn1Exception] on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readUInt() = runRethrowing { decode(Asn1Element.Tag.INT) { UInt.decodeFromDerValue(it) } } - -/** Exception-free version of [readUInt] */ -inline fun Asn1Primitive.readUIntOrNull() = runCatching { readUInt() }.getOrNull() - -/** - * decodes this [Asn1Primitive]'s content into an [ULong] - * @throws [Asn1Exception] on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readULong() = runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromDerValue(it) } } - -/** Exception-free version of [readULong] */ -inline fun Asn1Primitive.readULongOrNull() = runCatching { readULong() }.getOrNull() - -/** Decode the [Asn1Primitive] as a [BigInteger] - * @throws [Asn1Exception] on invalid input */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readBigInteger() = runRethrowing { decode(Asn1Element.Tag.INT) { BigInteger.decodeFromDerValue(it) } } - -/** Exception-free version of [readBigInteger] */ -inline fun Asn1Primitive.readBigIntegerOrNull() = runCatching { readBigInteger() }.getOrNull() - -/** - * decodes this [Asn1Primitive]'s content into an [Asn1String] - * - * @throws [Asn1Exception] all sorts of exceptions on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readString(): Asn1String = runRethrowing { - when (tag.tagValue) { - UTF8_STRING.toULong() -> Asn1String.UTF8(content.decodeToString()) - UNIVERSAL_STRING.toULong() -> Asn1String.Universal(content.decodeToString()) - IA5_STRING.toULong() -> Asn1String.IA5(content.decodeToString()) - BMP_STRING.toULong() -> Asn1String.BMP(content.decodeToString()) - T61_STRING.toULong() -> Asn1String.Teletex(content.decodeToString()) - PRINTABLE_STRING.toULong() -> Asn1String.Printable(content.decodeToString()) - NUMERIC_STRING.toULong() -> Asn1String.Numeric(content.decodeToString()) - VISIBLE_STRING.toULong() -> Asn1String.Visible(content.decodeToString()) - else -> TODO("Support other string tag $tag") - } -} - -/** - * Exception-free version of [readString] - */ -fun Asn1Primitive.readStringOrNull() = catching { readString() }.getOrNull() - - -/** - * decodes this [Asn1Primitive]'s content into an [Instant] if it is encoded as UTC TIME or GENERALIZED TIME - * - * @throws Asn1Exception on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readInstant() = - when (tag) { - Asn1Element.Tag.TIME_UTC -> decode(Asn1Element.Tag.TIME_UTC, Instant.Companion::decodeUtcTimeFromDer) - Asn1Element.Tag.TIME_GENERALIZED -> decode( - Asn1Element.Tag.TIME_GENERALIZED, - Instant.Companion::decodeGeneralizedTimeFromDer - ) - - else -> TODO("Support time tag $tag") - } - -/** - * Exception-free version of [readInstant] - */ -fun Asn1Primitive.readInstantOrNull() = catching { readInstant() }.getOrNull() - - -/** - * decodes this [Asn1Primitive]'s content into a [ByteArray], assuming it was encoded as BIT STRING - * - * @throws Asn1Exception on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readBitString() = Asn1BitString.decodeFromTlv(this) - -/** - * Exception-free version of [readBitString] - */ -fun Asn1Primitive.readBitStringOrNull() = catching { readBitString() }.getOrNull() - - -/** - * decodes this [Asn1Primitive] to null (i.e. verifies the tag to be [BERTags.ASN1_NULL] and the content to be empty - * - * @throws Asn1Exception on invalid input - */ -@Throws(Asn1Exception::class) -fun Asn1Primitive.readNull() = decode(Asn1Element.Tag.NULL) {} - -/** - * Name seems odd, but this is just an exception-free version of [readNull] - */ -fun Asn1Primitive.readNullOrNull() = catching { readNull() }.getOrNull() - - - -/** - * Generic decoding function. Verifies that this [Asn1Primitive]'s tag matches [tag] - * and transforms its content as per [transform] - * @throws Asn1Exception all sorts of exceptions on invalid input - */ -@Throws(Asn1Exception::class) -inline fun Asn1Primitive.decode(tag: ULong, transform: (content: ByteArray) -> T): T = - decode(Asn1Element.Tag(tag, constructed = false), transform) - -/** - * Generic decoding function. Verifies that this [Asn1Primitive]'s tag matches [tag] - * and transforms its content as per [transform] - * @throws Asn1Exception all sorts of exceptions on invalid input - */ -@Throws(Asn1Exception::class) -inline fun Asn1Primitive.decode(tag: Asn1Element.Tag, transform: (content: ByteArray) -> T) = - runRethrowing { - if (tag.isConstructed) throw IllegalArgumentException("A primitive cannot have a CONSTRUCTED tag") - if (tag != this.tag) throw Asn1TagMismatchException(tag, this.tag) - transform(content) - } - -/** - * Exception-free version of [decode] - */ -inline fun Asn1Primitive.decodeOrNull(tag: ULong, transform: (content: ByteArray) -> T) = - catching { decode(tag, transform) }.getOrNull() - -@Throws(Asn1Exception::class) -private fun Instant.Companion.decodeUtcTimeFromDer(input: ByteArray): Instant = runRethrowing { - val s = input.decodeToString() - if (s.length != 13) throw IllegalArgumentException("Input too short: $input") - val year = "${s[0]}${s[1]}".toInt() - val century = if (year <= 49) "20" else "19" // RFC 5280 4.1.2.5 Validity - val isoString = "$century${s[0]}${s[1]}" + // year - "-${s[2]}${s[3]}" + // month - "-${s[4]}${s[5]}" + // day - "T${s[6]}${s[7]}" + // hour - ":${s[8]}${s[9]}" + // minute - ":${s[10]}${s[11]}" + // seconds - "${s[12]}" // time offset - return parse(isoString) -} - -@Throws(Asn1Exception::class) -private fun Instant.Companion.decodeGeneralizedTimeFromDer(input: ByteArray): Instant = runRethrowing { - val s = input.decodeToString() - if (s.length != 15) throw IllegalArgumentException("Input too short: $input") - val isoString = "${s[0]}${s[1]}${s[2]}${s[3]}" + // year - "-${s[4]}${s[5]}" + // month - "-${s[6]}${s[7]}" + // day - "T${s[8]}${s[9]}" + // hour - ":${s[10]}${s[11]}" + // minute - ":${s[12]}${s[13]}" + // seconds - "${s[14]}" // time offset - return parse(isoString) -} - -/** @throws Asn1Exception if the byte array is out of bounds for a signed int */ -@Throws(Asn1Exception::class) -fun Int.Companion.decodeFromDerValue(bytes: ByteArray): Int = runRethrowing { fromTwosComplementByteArray(bytes) } - -/** @throws Asn1Exception if the byte array is out of bounds for a signed long */ -@Throws(Asn1Exception::class) -fun Long.Companion.decodeFromDerValue(bytes: ByteArray): Long = runRethrowing { fromTwosComplementByteArray(bytes) } - -/** @throws Asn1Exception if the byte array is out of bounds for an unsigned int */ -@Throws(Asn1Exception::class) -fun UInt.Companion.decodeFromDerValue(bytes: ByteArray): UInt = runRethrowing { fromTwosComplementByteArray(bytes) } - -/** @throws Asn1Exception if the byte array is out of bounds for an unsigned long */ -@Throws(Asn1Exception::class) -fun ULong.Companion.decodeFromDerValue(bytes: ByteArray): ULong = runRethrowing { fromTwosComplementByteArray(bytes) } - -@Throws(Asn1Exception::class) -fun BigInteger.Companion.decodeFromDerValue(bytes: ByteArray): BigInteger = - runRethrowing { fromTwosComplementByteArray(bytes) } - - -@Throws(Asn1Exception::class) -private fun ByteArray.readTlv(): TLV = runRethrowing { - if (this.isEmpty()) throw IllegalArgumentException("Can't read TLV, input empty") - if (this.size == 1) return TLV(Asn1Element.Tag(byteArrayOf(this[0])), byteArrayOf()) - - val iterator = iterator() - val tag = iterator.decodeTag() - val length = iterator.decodeLength() - require(length < 1024 * 1024) { "Heap space" } - val value = with(iterator) { - ByteArray(length) { - require(hasNext()) { "Out of bytes to decode" } - next() - } - } - return TLV(Asn1Element.Tag(tag.second), value) -} - -@Throws(IllegalArgumentException::class) -private fun ByteIterator.decodeLength() = - next().let { firstByte -> - if (firstByte.isBerShortForm()) { - firstByte.toUByte().toInt() - } else { // its BER long form! - val numberOfLengthOctets = (firstByte byteMask 0x7F).toInt() - (0 until numberOfLengthOctets).fold(0) { acc, index -> - require(hasNext()) { "Can't decode length" } - acc + (next().toUByte().toInt() shl Byte.SIZE_BITS * (numberOfLengthOctets - index - 1)) - } - } - } - -private fun Byte.isBerShortForm() = this byteMask 0x80 == 0x00.toUByte() - -internal infix fun Byte.byteMask(mask: Int) = (this and mask.toUInt().toByte()).toUByte() - -internal fun ByteIterator.decodeTag(): Pair = - next().let { firstByte -> - (firstByte byteMask 0x1F).let { tagNumber -> - if (tagNumber <= 30U) { - tagNumber.toULong() to byteArrayOf(firstByte) - } else { - decodeAsn1VarULong().let { (l, b) -> l to byteArrayOf(firstByte, *b) } - } - } - } - - -/** - * Decodes an ULong 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 ULong and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] - */ -inline fun Iterable.decodeAsn1VarULong(): Pair = iterator().decodeAsn1VarULong() - -/** - * Decodes an ULong 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 ULong and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] - */ -inline fun ByteArray.decodeAsn1VarULong(): Pair = iterator().decodeAsn1VarULong() - -/** - * Decodes an ULong 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 ULong and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] - */ -fun Iterator.decodeAsn1VarULong(): Pair { - var offset = 0 - var result = 0uL - val accumulator = mutableListOf() - while (hasNext()) { - val current = next().toUByte() - accumulator += current.toByte() - if (current >= 0x80.toUByte()) { - result = (current and 0x7F.toUByte()).toULong() or (result shl 7) - } else { - result = (current and 0x7F.toUByte()).toULong() or (result shl 7) - break - } - if (++offset > ceil(ULong.SIZE_BYTES.toFloat() * 8f / 7f)) throw IllegalArgumentException("Tag number too Large do decode into ULong!") - } - - 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, - * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. - * - * @return the decoded UInt and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] - */ -inline fun Iterable.decodeAsn1VarUInt(): Pair = iterator().decodeAsn1VarUInt() - -/** - * Decodes an UInt 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 UInt and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] - */ -inline fun ByteArray.decodeAsn1VarUInt(): Pair = iterator().decodeAsn1VarUInt() - -/** - * Decodes an UInt 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 UInt and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] - */ -fun Iterator.decodeAsn1VarUInt(): Pair { - var offset = 0 - var result = 0u - val accumulator = mutableListOf() - while (hasNext()) { - val current = next().toUByte() - accumulator += current.toByte() - if (current >= 0x80.toUByte()) { - result = (current and 0x7F.toUByte()).toUInt() or (result shl 7) - } else { - result = (current and 0x7F.toUByte()).toUInt() or (result shl 7) - break - } - if (++offset > ceil(UInt.SIZE_BYTES.toFloat() * 8f / 7f)) throw IllegalArgumentException("Tag number too Large do decode into UInt!") - } - - return result to accumulator.toByteArray() -} diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Elements.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Elements.kt index b86a2d8d..5ab83961 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Elements.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Elements.kt @@ -2,6 +2,7 @@ package at.asitplus.signum.indispensable.asn1 import at.asitplus.catching import at.asitplus.signum.indispensable.asn1.Asn1Element.Tag.Template.Companion.withClass +import at.asitplus.signum.indispensable.asn1.encoding.* import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray @@ -41,7 +42,8 @@ sealed class Asn1Element( companion object { /** - * Convenience method to directly parse a HEX-string representation of DER-encoded data + * Convenience method to directly parse a HEX-string representation of DER-encoded data. + * Ignores and strips all whitespace. * @throws [Throwable] all sorts of errors on invalid input */ @Throws(Throwable::class) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Encodable.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Encodable.kt index f40b63c5..2aac946c 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Encodable.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Encodable.kt @@ -5,6 +5,7 @@ package at.asitplus.signum.indispensable.asn1 import at.asitplus.KmmResult import at.asitplus.catching import at.asitplus.signum.indispensable.asn1.Asn1Element.Tag +import at.asitplus.signum.indispensable.asn1.encoding.parse /** * Interface providing methods to encode to ASN.1 diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Encoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Encoding.kt deleted file mode 100644 index d1e9ab7e..00000000 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Encoding.kt +++ /dev/null @@ -1,641 +0,0 @@ -package at.asitplus.signum.indispensable.asn1 - -import at.asitplus.KmmResult -import at.asitplus.catching -import at.asitplus.signum.indispensable.io.BitSet -import com.ionspin.kotlin.bignum.integer.BigInteger -import com.ionspin.kotlin.bignum.integer.util.toTwosComplementByteArray -import kotlinx.datetime.Instant -import kotlin.experimental.or - -/** - * Class Providing a DSL for creating arbitrary ASN.1 structures. You will almost certainly never use it directly, but rather use it as follows: - * ```kotlin - * Sequence { - * +ExplicitlyTagged(1uL) { - * +Asn1Primitive(Asn1Element.Tag.BOOL, byteArrayOf(0x00)) //or +Asn1.Bool(false) - * } - * +Asn1.Set { - * +Asn1.Sequence { - * +Asn1.SetOf { - * +PrintableString("World") - * +PrintableString("Hello") - * } - * +Asn1.Set { - * +PrintableString("World") - * +PrintableString("Hello") - * +Utf8String("!!!") - * } - * - * } - * } - * +Asn1.Null() - * - * +ObjectIdentifier("1.2.603.624.97") - * - * +(Utf8String("Foo") withImplicitTag (0xCAFEuL withClass TagClass.PRIVATE)) - * +PrintableString("Bar") - * - * //fake Primitive - * +(Asn1.Sequence { +Asn1.Int(42) } withImplicitTag (0x5EUL without CONSTRUCTED)) - * - * +Asn1.Set { - * +Asn1.Int(3) - * +Asn1.Int(-65789876543L) - * +Asn1.Bool(false) - * +Asn1.Bool(true) - * } - * +Asn1.Sequence { - * +Asn1.Null() - * +Asn1String.Numeric("12345") - * +UtcTime(Clock.System.now()) - * } - * } withImplicitTag (1337uL withClass TagClass.APPLICATION) - * ``` - */ -class Asn1TreeBuilder { - internal val elements = mutableListOf() - - /** - * appends a single [Asn1Element] to this ASN.1 structure - */ - operator fun Asn1Element.unaryPlus() { - elements += this - } - - /** - * appends a single [Asn1Encodable] to this ASN.1 structure - * @throws Asn1Exception in case encoding constraints of children are violated - */ - @Throws(Asn1Exception::class) - operator fun Asn1Encodable<*>.unaryPlus() { - +encodeToTlv() - } -} - -/** - * Namespace object for ASN.1 builder DSL functions and utility functions for creating ASN.1 primitives - */ -object Asn1 { - /** - * Creates a new SEQUENCE as [Asn1Sequence]. - * Use as follows: - * - * ```kotlin - * Sequence { - * +Null() - * +PrintableString("World") - * +PrintableString("Hello") - * +Utf8String("!!!") - * } - * ``` - */ - fun Sequence(root: Asn1TreeBuilder.() -> Unit): Asn1Sequence { - val seq = Asn1TreeBuilder() - seq.root() - return Asn1Sequence(seq.elements) - } - - - /** - * Exception-free version of [Sequence] - */ - fun SequenceOrNull(root: Asn1TreeBuilder.() -> Unit) = - catching { Sequence(root) }.getOrNull() - - - /** - * Safe version of [Sequence], wrapping the result into a [KmmResult] - */ - fun SequenceSafe(root: Asn1TreeBuilder.() -> Unit) = catching { Sequence(root) } - - - /** - * Creates a new SET as [Asn1Set]. Elements are sorted by tag. - * Use as follows: - * - * ```kotlin - * Set { - * +Null() - * +PrintableString("World") - * +PrintableString("Hello") - * +Utf8String("!!!") - * } - * ``` - */ - fun Set(root: Asn1TreeBuilder.() -> Unit): Asn1Set { - val seq = Asn1TreeBuilder() - seq.root() - return Asn1Set(seq.elements) - } - - /** - * Exception-free version of [Set] - */ - fun SetOrNull(root: Asn1TreeBuilder.() -> Unit) = catching { Set(root) }.getOrNull() - - - /** - * Safe version of [Set], wrapping the result into a [KmmResult] - */ - fun SetSafe(root: Asn1TreeBuilder.() -> Unit) = catching { Set(root) } - - - /** - * Creates a new SET OF as [Asn1Set]. Tags of all added elements need to be the same. Elements are sorted by encoded value - * Use as follows: - * - * ```kotlin - * SetOf { - * +PrintableString("World") - * +PrintableString("!!!") - * +PrintableString("Hello") - * } - * ``` - * - * @throws Asn1Exception if children of different tags are added - */ - @Throws(Asn1Exception::class) - fun SetOf(root: Asn1TreeBuilder.() -> Unit): Asn1Set { - val seq = Asn1TreeBuilder() - seq.root() - return Asn1SetOf(seq.elements) - } - - /** - * Exception-free version of [SetOf] - */ - fun SetOfOrNull(root: Asn1TreeBuilder.() -> Unit) = catching { SetOf(root) }.getOrNull() - - - /** - * Safe version of [SetOf], wrapping the result into a [KmmResult] - */ - fun SetOfSafe(root: Asn1TreeBuilder.() -> Unit) = catching { SetOf(root) } - - - /** - * Creates a new EXPLICITLY TAGGED ASN.1 structure as [Asn1ExplicitlyTagged] using [tag]. - * - * Use as follows: - * - * ```kotlin - * ExplicitlyTagged(2uL) { - * +PrintableString("World World") - * +Null() - * +Int(1337) - * } - * ``` - */ - fun ExplicitlyTagged(tag: ULong, root: Asn1TreeBuilder.() -> Unit): Asn1ExplicitlyTagged { - val seq = Asn1TreeBuilder() - seq.root() - return Asn1ExplicitlyTagged(tag, seq.elements) - } - - /** - * Exception-free version of [ExplicitlyTagged] - */ - fun ExplicitlyTaggedOrNull(tag: ULong, root: Asn1TreeBuilder.() -> Unit) = - catching { ExplicitlyTagged(tag, root) }.getOrNull() - - /** - * Safe version on [ExplicitlyTagged], wrapping the result into a [KmmResult] - */ - fun ExplicitlyTaggedSafe(tag: ULong, root: Asn1TreeBuilder.() -> Unit) = - catching { ExplicitlyTagged(tag, root) } - - - /** - * Adds a BOOL [Asn1Primitive] to this ASN.1 structure - */ - fun Bool(value: Boolean) = value.encodeToTlv() - - - /** Adds an INTEGER [Asn1Primitive] to this ASN.1 structure */ - fun Int(value: Int) = value.encodeToTlv() - - /** Adds an INTEGER [Asn1Primitive] to this ASN.1 structure */ - fun Int(value: Long) = value.encodeToTlv() - - /** Adds an INTEGER [Asn1Primitive] to this ASN.1 structure */ - fun Int(value: UInt) = value.encodeToTlv() - - /** Adds an INTEGER [Asn1Primitive] to this ASN.1 structure */ - fun Int(value: ULong) = value.encodeToTlv() - - /** Adds an INTEGER [Asn1Primitive] to this ASN.1 structure */ - fun Int(value: BigInteger) = value.encodeToTlv() - - - /** - * Adds the passed bytes as OCTET STRING [Asn1Element] to this ASN.1 structure - */ - fun OctetString(bytes: ByteArray) = bytes.encodeToTlvOctetString() - - - /** - * Adds the passed bytes as BIT STRING [Asn1Primitive] to this ASN.1 structure - */ - fun BitString(bytes: ByteArray) = bytes.encodeToTlvBitString() - - - /** - * Transforms the passed BitSet as BIT STRING [Asn1Primitive] to this ASN.1 structure. - * **Left-Aligned and right-padded (see [Asn1BitString])** - */ - fun BitString(bitSet: BitSet) = Asn1BitString(bitSet).encodeToTlv() - - /** - * Adds the passed string as UTF8 STRING [Asn1Primitive] to this ASN.1 structure - */ - fun Utf8String(value: String) = Asn1String.UTF8(value).encodeToTlv() - - - /** - * Adds the passed string as PRINTABLE STRING [Asn1Primitive] to this ASN.1 structure - * - * @throws Asn1Exception if illegal characters are to be encoded into a printable string - */ - @Throws(Asn1Exception::class) - fun PrintableString(value: String) = Asn1String.Printable(value).encodeToTlv() - - - /** - * Adds a NULL [Asn1Primitive] to this ASN.1 structure - */ - fun Null() = Asn1Primitive(Asn1Element.Tag.NULL, byteArrayOf()) - - - /** - * Adds the passed instant as UTC TIME [Asn1Primitive] to this ASN.1 structure - */ - fun UtcTime(value: Instant) = value.encodeToAsn1UtcTime() - - - /** - * Adds the passed instant as GENERALIZED TIME [Asn1Primitive] to this ASN.1 structure - */ - fun GeneralizedTime(value: Instant) = value.encodeToAsn1GeneralizedTime() - - - /** - * OCTET STRING builder. The result of [init] is encapsulated into an ASN.1 OCTET STRING and then added to this ASN.1 structure - * ```kotlin - * OctetStringEncapsulating { - * +PrintableString("Hello") - * +PrintableString("World") - * +Sequence { - * +PrintableString("World") - * +PrintableString("Hello") - * +Utf8String("!!!") - * } - * } - * ``` - */ - fun OctetStringEncapsulating(init: Asn1TreeBuilder.() -> Unit): Asn1EncapsulatingOctetString { - val seq = Asn1TreeBuilder() - seq.init() - return Asn1EncapsulatingOctetString(seq.elements) - } - - /** - * Convenience helper to easily construct implicitly tagged elements. - * Shorthand for `Tag(tagValue, constructed=false, tagClass=TagClass.CONTEXT_SPECIFIC) - */ - fun ImplicitTag(tagNum: ULong, tagClass: TagClass = TagClass.CONTEXT_SPECIFIC) = - Asn1Element.Tag(tagNum, constructed = false, tagClass = tagClass) - - /** - * Convenience helper to easily construct implicitly tagged elements. - * Shorthand for `Tag(tagValue, constructed=true, tagClass=TagClass.CONTEXT_SPECIFIC) - */ - fun ExplicitTag(tagNum: ULong) = - Asn1Element.Tag(tagNum, constructed = true, tagClass = TagClass.CONTEXT_SPECIFIC) - -} - -/** - * Produces a BOOLEAN as [Asn1Primitive] - */ -fun Boolean.encodeToTlv() = Asn1Primitive(Asn1Element.Tag.BOOL, byteArrayOf(if (this) 0xff.toByte() else 0)) - -/** Produces an INTEGER as [Asn1Primitive] */ -fun Int.encodeToTlv() = Asn1Primitive(Asn1Element.Tag.INT, encodeToDer()) - -/** Produces an INTEGER as [Asn1Primitive] */ -fun Long.encodeToTlv() = Asn1Primitive(Asn1Element.Tag.INT, encodeToDer()) - -/** Produces an INTEGER as [Asn1Primitive] */ -fun UInt.encodeToTlv() = Asn1Primitive(Asn1Element.Tag.INT, encodeToDer()) - -/** Produces an INTEGER as [Asn1Primitive] */ -fun ULong.encodeToTlv() = Asn1Primitive(Asn1Element.Tag.INT, encodeToDer()) - -/** Produces an INTEGER as [Asn1Primitive] */ -fun BigInteger.encodeToTlv() = Asn1Primitive(Asn1Element.Tag.INT, encodeToDer()) - -/** - * Produces an OCTET STRING as [Asn1Primitive] - */ -fun ByteArray.encodeToTlvOctetString() = Asn1PrimitiveOctetString(this) - -/** - * Produces a BIT STRING as [Asn1Primitive] - */ -fun ByteArray.encodeToTlvBitString() = Asn1Primitive(Asn1Element.Tag.BIT_STRING, encodeToBitString()) - -/** - * Prepends 0x00 to this ByteArray for encoding it into a BIT STRING. Useful for implicit tagging - */ -fun ByteArray.encodeToBitString() = byteArrayOf(0x00) + this - -private fun Int.encodeToDer() = toTwosComplementByteArray() -private fun Long.encodeToDer() = toTwosComplementByteArray() -private fun UInt.encodeToDer() = toTwosComplementByteArray() -private fun ULong.encodeToDer() = toTwosComplementByteArray() -private fun BigInteger.encodeToDer() = toTwosComplementByteArray() - -/** - * Produces a UTC TIME as [Asn1Primitive] - */ -fun Instant.encodeToAsn1UtcTime() = - Asn1Primitive(Asn1Element.Tag.TIME_UTC, encodeToAsn1Time().drop(2).encodeToByteArray()) - -/** - * Produces a GENERALIZED TIME as [Asn1Primitive] - */ -fun Instant.encodeToAsn1GeneralizedTime() = - Asn1Primitive(Asn1Element.Tag.TIME_GENERALIZED, encodeToAsn1Time().encodeToByteArray()) - -private fun Instant.encodeToAsn1Time(): String { - val value = this.toString() - if (value.isEmpty()) - throw IllegalArgumentException("Instant serialization failed: no value") - val matchResult = Regex("([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})") - .matchAt(value, 0) - ?: throw IllegalArgumentException("Instant serialization failed: $value") - val year = matchResult.groups[1]?.value - ?: throw IllegalArgumentException("Instant serialization year failed: $value") - val month = matchResult.groups[2]?.value - ?: throw IllegalArgumentException("Instant serialization month failed: $value") - val day = matchResult.groups[3]?.value - ?: throw IllegalArgumentException("Instant serialization day failed: $value") - val hour = matchResult.groups[4]?.value - ?: throw IllegalArgumentException("Instant serialization hour failed: $value") - val minute = matchResult.groups[5]?.value - ?: throw IllegalArgumentException("Instant serialization minute failed: $value") - val seconds = matchResult.groups[6]?.value - ?: throw IllegalArgumentException("Instant serialization seconds failed: $value") - return "$year$month$day$hour$minute$seconds" + "Z" -} - -/** - * Encode as a four-byte array - */ -fun Int.encodeTo4Bytes(): ByteArray = byteArrayOf( - (this ushr 24).toByte(), - (this ushr 16).toByte(), - (this ushr 8).toByte(), - (this).toByte() -) - -/** - * Encode as an eight-byte array - */ -fun Long.encodeTo8Bytes(): ByteArray = byteArrayOf( - (this ushr 56).toByte(), - (this ushr 48).toByte(), - (this ushr 40).toByte(), - (this ushr 32).toByte(), - (this ushr 24).toByte(), - (this ushr 16).toByte(), - (this ushr 8).toByte(), - (this).toByte() -) - -/** Encodes an unsigned Long to a minimum-size twos-complement byte array */ -fun ULong.toTwosComplementByteArray() = when { - this >= 0x8000000000000000UL -> - byteArrayOf( - 0x00, - (this shr 56).toByte(), - (this shr 48).toByte(), - (this shr 40).toByte(), - (this shr 32).toByte(), - (this shr 24).toByte(), - (this shr 16).toByte(), - (this shr 8).toByte(), - this.toByte() - ) - - else -> this.toLong().toTwosComplementByteArray() -} - -/** Encodes an unsigned Int to a minimum-size twos-complement byte array */ -fun UInt.toTwosComplementByteArray() = toLong().toTwosComplementByteArray() - -/** Encodes a signed Long to a minimum-size twos-complement byte array */ -fun Long.toTwosComplementByteArray() = when { - (this >= -0x80L && this <= 0x7FL) -> - byteArrayOf( - this.toByte() - ) - - (this >= -0x8000L && this <= 0x7FFFL) -> - byteArrayOf( - (this ushr 8).toByte(), - this.toByte() - ) - - (this >= -0x800000L && this <= 0x7FFFFFL) -> - byteArrayOf( - (this ushr 16).toByte(), - (this ushr 8).toByte(), - this.toByte() - ) - - (this >= -0x80000000L && this <= 0x7FFFFFFFL) -> - byteArrayOf( - (this ushr 24).toByte(), - (this ushr 16).toByte(), - (this ushr 8).toByte(), - this.toByte() - ) - - (this >= -0x8000000000L && this <= 0x7FFFFFFFFFL) -> - byteArrayOf( - (this ushr 32).toByte(), - (this ushr 24).toByte(), - (this ushr 16).toByte(), - (this ushr 8).toByte(), - this.toByte() - ) - - (this >= -0x800000000000L && this <= 0x7FFFFFFFFFFFL) -> - byteArrayOf( - (this ushr 40).toByte(), - (this ushr 32).toByte(), - (this ushr 24).toByte(), - (this ushr 16).toByte(), - (this ushr 8).toByte(), - this.toByte() - ) - - (this >= -0x80000000000000L && this <= 0x7FFFFFFFFFFFFFL) -> - byteArrayOf( - (this ushr 48).toByte(), - (this ushr 40).toByte(), - (this ushr 32).toByte(), - (this ushr 24).toByte(), - (this ushr 16).toByte(), - (this ushr 8).toByte(), - this.toByte() - ) - - else -> - byteArrayOf( - (this ushr 56).toByte(), - (this ushr 48).toByte(), - (this ushr 40).toByte(), - (this ushr 32).toByte(), - (this ushr 24).toByte(), - (this ushr 16).toByte(), - (this ushr 8).toByte(), - this.toByte() - ) -} - -/** Encodes a signed Int to a minimum-size twos-complement byte array */ -fun Int.toTwosComplementByteArray() = toLong().toTwosComplementByteArray() - -fun Int.Companion.fromTwosComplementByteArray(it: ByteArray) = when (it.size) { - 4 -> (it[0].toInt() shl 24) or (it[1].toUByte().toInt() shl 16) or (it[2].toUByte() - .toInt() shl 8) or (it[3].toUByte().toInt()) - - 3 -> (it[0].toInt() shl 16) or (it[1].toUByte().toInt() shl 8) or (it[2].toUByte().toInt()) - 2 -> (it[0].toInt() shl 8) or (it[1].toUByte().toInt() shl 0) - 1 -> (it[0].toInt()) - else -> throw IllegalArgumentException("Input with size $it is out of bounds for Int") -} - -fun UInt.Companion.fromTwosComplementByteArray(it: ByteArray) = - Long.fromTwosComplementByteArray(it).let { - require((0 <= it) && (it <= 0xFFFFFFFFL)) { "Value $it is out of bounds for UInt" } - it.toUInt() - } - -fun Long.Companion.fromTwosComplementByteArray(it: ByteArray) = when (it.size) { - 8 -> (it[0].toLong() shl 56) or (it[1].toUByte().toLong() shl 48) or (it[2].toUByte().toLong() shl 40) or - (it[3].toUByte().toLong() shl 32) or (it[4].toUByte().toLong() shl 24) or - (it[5].toUByte().toLong() shl 16) or (it[6].toUByte().toLong() shl 8) or (it[7].toUByte().toLong()) - - 7 -> (it[0].toLong() shl 48) or (it[1].toUByte().toLong() shl 40) or (it[2].toUByte().toLong() shl 32) or - (it[3].toUByte().toLong() shl 24) or (it[4].toUByte().toLong() shl 16) or - (it[5].toUByte().toLong() shl 8) or (it[6].toUByte().toLong()) - - 6 -> (it[0].toLong() shl 40) or (it[1].toUByte().toLong() shl 32) or (it[2].toUByte().toLong() shl 24) or - (it[3].toUByte().toLong() shl 16) or (it[4].toUByte().toLong() shl 8) or (it[5].toUByte().toLong()) - - 5 -> (it[0].toLong() shl 32) or (it[1].toUByte().toLong() shl 24) or (it[2].toUByte().toLong() shl 16) or - (it[3].toUByte().toLong() shl 8) or (it[4].toUByte().toLong()) - - 4 -> (it[0].toLong() shl 24) or (it[1].toUByte().toLong() shl 16) or (it[2].toUByte().toLong() shl 8) or - (it[3].toUByte().toLong()) - - 3 -> (it[0].toLong() shl 16) or (it[1].toUByte().toLong() shl 8) or (it[2].toUByte().toLong()) - 2 -> (it[0].toLong() shl 8) or (it[1].toUByte().toLong() shl 0) - 1 -> (it[0].toLong()) - else -> throw IllegalArgumentException("Input with size $it is out of bounds for Long") -} - -fun ULong.Companion.fromTwosComplementByteArray(it: ByteArray) = when { - ((it.size == 9) && (it[0] == 0.toByte())) -> - (it[1].toUByte().toULong() shl 56) or (it[2].toUByte().toULong() shl 48) or (it[3].toUByte() - .toULong() shl 40) or - (it[4].toUByte().toULong() shl 32) or (it[5].toUByte().toULong() shl 24) or - (it[6].toUByte().toULong() shl 16) or (it[7].toUByte().toULong() shl 8) or - (it[8].toUByte().toULong()) - - else -> Long.fromTwosComplementByteArray(it).let { - require(it >= 0) { "Value $it is out of bounds for ULong" } - it.toULong() - } -} - -/** Encodes an unsigned Long to a minimum-size unsigned byte array */ -fun Long.toUnsignedByteArray(): ByteArray { - require(this >= 0) - return this.toTwosComplementByteArray().let { - if (it[0] == 0.toByte()) it.copyOfRange(1, it.size) - else it - } -} - -/** Encodes an unsigned Int to a minimum-size unsigned byte array */ -fun Int.toUnsignedByteArray() = toLong().toUnsignedByteArray() - -/** - * Drops bytes at the start, or adds zero bytes at the start, until the [size] is reached - */ -fun ByteArray.ensureSize(size: Int): ByteArray = (this.size - size).let { toDrop -> - when { - toDrop > 0 -> this.copyOfRange(toDrop, this.size) - toDrop < 0 -> ByteArray(-toDrop) + this - else -> this - } -} - -@Suppress("NOTHING_TO_INLINE") -inline fun ByteArray.ensureSize(size: UInt) = ensureSize(size.toInt()) - -/** - * 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 ULong.toAsn1VarInt(): ByteArray { - if (this < 128u) return byteArrayOf(this.toByte()) //Fast case - var offset = 0 - var result = mutableListOf() - - var b0 = (this shr offset and 0x7FuL).toByte() - while ((this shr offset > 0uL) || offset == 0) { - result += b0 - offset += 7 - 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) } - } -} - -//TODO: how to not duplicate this withut wasting bytes? -/** - * 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 UInt.toAsn1VarInt(): ByteArray { - if (this < 128u) return byteArrayOf(this.toByte()) //Fast case - var offset = 0 - var result = mutableListOf() - - var b0 = (this shr offset and 0x7Fu).toByte() - while ((this shr offset > 0u) || offset == 0) { - result += b0 - offset += 7 - if (offset > (UInt.SIZE_BITS - 1)) break //End of Fahnenstange - b0 = (this shr offset and 0x7Fu).toByte() - } - - return with(result) { - ByteArray(size) { fromBack(it) or asn1VarIntByteMask(it) } - } -} - -private fun MutableList.asn1VarIntByteMask(it: Int) = (if (isLastIndex(it)) 0x00 else 0x80).toByte() - -private fun MutableList.isLastIndex(it: Int) = it == size - 1 - -private fun MutableList.fromBack(it: Int) = this[size - 1 - it] diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1String.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1String.kt index b3e6bc2e..6e8b2706 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1String.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1String.kt @@ -1,5 +1,6 @@ package at.asitplus.signum.indispensable.asn1 +import at.asitplus.signum.indispensable.asn1.encoding.asAsn1String import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -119,6 +120,6 @@ sealed class Asn1String : Asn1Encodable { companion object : Asn1Decodable { @Throws(Asn1Exception::class) - override fun doDecode(src: Asn1Primitive): Asn1String = src.readString() + override fun doDecode(src: Asn1Primitive): Asn1String = src.asAsn1String() } } \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Time.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Time.kt index fb153d96..37d2221f 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Time.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Time.kt @@ -1,5 +1,8 @@ package at.asitplus.signum.indispensable.asn1 +import at.asitplus.signum.indispensable.asn1.encoding.encodeToAsn1GeneralizedTimePrimitive +import at.asitplus.signum.indispensable.asn1.encoding.encodeToAsn1UtcTimePrimitive +import at.asitplus.signum.indispensable.asn1.encoding.decodeToInstant import kotlinx.datetime.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -30,13 +33,13 @@ class Asn1Time(instant: Instant, formatOverride: Format? = null) : Asn1Encodable @Throws(Asn1Exception::class) override fun doDecode(src: Asn1Primitive) = - Asn1Time(src.readInstant(), if (src.tag == Asn1Element.Tag.TIME_UTC) Format.UTC else Format.GENERALIZED) + Asn1Time(src.decodeToInstant(), if (src.tag == Asn1Element.Tag.TIME_UTC) Format.UTC else Format.GENERALIZED) } override fun encodeToTlv(): Asn1Primitive = when (format) { - Format.UTC -> instant.encodeToAsn1UtcTime() - Format.GENERALIZED -> instant.encodeToAsn1GeneralizedTime() + Format.UTC -> instant.encodeToAsn1UtcTimePrimitive() + Format.GENERALIZED -> instant.encodeToAsn1GeneralizedTimePrimitive() } override fun equals(other: Any?): Boolean { diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/BERTags.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/BERTags.kt index 02c98161..8b6199f9 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/BERTags.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/BERTags.kt @@ -2,6 +2,7 @@ package at.asitplus.signum.indispensable.asn1 import at.asitplus.KmmResult import at.asitplus.catching +import at.asitplus.signum.indispensable.asn1.encoding.byteMask //Based on https://github.com/bcgit/bc-java/blob/main/core/src/main/java/org/bouncycastle/asn1/BERTags.java object BERTags { 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 a1f1a302..8faae1b8 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,5 +1,8 @@ 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.toAsn1VarInt import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Transient diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt new file mode 100644 index 00000000..01a59b75 --- /dev/null +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt @@ -0,0 +1,399 @@ +package at.asitplus.signum.indispensable.asn1.encoding + +import at.asitplus.catching +import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.BERTags.BMP_STRING +import at.asitplus.signum.indispensable.asn1.BERTags.IA5_STRING +import at.asitplus.signum.indispensable.asn1.BERTags.NUMERIC_STRING +import at.asitplus.signum.indispensable.asn1.BERTags.PRINTABLE_STRING +import at.asitplus.signum.indispensable.asn1.BERTags.T61_STRING +import at.asitplus.signum.indispensable.asn1.BERTags.UNIVERSAL_STRING +import at.asitplus.signum.indispensable.asn1.BERTags.UTF8_STRING +import at.asitplus.signum.indispensable.asn1.BERTags.VISIBLE_STRING +import com.ionspin.kotlin.bignum.integer.BigInteger +import com.ionspin.kotlin.bignum.integer.util.fromTwosComplementByteArray +import kotlinx.datetime.Instant +import kotlin.experimental.and + + +/** + * Parses the provided [input] into a single [Asn1Element]. Consumes all Bytes and throws if more than one Asn.1 Structure was found or trailing bytes were detected + * @return the parsed [Asn1Element] + * + * @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input] + */ +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parse(input: ByteIterator): Asn1Element = parseFirst(input).also { + if (input.hasNext()) throw Asn1StructuralException("Trailing bytes found after the first ASN.1 element") +} + +/** + * Convenience wrapper around [parse], taking a [ByteArray] as [source] + * @see parse + */ +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parse(source: ByteArray): Asn1Element = parse(source.iterator()) + +/** + * Tries to parse the [input] into a list of [Asn1Element]s. Consumes all Bytes and throws if an invalid ASN.1 Structure is found at any point. + * @return the parsed elements + * + * @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input] + */ +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parseAll(input: ByteIterator): List = input.doParseAll() + +/** + * Convenience wrapper around [parseAll], taking a [ByteArray] as [source] + * @see parse + */ +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parseAll(source: ByteArray): List = parseAll(source.iterator()) + + +/** + * Parses the first [Asn1Element] from [input]. + * @return the parsed [Asn1Element]. Trailing byte are left untouched and can be consumed from [input] after parsing + * + * @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input] + */ +//this only makes sense until we switch to kotlinx.io +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parseFirst(input: ByteIterator): Asn1Element = input.doParseSingle() + + +/** + * Convenience wrapper around [parseFirst], taking a [ByteArray] as [source]. + * @return a pari of the first parsed [Asn1Element] mapped to the remaining bytes + * @see parse + */ +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parseFirst(source: ByteArray): Pair = + source.iterator().doParseSingle().let { Pair(it, source.copyOfRange(it.overallLength, source.size)) } + +@Throws(Asn1Exception::class) +private fun ByteIterator.doParseAll(): List = runRethrowing { + val result = mutableListOf() + while (hasNext()) result += doParseSingle() + return result +} + +private fun ByteIterator.doParseSingle(): Asn1Element = runRethrowing { + val tlv = readTlv() + if (tlv.isSequence()) Asn1Sequence(tlv.content.iterator().doParseAll()) + else if (tlv.isSet()) Asn1Set.fromPresorted(tlv.content.iterator().doParseAll()) + else if (tlv.isExplicitlyTagged()) + Asn1ExplicitlyTagged(tlv.tag.tagValue, tlv.content.iterator().doParseAll()) + else if (tlv.tag == Asn1Element.Tag.OCTET_STRING) catching { + Asn1EncapsulatingOctetString(tlv.content.iterator().doParseAll()) as Asn1Element + }.getOrElse { Asn1PrimitiveOctetString(tlv.content) as Asn1Element } + else if (tlv.tag.isConstructed) { //custom tags, we don't know if it is a SET OF, SET, SEQUENCE,… so we default to sequence semantics + Asn1CustomStructure(tlv.content.iterator().doParseAll(), tlv.tag.tagValue, tlv.tagClass) + } else Asn1Primitive(tlv.tag, tlv.content) + } + + private fun TLV.isSet() = tag == Asn1Element.Tag.SET + private fun TLV.isSequence() = (tag == Asn1Element.Tag.SEQUENCE) + private fun TLV.isExplicitlyTagged() = tag.isExplicitlyTagged + + +/** + * decodes this [Asn1Primitive]'s content into an [Boolean] + * @throws [Asn1Exception] all sorts of exceptions on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.decodeToBoolean() = runRethrowing { decode(Asn1Element.Tag.BOOL) { Boolean.decodeFromAsn1ContentBytes(it) } } + +/** Exception-free version of [decodeToBoolean] */ +fun Asn1Primitive.decodeToBooleanOrNull() = runCatching { decodeToBoolean() }.getOrNull() + +/** + * decodes this [Asn1Primitive]'s content into an [Int] + * @throws [Asn1Exception] on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.decodeToInt() = runRethrowing { decode(Asn1Element.Tag.INT) { Int.decodeFromAsn1ContentBytes(it) } } + +/** Exception-free version of [decodeToInt] */ +fun Asn1Primitive.decodeToIntOrNull() = runCatching { decodeToInt() }.getOrNull() + +/** + * decodes this [Asn1Primitive]'s content into a [Long] + * @throws [Asn1Exception] on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.decodeToLong() = runRethrowing { decode(Asn1Element.Tag.INT) { Long.decodeFromAsn1ContentBytes(it) } } + +/** Exception-free version of [decodeToLong] */ +inline fun Asn1Primitive.decodeToLongOrNull() = runCatching { decodeToLong() }.getOrNull() + +/** + * decodes this [Asn1Primitive]'s content into an [UInt] + * @throws [Asn1Exception] on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.decodeToUInt() = runRethrowing { decode(Asn1Element.Tag.INT) { UInt.decodeFromAsn1ContentBytes(it) } } + +/** Exception-free version of [decodeToUInt] */ +inline fun Asn1Primitive.decodeToUIntOrNull() = runCatching { decodeToUInt() }.getOrNull() + +/** + * decodes this [Asn1Primitive]'s content into an [ULong] + * @throws [Asn1Exception] on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.decodeToULong() = runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromAsn1ContentBytes(it) } } + +/** Exception-free version of [decodeToULong] */ +inline fun Asn1Primitive.decodeToULongOrNull() = runCatching { decodeToULong() }.getOrNull() + +/** Decode the [Asn1Primitive] as a [BigInteger] + * @throws [Asn1Exception] on invalid input */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.decodeToBigInteger() = + runRethrowing { decode(Asn1Element.Tag.INT) { BigInteger.decodeFromAsn1ContentBytes(it) } } + +/** Exception-free version of [decodeToBigInteger] */ +inline fun Asn1Primitive.decodeToBigIntegerOrNull() = runCatching { decodeToBigInteger() }.getOrNull() + +/** + * transforms this [Asn1Primitive] into an [Asn1String] subtype based on its tag + * + * @throws [Asn1Exception] all sorts of exceptions on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.asAsn1String(): Asn1String = runRethrowing { + when (tag.tagValue) { + UTF8_STRING.toULong() -> Asn1String.UTF8(String.decodeFromAsn1ContentBytes(content)) + UNIVERSAL_STRING.toULong() -> Asn1String.Universal(String.decodeFromAsn1ContentBytes(content)) + IA5_STRING.toULong() -> Asn1String.IA5(String.decodeFromAsn1ContentBytes(content)) + BMP_STRING.toULong() -> Asn1String.BMP(String.decodeFromAsn1ContentBytes(content)) + T61_STRING.toULong() -> Asn1String.Teletex(String.decodeFromAsn1ContentBytes(content)) + PRINTABLE_STRING.toULong() -> Asn1String.Printable(String.decodeFromAsn1ContentBytes(content)) + NUMERIC_STRING.toULong() -> Asn1String.Numeric(String.decodeFromAsn1ContentBytes(content)) + VISIBLE_STRING.toULong() -> Asn1String.Visible(String.decodeFromAsn1ContentBytes(content)) + else -> TODO("Support other string tag $tag") + } +} + +/** + * Decodes this [Asn1Primitive]'s content into a String. + * @throws [Asn1Exception] all sorts of exceptions on invalid input + */ +fun Asn1Primitive.decodeToString() = runRethrowing {asAsn1String().value} + +/** Exception-free version of [decodeToString] */ +fun Asn1Primitive.decodeToStringOrNull() = runCatching { decodeToString() }.getOrNull() + + + +/** + * decodes this [Asn1Primitive]'s content into an [Instant] if it is encoded as UTC TIME or GENERALIZED TIME + * + * @throws Asn1Exception on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.decodeToInstant() = + when (tag) { + Asn1Element.Tag.TIME_UTC -> decode(Asn1Element.Tag.TIME_UTC, Instant.Companion::decodeUtcTimeFromAsn1ContentBytes) + Asn1Element.Tag.TIME_GENERALIZED -> decode( + Asn1Element.Tag.TIME_GENERALIZED, + Instant.Companion::decodeGeneralizedTimeFromAsn1ContentBytes + ) + + else -> TODO("Support time tag $tag") + } + +/** + * Exception-free version of [decodeToInstant] + */ +fun Asn1Primitive.decodeToInstantOrNull() = catching { decodeToInstant() }.getOrNull() + + +/** + * Transforms this [Asn1Primitive]' into an [Asn1BitString], assuming it was encoded as BIT STRING + * @throws Asn1Exception on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.asAsn1BitString() = Asn1BitString.decodeFromTlv(this, Asn1Element.Tag.BIT_STRING) + +/** + * decodes this [Asn1Primitive] to null (i.e. verifies the tag to be [BERTags.ASN1_NULL] and the content to be empty + * + * @throws Asn1Exception on invalid input + */ +@Throws(Asn1Exception::class) +fun Asn1Primitive.readNull() = decode(Asn1Element.Tag.NULL) {} + +/** + * Name seems odd, but this is just an exception-free version of [readNull] + */ +fun Asn1Primitive.readNullOrNull() = catching { readNull() }.getOrNull() + + +/** + * Generic decoding function. Verifies that this [Asn1Primitive]'s tag matches [assertTag] + * and transforms its content as per [transform] + * @throws Asn1Exception all sorts of exceptions on invalid input + */ +@Throws(Asn1Exception::class) +inline fun Asn1Primitive.decode(assertTag: ULong, transform: (content: ByteArray) -> T): T = + decode(Asn1Element.Tag(assertTag, constructed = false), transform) + +/** + * Generic decoding function. Verifies that this [Asn1Primitive]'s tag matches [assertTag] + * and transforms its content as per [transform] + * @throws Asn1Exception all sorts of exceptions on invalid input + */ +@Throws(Asn1Exception::class) +inline fun Asn1Primitive.decode(assertTag: Asn1Element.Tag, transform: (content: ByteArray) -> T) = + runRethrowing { + if (assertTag.isConstructed) throw IllegalArgumentException("A primitive cannot have a CONSTRUCTED tag") + if (assertTag != this.tag) throw Asn1TagMismatchException(assertTag, this.tag) + transform(content) + } + +/** + * Exception-free version of [decode] + */ +inline fun Asn1Primitive.decodeOrNull(tag: ULong, transform: (content: ByteArray) -> T) = + catching { decode(tag, transform) }.getOrNull() + +/** + * Decodes an [Instant] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 UTC TIME + * @throws Asn1Exception if the input does not parse + */ +@Throws(Asn1Exception::class) +fun Instant.Companion.decodeUtcTimeFromAsn1ContentBytes(input: ByteArray): Instant = runRethrowing { + val s = input.decodeToString() + if (s.length != 13) throw IllegalArgumentException("Input too short: $input") + val year = "${s[0]}${s[1]}".toInt() + val century = if (year <= 49) "20" else "19" // RFC 5280 4.1.2.5 Validity + val isoString = "$century${s[0]}${s[1]}" + // year + "-${s[2]}${s[3]}" + // month + "-${s[4]}${s[5]}" + // day + "T${s[6]}${s[7]}" + // hour + ":${s[8]}${s[9]}" + // minute + ":${s[10]}${s[11]}" + // seconds + "${s[12]}" // time offset + return parse(isoString) +} + +/** + * Decodes an [Instant] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 GENERALIZED TIME + * @throws Asn1Exception if the input does not parse + */ +@Throws(Asn1Exception::class) +fun Instant.Companion.decodeGeneralizedTimeFromAsn1ContentBytes(bytes: ByteArray): Instant = runRethrowing { + val s = bytes.decodeToString() + if (s.length != 15) throw IllegalArgumentException("Input too short: $bytes") + val isoString = "${s[0]}${s[1]}${s[2]}${s[3]}" + // year + "-${s[4]}${s[5]}" + // month + "-${s[6]}${s[7]}" + // day + "T${s[8]}${s[9]}" + // hour + ":${s[10]}${s[11]}" + // minute + ":${s[12]}${s[13]}" + // seconds + "${s[14]}" // time offset + return parse(isoString) +} + +/** + * Decodes a signed [Int] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER + * @throws Asn1Exception if the byte array is out of bounds for a signed int + */ +@Throws(Asn1Exception::class) +fun Int.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): Int = + runRethrowing { fromTwosComplementByteArray(bytes) } + +/** + * Decodes a signed [Long] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER + * @throws Asn1Exception if the byte array is out of bounds for a signed long + */ +@Throws(Asn1Exception::class) +fun Long.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): Long = + runRethrowing { fromTwosComplementByteArray(bytes) } + +/** + * Decodes a [UInt] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER + * @throws Asn1Exception if the byte array is out of bounds for an unsigned int + */ +@Throws(Asn1Exception::class) +fun UInt.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): UInt = + runRethrowing { fromTwosComplementByteArray(bytes) } + +/** + * Decodes a [ULong] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER + * @throws Asn1Exception if the byte array is out of bounds for an unsigned long + */ +@Throws(Asn1Exception::class) +fun ULong.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): ULong = + runRethrowing { fromTwosComplementByteArray(bytes) } + +/** + * Decodes a [BigInteger] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER + */ +@Throws(Asn1Exception::class) +fun BigInteger.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): BigInteger = + runRethrowing { fromTwosComplementByteArray(bytes) } + +/** + * Decodes a [Boolean] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 BOOLEAN + */ +fun Boolean.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): Boolean { + if (bytes.size != 1) throw Asn1Exception("Not a Boolean!") + return when (bytes.first().toUByte()) { + 0.toUByte() -> false + 0xff.toUByte() -> true + else -> throw Asn1Exception("${bytes.first().toString(16).uppercase()} is not a boolean value!") + } +} + + +/** + * Decodes a [String] from [bytes] assuming the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 STRING (any kind) + */ +fun String.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray) = bytes.decodeToString() + + +private fun ByteIterator.readTlv(): TLV = runRethrowing { + if (!hasNext()) throw IllegalArgumentException("Can't read TLV, input empty") + + val tag = decodeTag() + val length = decodeLength() + require(length < 1024 * 1024) { "Heap space" } + val value = ByteArray(length) { + require(hasNext()) { "Out of bytes to decode" } + nextByte() + } + + return TLV(Asn1Element.Tag(tag.second), value) +} + +@Throws(IllegalArgumentException::class) +private fun ByteIterator.decodeLength() = + nextByte().let { firstByte -> + if (firstByte.isBerShortForm()) { + firstByte.toUByte().toInt() + } else { // its BER long form! + val numberOfLengthOctets = (firstByte byteMask 0x7F).toInt() + (0 until numberOfLengthOctets).fold(0) { acc, index -> + require(hasNext()) { "Can't decode length" } + acc + (nextByte().toUByte().toInt() shl Byte.SIZE_BITS * (numberOfLengthOctets - index - 1)) + } + } + } + +private fun Byte.isBerShortForm() = this byteMask 0x80 == 0x00.toUByte() + +internal infix fun Byte.byteMask(mask: Int) = (this and mask.toUInt().toByte()).toUByte() + +internal fun ByteIterator.decodeTag(): Pair = + nextByte().let { firstByte -> + (firstByte byteMask 0x1F).let { tagNumber -> + if (tagNumber <= 30U) { + tagNumber.toULong() to byteArrayOf(firstByte) + } else { + decodeAsn1VarULong().let { (l, b) -> l to byteArrayOf(firstByte, *b) } + } + } + } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Encoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Encoding.kt new file mode 100644 index 00000000..b10b95ed --- /dev/null +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Encoding.kt @@ -0,0 +1,398 @@ +package at.asitplus.signum.indispensable.asn1.encoding + +import at.asitplus.KmmResult +import at.asitplus.catching +import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.io.BitSet +import com.ionspin.kotlin.bignum.integer.BigInteger +import com.ionspin.kotlin.bignum.integer.util.toTwosComplementByteArray +import kotlinx.datetime.Instant +import kotlin.experimental.or + +/** + * Class Providing a DSL for creating arbitrary ASN.1 structures. You will almost certainly never use it directly, but rather use it as follows: + * ```kotlin + * Sequence { + * +ExplicitlyTagged(1uL) { + * +Asn1Primitive(Asn1Element.Tag.BOOL, byteArrayOf(0x00)) //or +Asn1.Bool(false) + * } + * +Asn1.Set { + * +Asn1.Sequence { + * +Asn1.SetOf { + * +PrintableString("World") + * +PrintableString("Hello") + * } + * +Asn1.Set { + * +PrintableString("World") + * +PrintableString("Hello") + * +Utf8String("!!!") + * } + * + * } + * } + * +Asn1.Null() + * + * +ObjectIdentifier("1.2.603.624.97") + * + * +(Utf8String("Foo") withImplicitTag (0xCAFEuL withClass TagClass.PRIVATE)) + * +PrintableString("Bar") + * + * //fake Primitive + * +(Asn1.Sequence { +Asn1.Int(42) } withImplicitTag (0x5EUL without CONSTRUCTED)) + * + * +Asn1.Set { + * +Asn1.Int(3) + * +Asn1.Int(-65789876543L) + * +Asn1.Bool(false) + * +Asn1.Bool(true) + * } + * +Asn1.Sequence { + * +Asn1.Null() + * +Asn1String.Numeric("12345") + * +UtcTime(Clock.System.now()) + * } + * } withImplicitTag (1337uL withClass TagClass.APPLICATION) + * ``` + */ +class Asn1TreeBuilder { + internal val elements = mutableListOf() + + /** + * appends a single [Asn1Element] to this ASN.1 structure + */ + operator fun Asn1Element.unaryPlus() { + elements += this + } + + /** + * appends a single [Asn1Encodable] to this ASN.1 structure + * @throws Asn1Exception in case encoding constraints of children are violated + */ + @Throws(Asn1Exception::class) + operator fun Asn1Encodable<*>.unaryPlus() { + +encodeToTlv() + } +} + +/** + * Namespace object for ASN.1 builder DSL functions and utility functions for creating ASN.1 primitives + */ +object Asn1 { + /** + * Creates a new SEQUENCE as [Asn1Sequence]. + * Use as follows: + * + * ```kotlin + * Sequence { + * +Null() + * +PrintableString("World") + * +PrintableString("Hello") + * +Utf8String("!!!") + * } + * ``` + */ + fun Sequence(root: Asn1TreeBuilder.() -> Unit): Asn1Sequence { + val seq = Asn1TreeBuilder() + seq.root() + return Asn1Sequence(seq.elements) + } + + + /** + * Exception-free version of [Sequence] + */ + fun SequenceOrNull(root: Asn1TreeBuilder.() -> Unit) = + catching { Sequence(root) }.getOrNull() + + + /** + * Safe version of [Sequence], wrapping the result into a [KmmResult] + */ + fun SequenceSafe(root: Asn1TreeBuilder.() -> Unit) = catching { Sequence(root) } + + + /** + * Creates a new SET as [Asn1Set]. Elements are sorted by tag. + * Use as follows: + * + * ```kotlin + * Set { + * +Null() + * +PrintableString("World") + * +PrintableString("Hello") + * +Utf8String("!!!") + * } + * ``` + */ + fun Set(root: Asn1TreeBuilder.() -> Unit): Asn1Set { + val seq = Asn1TreeBuilder() + seq.root() + return Asn1Set(seq.elements) + } + + /** + * Exception-free version of [Set] + */ + fun SetOrNull(root: Asn1TreeBuilder.() -> Unit) = catching { Set(root) }.getOrNull() + + + /** + * Safe version of [Set], wrapping the result into a [KmmResult] + */ + fun SetSafe(root: Asn1TreeBuilder.() -> Unit) = catching { Set(root) } + + + /** + * Creates a new SET OF as [Asn1Set]. Tags of all added elements need to be the same. Elements are sorted by encoded value + * Use as follows: + * + * ```kotlin + * SetOf { + * +PrintableString("World") + * +PrintableString("!!!") + * +PrintableString("Hello") + * } + * ``` + * + * @throws Asn1Exception if children of different tags are added + */ + @Throws(Asn1Exception::class) + fun SetOf(root: Asn1TreeBuilder.() -> Unit): Asn1Set { + val seq = Asn1TreeBuilder() + seq.root() + return Asn1SetOf(seq.elements) + } + + /** + * Exception-free version of [SetOf] + */ + fun SetOfOrNull(root: Asn1TreeBuilder.() -> Unit) = catching { SetOf(root) }.getOrNull() + + + /** + * Safe version of [SetOf], wrapping the result into a [KmmResult] + */ + fun SetOfSafe(root: Asn1TreeBuilder.() -> Unit) = catching { SetOf(root) } + + + /** + * Creates a new EXPLICITLY TAGGED ASN.1 structure as [Asn1ExplicitlyTagged] using [tag]. + * + * Use as follows: + * + * ```kotlin + * ExplicitlyTagged(2uL) { + * +PrintableString("World World") + * +Null() + * +Int(1337) + * } + * ``` + */ + fun ExplicitlyTagged(tag: ULong, root: Asn1TreeBuilder.() -> Unit): Asn1ExplicitlyTagged { + val seq = Asn1TreeBuilder() + seq.root() + return Asn1ExplicitlyTagged(tag, seq.elements) + } + + /** + * Exception-free version of [ExplicitlyTagged] + */ + fun ExplicitlyTaggedOrNull(tag: ULong, root: Asn1TreeBuilder.() -> Unit) = + catching { ExplicitlyTagged(tag, root) }.getOrNull() + + /** + * Safe version on [ExplicitlyTagged], wrapping the result into a [KmmResult] + */ + fun ExplicitlyTaggedSafe(tag: ULong, root: Asn1TreeBuilder.() -> Unit) = + catching { ExplicitlyTagged(tag, root) } + + + /** + * Adds a BOOL [Asn1Primitive] to this ASN.1 structure + */ + fun Bool(value: Boolean) = value.encodeToAsn1Primitive() + + + /** Creates an INTEGER [Asn1Primitive] from [value] */ + fun Int(value: Int) = value.encodeToAsn1Primitive() + + /** Creates an INTEGER [Asn1Primitive] from [value] */ + fun Int(value: Long) = value.encodeToAsn1Primitive() + + /** Creates an INTEGER [Asn1Primitive] from [value] */ + fun Int(value: UInt) = value.encodeToAsn1Primitive() + + /** Creates an INTEGER [Asn1Primitive] from [value] */ + fun Int(value: ULong) = value.encodeToAsn1Primitive() + + /** Creates an INTEGER [Asn1Primitive] from [value] */ + fun Int(value: BigInteger) = value.encodeToAsn1Primitive() + + /** Creates an OCTET STRING [Asn1Element] from [bytes] */ + fun OctetString(bytes: ByteArray) = bytes.encodeToAsn1OctetStringPrimitive() + + + /** Creates an BIT STRING [Asn1Primitive] from [bytes] */ + fun BitString(bytes: ByteArray) = bytes.encodeToAsn1BitStringPrimitive() + + + /** + * Creates an BIT STRING [Asn1Primitive] from [bitSet]. + * **Left-Aligned and right-padded (see [Asn1BitString])** + */ + fun BitString(bitSet: BitSet) = Asn1BitString(bitSet).encodeToTlv() + + /** Creates an UTF8 STRING [Asn1Primitive] from [value] */ + fun Utf8String(value: String) = Asn1String.UTF8(value).encodeToTlv() + + + /** + * Creates a PRINTABLE STRING [Asn1Primitive] from [value]. + * @throws Asn1Exception if illegal characters are to be encoded into a printable string + */ + @Throws(Asn1Exception::class) + fun PrintableString(value: String) = Asn1String.Printable(value).encodeToTlv() + + + /** + * Create a NULL [Asn1Primitive] + */ + fun Null() = Asn1Primitive(Asn1Element.Tag.NULL, byteArrayOf()) + + + /** Creates a UTC TIME [Asn1Primitive] from [value] */ + fun UtcTime(value: Instant) = value.encodeToAsn1UtcTimePrimitive() + + + /** Creates a GENERALIZED TIME [Asn1Primitive] from [value]*/ + fun GeneralizedTime(value: Instant) = value.encodeToAsn1GeneralizedTimePrimitive() + + + /** + * OCTET STRING builder. The result of [init] is encapsulated into an ASN.1 OCTET STRING [Asn1Structure] + * ```kotlin + * OctetStringEncapsulating { + * +PrintableString("Hello") + * +PrintableString("World") + * +Sequence { + * +PrintableString("World") + * +PrintableString("Hello") + * +Utf8String("!!!") + * } + * } + * ``` + */ + fun OctetStringEncapsulating(init: Asn1TreeBuilder.() -> Unit): Asn1EncapsulatingOctetString { + val seq = Asn1TreeBuilder() + seq.init() + return Asn1EncapsulatingOctetString(seq.elements) + } + + /** + * Convenience helper to easily construct implicitly tagged elements. + * Shorthand for `Tag(tagValue, constructed=false, tagClass=TagClass.CONTEXT_SPECIFIC)` + */ + fun ImplicitTag(tagNum: ULong, tagClass: TagClass = TagClass.CONTEXT_SPECIFIC) = + Asn1Element.Tag(tagNum, constructed = false, tagClass = tagClass) + + /** + * Convenience helper to easily construct implicitly tagged elements. + * Shorthand for `Tag(tagValue, constructed=true, tagClass=TagClass.CONTEXT_SPECIFIC)` + */ + fun ExplicitTag(tagNum: ULong) = + Asn1Element.Tag(tagNum, constructed = true, tagClass = TagClass.CONTEXT_SPECIFIC) + +} + +/** + * Produces a BOOLEAN as [Asn1Primitive] + */ +fun Boolean.encodeToAsn1Primitive() = Asn1Primitive(Asn1Element.Tag.BOOL, encodeToAsn1ContentBytes()) + +/** Produces an INTEGER as [Asn1Primitive] */ +fun Int.encodeToAsn1Primitive() = Asn1Primitive(Asn1Element.Tag.INT, encodeToAsn1ContentBytes()) + +/** Produces an INTEGER as [Asn1Primitive] */ +fun Long.encodeToAsn1Primitive() = Asn1Primitive(Asn1Element.Tag.INT, encodeToAsn1ContentBytes()) + +/** Produces an INTEGER as [Asn1Primitive] */ +fun UInt.encodeToAsn1Primitive() = Asn1Primitive(Asn1Element.Tag.INT, encodeToAsn1ContentBytes()) + +/** Produces an INTEGER as [Asn1Primitive] */ +fun ULong.encodeToAsn1Primitive() = Asn1Primitive(Asn1Element.Tag.INT, encodeToAsn1ContentBytes()) + +/** Produces an INTEGER as [Asn1Primitive] */ +fun BigInteger.encodeToAsn1Primitive() = Asn1Primitive(Asn1Element.Tag.INT, encodeToAsn1ContentBytes()) + +/** Produces an ASN.1 UTF8 STRING as [Asn1Primitive] */ +fun String.encodeToAsn1Primitive() = Asn1String.UTF8(this).encodeToTlv() + +/** + * Produces an OCTET STRING as [Asn1Primitive] + */ +fun ByteArray.encodeToAsn1OctetStringPrimitive() = Asn1PrimitiveOctetString(this) + +/** + * Produces a BIT STRING as [Asn1Primitive] + */ +fun ByteArray.encodeToAsn1BitStringPrimitive() = + Asn1Primitive(Asn1Element.Tag.BIT_STRING, encodeToAsn1BitStringContentBytes()) + +/** + * Prepends 0x00 to this ByteArray for encoding it into a BIT STRING. No inverse function is implemented, since `.drop(1)` does the job. + */ +fun ByteArray.encodeToAsn1BitStringContentBytes() = byteArrayOf(0x00) + this + + +/** Encodes this boolean into a [ByteArray] using the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 BOOLEAN */ +fun Boolean.encodeToAsn1ContentBytes() = byteArrayOf(if (this) 0xff.toByte() else 0) + +/** Encodes this number into a [ByteArray] using the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER */ +fun Int.encodeToAsn1ContentBytes() = toTwosComplementByteArray() + +/** Encodes this number into a [ByteArray] using the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER */ +fun Long.encodeToAsn1ContentBytes() = toTwosComplementByteArray() + +/** Encodes this number into a [ByteArray] using the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER */ +fun UInt.encodeToAsn1ContentBytes() = toTwosComplementByteArray() + +/** Encodes this number into a [ByteArray] using the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER */ +fun ULong.encodeToAsn1ContentBytes() = toTwosComplementByteArray() + +/** Encodes this number into a [ByteArray] using the same encoding as the [Asn1Primitive.content] property of an [Asn1Primitive] containing an ASN.1 INTEGER */ +fun BigInteger.encodeToAsn1ContentBytes() = toTwosComplementByteArray() + +/** + * Produces a UTC TIME as [Asn1Primitive] + */ +fun Instant.encodeToAsn1UtcTimePrimitive() = + Asn1Primitive(Asn1Element.Tag.TIME_UTC, encodeToAsn1Time().drop(2).encodeToByteArray()) + +/** + * Produces a GENERALIZED TIME as [Asn1Primitive] + */ +fun Instant.encodeToAsn1GeneralizedTimePrimitive() = + Asn1Primitive(Asn1Element.Tag.TIME_GENERALIZED, encodeToAsn1Time().encodeToByteArray()) + +private fun Instant.encodeToAsn1Time(): String { + val value = this.toString() + if (value.isEmpty()) + throw IllegalArgumentException("Instant serialization failed: no value") + val matchResult = Regex("([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})") + .matchAt(value, 0) + ?: throw IllegalArgumentException("Instant serialization failed: $value") + val year = matchResult.groups[1]?.value + ?: throw IllegalArgumentException("Instant serialization year failed: $value") + val month = matchResult.groups[2]?.value + ?: throw IllegalArgumentException("Instant serialization month failed: $value") + val day = matchResult.groups[3]?.value + ?: throw IllegalArgumentException("Instant serialization day failed: $value") + val hour = matchResult.groups[4]?.value + ?: throw IllegalArgumentException("Instant serialization hour failed: $value") + val minute = matchResult.groups[5]?.value + ?: throw IllegalArgumentException("Instant serialization minute failed: $value") + val seconds = matchResult.groups[6]?.value + ?: throw IllegalArgumentException("Instant serialization seconds failed: $value") + return "$year$month$day$hour$minute$seconds" + "Z" +} + 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 new file mode 100644 index 00000000..dcf79eb5 --- /dev/null +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt @@ -0,0 +1,335 @@ +package at.asitplus.signum.indispensable.asn1.encoding + +import at.asitplus.signum.indispensable.asn1.ObjectIdentifier +import kotlin.experimental.or +import kotlin.math.ceil + + +/** + * Encode as a four-byte array + */ +fun Int.encodeTo4Bytes(): ByteArray = byteArrayOf( + (this ushr 24).toByte(), + (this ushr 16).toByte(), + (this ushr 8).toByte(), + (this).toByte() +) + +/** + * Encode as an eight-byte array + */ +fun Long.encodeTo8Bytes(): ByteArray = byteArrayOf( + (this ushr 56).toByte(), + (this ushr 48).toByte(), + (this ushr 40).toByte(), + (this ushr 32).toByte(), + (this ushr 24).toByte(), + (this ushr 16).toByte(), + (this ushr 8).toByte(), + (this).toByte() +) + +/** Encodes an unsigned Long to a minimum-size twos-complement byte array */ +fun ULong.toTwosComplementByteArray() = when { + this >= 0x8000000000000000UL -> + byteArrayOf( + 0x00, + (this shr 56).toByte(), + (this shr 48).toByte(), + (this shr 40).toByte(), + (this shr 32).toByte(), + (this shr 24).toByte(), + (this shr 16).toByte(), + (this shr 8).toByte(), + this.toByte() + ) + + else -> this.toLong().toTwosComplementByteArray() +} + +/** Encodes an unsigned Int to a minimum-size twos-complement byte array */ +fun UInt.toTwosComplementByteArray() = toLong().toTwosComplementByteArray() + +/** Encodes a signed Long to a minimum-size twos-complement byte array */ +fun Long.toTwosComplementByteArray() = when { + (this >= -0x80L && this <= 0x7FL) -> + byteArrayOf( + this.toByte() + ) + + (this >= -0x8000L && this <= 0x7FFFL) -> + byteArrayOf( + (this ushr 8).toByte(), + this.toByte() + ) + + (this >= -0x800000L && this <= 0x7FFFFFL) -> + byteArrayOf( + (this ushr 16).toByte(), + (this ushr 8).toByte(), + this.toByte() + ) + + (this >= -0x80000000L && this <= 0x7FFFFFFFL) -> + byteArrayOf( + (this ushr 24).toByte(), + (this ushr 16).toByte(), + (this ushr 8).toByte(), + this.toByte() + ) + + (this >= -0x8000000000L && this <= 0x7FFFFFFFFFL) -> + byteArrayOf( + (this ushr 32).toByte(), + (this ushr 24).toByte(), + (this ushr 16).toByte(), + (this ushr 8).toByte(), + this.toByte() + ) + + (this >= -0x800000000000L && this <= 0x7FFFFFFFFFFFL) -> + byteArrayOf( + (this ushr 40).toByte(), + (this ushr 32).toByte(), + (this ushr 24).toByte(), + (this ushr 16).toByte(), + (this ushr 8).toByte(), + this.toByte() + ) + + (this >= -0x80000000000000L && this <= 0x7FFFFFFFFFFFFFL) -> + byteArrayOf( + (this ushr 48).toByte(), + (this ushr 40).toByte(), + (this ushr 32).toByte(), + (this ushr 24).toByte(), + (this ushr 16).toByte(), + (this ushr 8).toByte(), + this.toByte() + ) + + else -> + byteArrayOf( + (this ushr 56).toByte(), + (this ushr 48).toByte(), + (this ushr 40).toByte(), + (this ushr 32).toByte(), + (this ushr 24).toByte(), + (this ushr 16).toByte(), + (this ushr 8).toByte(), + this.toByte() + ) +} + +/** Encodes a signed Int to a minimum-size twos-complement byte array */ +fun Int.toTwosComplementByteArray() = toLong().toTwosComplementByteArray() + +fun Int.Companion.fromTwosComplementByteArray(it: ByteArray) = when (it.size) { + 4 -> (it[0].toInt() shl 24) or (it[1].toUByte().toInt() shl 16) or (it[2].toUByte() + .toInt() shl 8) or (it[3].toUByte().toInt()) + + 3 -> (it[0].toInt() shl 16) or (it[1].toUByte().toInt() shl 8) or (it[2].toUByte().toInt()) + 2 -> (it[0].toInt() shl 8) or (it[1].toUByte().toInt() shl 0) + 1 -> (it[0].toInt()) + else -> throw IllegalArgumentException("Input with size $it is out of bounds for Int") +} + +fun UInt.Companion.fromTwosComplementByteArray(it: ByteArray) = + Long.fromTwosComplementByteArray(it).let { + require((0 <= it) && (it <= 0xFFFFFFFFL)) { "Value $it is out of bounds for UInt" } + it.toUInt() + } + +fun Long.Companion.fromTwosComplementByteArray(it: ByteArray) = when (it.size) { + 8 -> (it[0].toLong() shl 56) or (it[1].toUByte().toLong() shl 48) or (it[2].toUByte().toLong() shl 40) or + (it[3].toUByte().toLong() shl 32) or (it[4].toUByte().toLong() shl 24) or + (it[5].toUByte().toLong() shl 16) or (it[6].toUByte().toLong() shl 8) or (it[7].toUByte().toLong()) + + 7 -> (it[0].toLong() shl 48) or (it[1].toUByte().toLong() shl 40) or (it[2].toUByte().toLong() shl 32) or + (it[3].toUByte().toLong() shl 24) or (it[4].toUByte().toLong() shl 16) or + (it[5].toUByte().toLong() shl 8) or (it[6].toUByte().toLong()) + + 6 -> (it[0].toLong() shl 40) or (it[1].toUByte().toLong() shl 32) or (it[2].toUByte().toLong() shl 24) or + (it[3].toUByte().toLong() shl 16) or (it[4].toUByte().toLong() shl 8) or (it[5].toUByte().toLong()) + + 5 -> (it[0].toLong() shl 32) or (it[1].toUByte().toLong() shl 24) or (it[2].toUByte().toLong() shl 16) or + (it[3].toUByte().toLong() shl 8) or (it[4].toUByte().toLong()) + + 4 -> (it[0].toLong() shl 24) or (it[1].toUByte().toLong() shl 16) or (it[2].toUByte().toLong() shl 8) or + (it[3].toUByte().toLong()) + + 3 -> (it[0].toLong() shl 16) or (it[1].toUByte().toLong() shl 8) or (it[2].toUByte().toLong()) + 2 -> (it[0].toLong() shl 8) or (it[1].toUByte().toLong() shl 0) + 1 -> (it[0].toLong()) + else -> throw IllegalArgumentException("Input with size $it is out of bounds for Long") +} + +fun ULong.Companion.fromTwosComplementByteArray(it: ByteArray) = when { + ((it.size == 9) && (it[0] == 0.toByte())) -> + (it[1].toUByte().toULong() shl 56) or (it[2].toUByte().toULong() shl 48) or (it[3].toUByte() + .toULong() shl 40) or + (it[4].toUByte().toULong() shl 32) or (it[5].toUByte().toULong() shl 24) or + (it[6].toUByte().toULong() shl 16) or (it[7].toUByte().toULong() shl 8) or + (it[8].toUByte().toULong()) + + else -> Long.fromTwosComplementByteArray(it).let { + require(it >= 0) { "Value $it is out of bounds for ULong" } + it.toULong() + } +} + +/** Encodes an unsigned Long to a minimum-size unsigned byte array */ +fun Long.toUnsignedByteArray(): ByteArray { + require(this >= 0) + return this.toTwosComplementByteArray().let { + if (it[0] == 0.toByte()) it.copyOfRange(1, it.size) + else it + } +} + +/** Encodes an unsigned Int to a minimum-size unsigned byte array */ +fun Int.toUnsignedByteArray() = toLong().toUnsignedByteArray() + + +/** + * 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 ULong.toAsn1VarInt(): ByteArray { + if (this < 128u) return byteArrayOf(this.toByte()) //Fast case + var offset = 0 + var result = mutableListOf() + + var b0 = (this shr offset and 0x7FuL).toByte() + while ((this shr offset > 0uL) || offset == 0) { + result += b0 + offset += 7 + 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 unsigned 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. + * + * This kind of encoding is used to encode [ObjectIdentifier] nodes and ASN.1 Tag values > 30 + */ +fun UInt.toAsn1VarInt(): ByteArray { + if (this < 128u) return byteArrayOf(this.toByte()) //Fast case + var offset = 0 + var result = mutableListOf() + + var b0 = (this shr offset and 0x7Fu).toByte() + while ((this shr offset > 0u) || offset == 0) { + result += b0 + offset += 7 + if (offset > (UInt.SIZE_BITS - 1)) break //End of Fahnenstange + b0 = (this shr offset and 0x7Fu).toByte() + } + + return with(result) { + ByteArray(size) { fromBack(it) or asn1VarIntByteMask(it) } + } +} + +private fun MutableList.asn1VarIntByteMask(it: Int) = (if (isLastIndex(it)) 0x00 else 0x80).toByte() + +private fun MutableList.isLastIndex(it: Int) = it == size - 1 + +private fun MutableList.fromBack(it: Int) = this[size - 1 - it] + + +/** + * Decodes an ULong 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 ULong and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + */ +inline fun Iterable.decodeAsn1VarULong(): Pair = iterator().decodeAsn1VarULong() + +/** + * Decodes an ULong 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 ULong and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + */ +inline fun ByteArray.decodeAsn1VarULong(): Pair = iterator().decodeAsn1VarULong() + +/** + * Decodes an ULong 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 ULong and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + */ +fun Iterator.decodeAsn1VarULong(): Pair { + var offset = 0 + var result = 0uL + val accumulator = mutableListOf() + while (hasNext()) { + val current = next().toUByte() + accumulator += current.toByte() + if (current >= 0x80.toUByte()) { + result = (current and 0x7F.toUByte()).toULong() or (result shl 7) + } else { + result = (current and 0x7F.toUByte()).toULong() or (result shl 7) + break + } + if (++offset > ceil(ULong.SIZE_BYTES.toFloat() * 8f / 7f)) throw IllegalArgumentException("Tag number too Large do decode into ULong!") + } + + 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, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded UInt and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] + */ +inline fun Iterable.decodeAsn1VarUInt(): Pair = iterator().decodeAsn1VarUInt() + +/** + * Decodes an UInt 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 UInt and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] + */ +inline fun ByteArray.decodeAsn1VarUInt(): Pair = iterator().decodeAsn1VarUInt() + +/** + * Decodes an UInt 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 UInt and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] + */ +fun Iterator.decodeAsn1VarUInt(): Pair { + var offset = 0 + var result = 0u + val accumulator = mutableListOf() + while (hasNext()) { + val current = next().toUByte() + accumulator += current.toByte() + if (current >= 0x80.toUByte()) { + result = (current and 0x7F.toUByte()).toUInt() or (result shl 7) + } else { + result = (current and 0x7F.toUByte()).toUInt() or (result shl 7) + break + } + if (++offset > ceil(UInt.SIZE_BYTES.toFloat() * 8f / 7f)) throw IllegalArgumentException("Tag number too Large do decode into UInt!") + } + + return result to accumulator.toByteArray() +} diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt index 97673eaf..014c1593 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt @@ -82,7 +82,7 @@ object ByteArrayBase64UrlSerializer: TransformingSerializerTemplate( parent = ByteArrayBase64UrlSerializer, encodeAs = X509Certificate::encodeToDer, - decodeAs = X509Certificate::decodeFromDer + decodeAs = { X509Certificate.decodeFromDer(it) } // workaround iOS compilation bug KT-71498 ) /** De-/serializes a public key as a Base64Url-encoded IOS encoding public key */ @@ -109,3 +109,17 @@ sealed class ListSerializerTemplate( object CertificateChainBase64UrlSerializer: ListSerializerTemplate( using = X509CertificateBase64UrlSerializer) + +/** + * Drops bytes at the start, or adds zero bytes at the start, until the [size] is reached + */ +fun ByteArray.ensureSize(size: Int): ByteArray = (this.size - size).let { toDrop -> + when { + toDrop > 0 -> this.copyOfRange(toDrop, this.size) + toDrop < 0 -> ByteArray(-toDrop) + this + else -> this + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun ByteArray.ensureSize(size: UInt) = ensureSize(size.toInt()) \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/AlternativeNames.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/AlternativeNames.kt index 96747ee2..6252ccea 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/AlternativeNames.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/AlternativeNames.kt @@ -1,6 +1,7 @@ package at.asitplus.signum.indispensable.pki import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 import at.asitplus.signum.indispensable.pki.AlternativeNames.Companion.findIssuerAltNames import at.asitplus.signum.indispensable.pki.AlternativeNames.Companion.findSubjectAltNames diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequest.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequest.kt index eb351001..eabd432b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequest.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequest.kt @@ -3,8 +3,11 @@ package at.asitplus.signum.indispensable.pki import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.X509SignatureAlgorithm import at.asitplus.signum.indispensable.asn1.* -import at.asitplus.signum.indispensable.asn1.Asn1.BitString -import at.asitplus.signum.indispensable.asn1.Asn1.ExplicitlyTagged +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.BitString +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.ExplicitlyTagged +import at.asitplus.signum.indispensable.asn1.encoding.asAsn1BitString +import at.asitplus.signum.indispensable.asn1.encoding.decodeToInt import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer import kotlinx.serialization.Serializable @@ -79,7 +82,7 @@ data class TbsCertificationRequest( companion object : Asn1Decodable { @Throws(Asn1Exception::class) override fun doDecode(src: Asn1Sequence) = runRethrowing { - val version = (src.nextChild() as Asn1Primitive).readInt() + val version = (src.nextChild() as Asn1Primitive).decodeToInt() val subject = (src.nextChild() as Asn1Sequence).children.map { RelativeDistinguishedName.decodeFromTlv(it as Asn1Set) } @@ -147,7 +150,7 @@ data class Pkcs10CertificationRequest( override fun doDecode(src: Asn1Sequence): Pkcs10CertificationRequest = runRethrowing { val tbsCsr = TbsCertificationRequest.decodeFromTlv(src.nextChild() as Asn1Sequence) val sigAlg = X509SignatureAlgorithm.decodeFromTlv(src.nextChild() as Asn1Sequence) - val signature = (src.nextChild() as Asn1Primitive).readBitString() + val signature = (src.nextChild() as Asn1Primitive).asAsn1BitString() if (src.hasMoreChildren()) throw Asn1StructuralException("Superfluous structure in CSR Structure") return Pkcs10CertificationRequest(tbsCsr, sigAlg, signature.rawBytes) } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequestAttribute.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequestAttribute.kt index 080101e6..8984f63b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequestAttribute.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequestAttribute.kt @@ -1,6 +1,7 @@ package at.asitplus.signum.indispensable.pki import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 import kotlinx.serialization.Serializable @Serializable diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt index 5a107f1a..30a705ba 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt @@ -2,6 +2,8 @@ package at.asitplus.signum.indispensable.pki import at.asitplus.catching import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 +import at.asitplus.signum.indispensable.asn1.encoding.asAsn1String import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -123,7 +125,7 @@ sealed class AttributeTypeAndValue : Asn1Encodable, Identifiable { val oid = (src.nextChild() as Asn1Primitive).readOid() if (oid.nodes.size >= 3 && oid.toString().startsWith("2.5.4.")) { val asn1String = src.nextChild() as Asn1Primitive - val str = catching { (asn1String).readString() } + val str = catching { (asn1String).asAsn1String() } if (src.hasMoreChildren()) throw Asn1StructuralException("Superfluous elements in RDN") return when (oid) { CommonName.OID -> str.fold(onSuccess = { CommonName(it) }, onFailure = { CommonName(asn1String) }) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt index f5abd8c9..82e3612c 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt @@ -5,6 +5,7 @@ import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.CryptoSignature import at.asitplus.signum.indispensable.X509SignatureAlgorithm import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.* import at.asitplus.signum.indispensable.io.BitSet import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer import at.asitplus.signum.indispensable.pki.AlternativeNames.Companion.findIssuerAltNames @@ -145,7 +146,7 @@ constructor( @Throws(Asn1Exception::class) override fun doDecode(src: Asn1Sequence) = runRethrowing { val version = src.nextChild().let { - ((it as Asn1ExplicitlyTagged).verifyTag(Tags.VERSION).single() as Asn1Primitive).readInt() + ((it as Asn1ExplicitlyTagged).verifyTag(Tags.VERSION).single() as Asn1Primitive).decodeToInt() } val serialNumber = (src.nextChild() as Asn1Primitive).decode(Asn1Element.Tag.INT) { it } val sigAlg = X509SignatureAlgorithm.decodeFromTlv(src.nextChild() as Asn1Sequence) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtension.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtension.kt index 34479e1e..87952a87 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtension.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtension.kt @@ -1,7 +1,8 @@ package at.asitplus.signum.indispensable.pki import at.asitplus.signum.indispensable.asn1.* -import at.asitplus.signum.indispensable.asn1.Asn1.Bool +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Bool import kotlinx.serialization.Serializable /** diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1EncodingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1EncodingTest.kt index 11243b0b..a2334d56 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1EncodingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1EncodingTest.kt @@ -1,15 +1,16 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* -import at.asitplus.signum.indispensable.asn1.Asn1.BitString -import at.asitplus.signum.indispensable.asn1.Asn1.Bool -import at.asitplus.signum.indispensable.asn1.Asn1.Null -import at.asitplus.signum.indispensable.asn1.Asn1.OctetString -import at.asitplus.signum.indispensable.asn1.Asn1.OctetStringEncapsulating -import at.asitplus.signum.indispensable.asn1.Asn1.PrintableString -import at.asitplus.signum.indispensable.asn1.Asn1.ExplicitlyTagged -import at.asitplus.signum.indispensable.asn1.Asn1.UtcTime -import at.asitplus.signum.indispensable.asn1.Asn1.Utf8String +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.BitString +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Bool +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Null +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.OctetString +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.OctetStringEncapsulating +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.PrintableString +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.ExplicitlyTagged +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.UtcTime +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Utf8String +import at.asitplus.signum.indispensable.asn1.encoding.* import at.asitplus.signum.indispensable.io.BitSet import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.base63.toJavaBigInteger @@ -32,7 +33,7 @@ class Asn1EncodingTest : FreeSpec({ "Boolean" - { checkAll(Arb.boolean()) { val seq = Asn1.Sequence { +Bool(it) } - val decoded = (seq.nextChild() as Asn1Primitive).readBool() + val decoded = (seq.nextChild() as Asn1Primitive).decodeToBoolean() decoded shouldBe it } } @@ -77,7 +78,7 @@ class Asn1EncodingTest : FreeSpec({ } }.derEncoded ) - +ExplicitlyTagged(9u) { +Clock.System.now().encodeToAsn1UtcTime() } + +ExplicitlyTagged(9u) { +Clock.System.now().encodeToAsn1UtcTimePrimitive() } +OctetString(byteArrayOf(17, -43, 23, -12, 8, 65, 90)) +Bool(false) +Bool(true) @@ -92,7 +93,7 @@ class Asn1EncodingTest : FreeSpec({ val bytes = (it).toTwosComplementByteArray() val fromBC = ASN1Integer(it).encoded - val long = Long.decodeFromDerValue(bytes) + val long = Long.decodeFromAsn1ContentBytes(bytes) val encoded = Asn1Primitive(Asn1Element.Tag.INT, bytes).derEncoded encoded shouldBe fromBC @@ -104,19 +105,19 @@ class Asn1EncodingTest : FreeSpec({ "failures: too small" - { checkAll(iterations = 5000, Arb.bigInt(128)) { val v = BigInteger.fromLong(Long.MIN_VALUE).minus(1).minus(BigInteger.fromTwosComplementByteArray(it.toByteArray())) - shouldThrow { Asn1.Int(v).readLong() } + shouldThrow { Asn1.Int(v).decodeToLong() } } } "failures: too large" - { checkAll(iterations = 5000, Arb.bigInt(128)) { val v = BigInteger.fromLong(Long.MAX_VALUE).plus(1).plus(BigInteger.fromTwosComplementByteArray(it.toByteArray())) - shouldThrow { Asn1.Int(v).readLong() } + shouldThrow { Asn1.Int(v).decodeToLong() } } } "successes" - { checkAll(iterations = 150000, Arb.long()) { val seq = Asn1.Sequence { +Asn1.Int(it) } - val decoded = (seq.nextChild() as Asn1Primitive).readLong() + val decoded = (seq.nextChild() as Asn1Primitive).decodeToLong() decoded shouldBe it Asn1.Int(it).derEncoded shouldBe ASN1Integer(it).encoded @@ -127,18 +128,18 @@ class Asn1EncodingTest : FreeSpec({ "ints" - { "failures: too small" - { checkAll(iterations = 5000, Arb.long(Long.MIN_VALUE.. { Asn1.Int(it).readInt() } + shouldThrow { Asn1.Int(it).decodeToInt() } } } "failures: too large" - { checkAll(iterations = 5000, Arb.long(Int.MAX_VALUE.toLong()+1.. { Asn1.Int(it).readInt() } + shouldThrow { Asn1.Int(it).decodeToInt() } } } "successes" - { checkAll(iterations = 75000, Arb.int()) { val seq = Asn1.Sequence { +Asn1.Int(it) } - val decoded = (seq.nextChild() as Asn1Primitive).readInt() + val decoded = (seq.nextChild() as Asn1Primitive).decodeToInt() decoded shouldBe it Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toLong()).encoded @@ -149,18 +150,18 @@ class Asn1EncodingTest : FreeSpec({ "unsigned ints" - { "failures: negative" - { checkAll(iterations = 5000, Arb.long(Long.MIN_VALUE..<0)) { - shouldThrow { Asn1.Int(it).readUInt() } + shouldThrow { Asn1.Int(it).decodeToUInt() } } } "failures: too large" - { checkAll(iterations = 5000, Arb.long(UInt.MAX_VALUE.toLong() + 1..Long.MAX_VALUE)) { - shouldThrow { Asn1.Int(it).readUInt() } + shouldThrow { Asn1.Int(it).decodeToUInt() } } } "successes" - { checkAll(iterations = 75000, Arb.uInt()) { val seq = Asn1.Sequence { +Asn1.Int(it) } - val decoded = (seq.nextChild() as Asn1Primitive).readUInt() + val decoded = (seq.nextChild() as Asn1Primitive).decodeToUInt() decoded shouldBe it Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toBigInteger().toJavaBigInteger()).encoded @@ -171,19 +172,19 @@ class Asn1EncodingTest : FreeSpec({ "unsigned longs" - { "failures: negative" - { checkAll(iterations = 5000, Arb.long(Long.MIN_VALUE..<0)) { - shouldThrow { Asn1.Int(it).readULong() } + shouldThrow { Asn1.Int(it).decodeToULong() } } } "failures: too large" - { checkAll(iterations = 5000, Arb.bigInt(128)) { val v = BigInteger.fromULong(ULong.MAX_VALUE).plus(1).plus(BigInteger.fromTwosComplementByteArray(it.toByteArray())) - shouldThrow { Asn1.Int(v).readULong() } + shouldThrow { Asn1.Int(v).decodeToULong() } } } "successes" - { checkAll(iterations = 75000, Arb.uLong()) { val seq = Asn1.Sequence { +Asn1.Int(it) } - val decoded = (seq.nextChild() as Asn1Primitive).readULong() + val decoded = (seq.nextChild() as Asn1Primitive).decodeToULong() decoded shouldBe it Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toBigInteger().toJavaBigInteger()).encoded diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1IntegerTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1IntegerTest.kt index 3b43c60a..2e4cbbf7 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1IntegerTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1IntegerTest.kt @@ -1,6 +1,9 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.encodeToAsn1Primitive +import at.asitplus.signum.indispensable.asn1.encoding.parse +import at.asitplus.signum.indispensable.asn1.encoding.decodeToBigInteger import com.ionspin.kotlin.bignum.integer.BigInteger import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe @@ -8,24 +11,24 @@ import io.kotest.matchers.shouldBe class Asn1IntegerTest : FreeSpec({ "Encoding: Negative" { val result = - BigInteger(-20).encodeToTlv() + BigInteger(-20).encodeToAsn1Primitive() result.toDerHexString() shouldBe "02 01 EC".replace(" ", "") } "Encoding: Large Positive" { val result = - BigInteger(0xEC).encodeToTlv() + BigInteger(0xEC).encodeToAsn1Primitive() result.toDerHexString() shouldBe "02 02 00 EC".replace(" ", "") } "Decoding: Negative" { val result = (Asn1Element.parse(ubyteArrayOf(0x02u, 0x01u, 0xECu).toByteArray()) as Asn1Primitive) - .readBigInteger() + .decodeToBigInteger() result shouldBe BigInteger(-20) } "Decoding: Large Positive" { val result = (Asn1Element.parse(ubyteArrayOf(0x02u, 0x02u, 0x00u, 0xECu).toByteArray()) as Asn1Primitive) - .readBigInteger() + .decodeToBigInteger() result shouldBe BigInteger(0xEC) } }) diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt new file mode 100644 index 00000000..668bc549 --- /dev/null +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt @@ -0,0 +1,76 @@ +package at.asitplus.signum.indispensable + +import at.asitplus.signum.indispensable.asn1.Asn1Element +import at.asitplus.signum.indispensable.asn1.Asn1Exception +import at.asitplus.signum.indispensable.asn1.Asn1PrimitiveOctetString +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 +import at.asitplus.signum.indispensable.asn1.encoding.parse +import at.asitplus.signum.indispensable.asn1.encoding.parseAll +import at.asitplus.signum.indispensable.asn1.encoding.parseFirst +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import kotlin.random.Random + +class Asn1ParserTest : FreeSpec({ + + "Multiple Elements" - { + val seq = Asn1.Sequence { + repeat(10) { + +Asn1PrimitiveOctetString(Random.nextBytes(16)) + } + } + + val encoded = seq.derEncoded + val rawChildren = + encoded.sliceArray(seq.tag.encodedTagLength + seq.encodedLength.size until seq.derEncoded.size) + + "without Garbage" { + val iterator = rawChildren.iterator() + val parseFirst = Asn1Element.parseFirst(iterator) + val childIterator = seq.children.iterator() + parseFirst shouldBe childIterator.next() + + + + val bytes = iterator.toByteArray() + bytes shouldBe rawChildren.sliceArray(parseFirst.overallLength until rawChildren.size) + Asn1Element.parseFirst(rawChildren).let { (elem,rest )-> + elem shouldBe seq.children.first() + rest shouldBe rawChildren.sliceArray(parseFirst.overallLength until rawChildren.size) + } + val byteIterator = bytes.iterator() + repeat(9) { Asn1Element.parseFirst(byteIterator) shouldBe childIterator.next() } + Asn1Element.parseAll(rawChildren.iterator()) shouldBe seq.children + + shouldThrow { Asn1Element.parse(rawChildren) } + shouldThrow { Asn1Element.parse(rawChildren.iterator()) } + } + + "with Garbage" { + val garbage = Random.nextBytes(32) + val withGarbage = rawChildren + garbage + val iterator = withGarbage.iterator() + val parseFirst = Asn1Element.parseFirst(iterator) + val childIterator = seq.children.iterator() + parseFirst shouldBe childIterator.next() + + val bytes = iterator.toByteArray() + bytes shouldBe withGarbage.sliceArray(parseFirst.overallLength until withGarbage.size) + + Asn1Element.parseFirst(withGarbage).let { (elem,rest )-> + elem shouldBe seq.children.first() + rest shouldBe withGarbage.sliceArray(parseFirst.overallLength until withGarbage.size) + } + + val byteIterator = bytes.iterator() + repeat(9) { Asn1Element.parseFirst(byteIterator) shouldBe childIterator.next() } + + + shouldThrow { Asn1Element.parseAll(withGarbage.iterator()) shouldBe seq.children } + + shouldThrow { Asn1Element.parse(withGarbage) } + shouldThrow { Asn1Element.parse(withGarbage.iterator()) } + } + } +}) \ No newline at end of file diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CastingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CastingTest.kt index d9ff3147..bdec3811 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CastingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CastingTest.kt @@ -1,6 +1,6 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.asn1.Asn1 +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 import at.asitplus.signum.indispensable.asn1.Asn1PrimitiveOctetString import at.asitplus.signum.indispensable.asn1.Asn1StructuralException import io.kotest.assertions.throwables.shouldThrow diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CryptoSignatureTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CryptoSignatureTest.kt index 2042aac7..36e9753f 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CryptoSignatureTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CryptoSignatureTest.kt @@ -1,6 +1,6 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.asn1.toTwosComplementByteArray +import at.asitplus.signum.indispensable.asn1.encoding.toTwosComplementByteArray import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.toBigInteger import io.kotest.assertions.throwables.shouldThrow diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CustomTaggedTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CustomTaggedTest.kt index 63278543..bd5e8baf 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CustomTaggedTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/CustomTaggedTest.kt @@ -1,6 +1,7 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.parse import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/ImplicitTaggingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/ImplicitTaggingTest.kt index 209f80ce..5a55486a 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/ImplicitTaggingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/ImplicitTaggingTest.kt @@ -4,6 +4,7 @@ import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.asn1.TagClass.* import at.asitplus.signum.indispensable.asn1.Asn1Element.Tag.Template.Companion.withClass import at.asitplus.signum.indispensable.asn1.Asn1Element.Tag.Template.Companion.without +import at.asitplus.signum.indispensable.asn1.encoding.parse import io.kotest.core.spec.style.FreeSpec import io.kotest.datatest.withData import io.kotest.matchers.booleans.shouldBeFalse diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt index 66f75be7..4e8d845f 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt @@ -1,6 +1,9 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.encodeToAsn1Primitive +import at.asitplus.signum.indispensable.io.ensureSize +import at.asitplus.signum.indispensable.asn1.encoding.parse import at.asitplus.signum.indispensable.pki.* import io.kotest.assertions.withClue import io.kotest.core.spec.style.FreeSpec @@ -21,7 +24,6 @@ import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder import java.security.KeyPair import java.security.KeyPairGenerator import java.security.PrivateKey -import java.security.Signature import java.security.interfaces.ECPublicKey internal fun X509SignatureAlgorithm.getContentSigner(key: PrivateKey) = @@ -191,7 +193,7 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({ attributes = listOf( Pkcs10CertificationRequestAttribute( ObjectIdentifier("1.2.1840.13549.1.9.16.1337.26"), - 1337.encodeToTlv() + 1337.encodeToAsn1Primitive() ) ) ) @@ -356,13 +358,13 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({ val extendedKeyUsage = ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage) val attr1 = - Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.26"), 1337.encodeToTlv()) + Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.26"), 1337.encodeToAsn1Primitive()) val attr11 = - Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.26"), 1337.encodeToTlv()) + Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.26"), 1337.encodeToAsn1Primitive()) val attr12 = - Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.27"), 1337.encodeToTlv()) + Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.27"), 1337.encodeToAsn1Primitive()) val attr13 = - Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.26"), 1338.encodeToTlv()) + Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.26"), 1338.encodeToAsn1Primitive()) val attr2 = Pkcs10CertificationRequestAttribute(KnownOIDs.keyUsage, Asn1Element.parse(keyUsage.encoded)) val attr3 = Pkcs10CertificationRequestAttribute(KnownOIDs.extKeyUsage, Asn1Element.parse(extendedKeyUsage.encoded)) diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/PublicKeyTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/PublicKeyTest.kt index c72c1e48..9584ae78 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/PublicKeyTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/PublicKeyTest.kt @@ -3,7 +3,7 @@ package at.asitplus.signum.indispensable import at.asitplus.KmmResult.Companion.wrap import at.asitplus.signum.indispensable.asn1.Asn1Element import at.asitplus.signum.indispensable.asn1.Asn1Sequence -import at.asitplus.signum.indispensable.asn1.parse +import at.asitplus.signum.indispensable.asn1.encoding.parse import at.asitplus.signum.indispensable.io.Base64Strict import io.kotest.assertions.withClue import io.kotest.core.spec.style.FreeSpec diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagEncodingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagEncodingTest.kt index a6cb689c..982541ee 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagEncodingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagEncodingTest.kt @@ -3,6 +3,7 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.* import io.kotest.assertions.withClue import io.kotest.core.spec.style.FreeSpec import io.kotest.datatest.withData @@ -21,7 +22,7 @@ class TagEncodingTest : FreeSpec({ val it = 2204309167L val bytes = (it).toTwosComplementByteArray() val fromBC = ASN1Integer(it).encoded - val long = Long.decodeFromDerValue(bytes) + val long = Long.decodeFromAsn1ContentBytes(bytes) val encoded = Asn1.Int(it).derEncoded encoded shouldBe fromBC long shouldBe it diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagSortingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagSortingTest.kt index 9e0d74b2..f48cec5a 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagSortingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagSortingTest.kt @@ -2,7 +2,6 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.asn1.Asn1 import at.asitplus.signum.indispensable.asn1.Asn1Element import at.asitplus.signum.indispensable.asn1.TagClass import io.kotest.core.spec.style.FreeSpec 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 4b8ea504..a191e4c2 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UVarIntTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UVarIntTest.kt @@ -1,8 +1,8 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.asn1.decodeAsn1VarUInt -import at.asitplus.signum.indispensable.asn1.decodeAsn1VarULong -import at.asitplus.signum.indispensable.asn1.toAsn1VarInt +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 io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe import io.kotest.property.Arb diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UtilTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UtilTest.kt index cd5cc169..bf2a56d5 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UtilTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/UtilTest.kt @@ -1,6 +1,6 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.asn1.ensureSize +import at.asitplus.signum.indispensable.io.ensureSize import at.asitplus.signum.indispensable.misc.BitLength import at.asitplus.signum.indispensable.misc.max import at.asitplus.signum.indispensable.misc.min diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt index 2206a2be..8c10059d 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt @@ -1,6 +1,8 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.parse +import at.asitplus.signum.indispensable.asn1.encoding.parseFirst import at.asitplus.signum.indispensable.pki.X509Certificate import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue @@ -22,8 +24,11 @@ import java.io.FileReader import java.io.InputStream import java.security.cert.CertificateFactory import java.util.* +import kotlin.random.Random +import kotlin.random.nextInt import java.security.cert.X509Certificate as JcaCertificate +internal fun ByteIterator.toByteArray(): ByteArray =asSequence().toList().toByteArray() private val json = Json { prettyPrint = true } @@ -34,6 +39,13 @@ class X509CertParserTest : FreeSpec({ val derBytes = javaClass.classLoader.getResourceAsStream("certs/ok-uniqueid-incomplete-byte.der").readBytes() X509Certificate.decodeFromDer(derBytes) + + val garbage = Random.nextBytes(Random.nextInt(0..128)) + val input = (derBytes + garbage).iterator() + Asn1Element.parseFirst(input).let { parsed -> + parsed.derEncoded shouldBe derBytes + input.toByteArray() shouldBe garbage + } } @@ -52,6 +64,13 @@ class X509CertParserTest : FreeSpec({ cert.encodeToTlv().derEncoded shouldBe jcaCert.encoded cert shouldBe X509Certificate.decodeFromByteArray(certBytes) + + val garbage = Random.nextBytes(Random.nextInt(0..128)) + val input = (certBytes + garbage).iterator() + Asn1Element.parseFirst(input).let { parsed -> + parsed.derEncoded shouldBe certBytes + input.toByteArray() shouldBe garbage + } } } } @@ -106,6 +125,13 @@ class X509CertParserTest : FreeSpec({ ) { own shouldBe crt.encoded parsed shouldBe X509Certificate.decodeFromByteArray(crt.encoded) + + val garbage = Random.nextBytes(Random.nextInt(0..128)) + val bytes = (crt.encoded + garbage).iterator() + Asn1Element.parseFirst(bytes).let { parsed -> + parsed.derEncoded shouldBe own + bytes.toByteArray() shouldBe garbage + } } } } @@ -122,6 +148,13 @@ class X509CertParserTest : FreeSpec({ val src = Asn1Element.parse(it.second) as Asn1Sequence val decoded = X509Certificate.decodeFromTlv(src) decoded shouldBe X509Certificate.decodeFromByteArray(it.second) + + val garbage = Random.nextBytes(Random.nextInt(0..128)) + val bytes = (it.second + garbage).iterator() + Asn1Element.parseFirst(bytes).let { parsed -> + parsed.derEncoded shouldBe it.second + bytes.toByteArray() shouldBe garbage + } } } "Faulty certs should glitch out" - { @@ -159,6 +192,13 @@ class X509CertParserTest : FreeSpec({ jcaCert.encoded shouldBe encodedSrc cert.encodeToTlv().derEncoded shouldBe encodedSrc + + val garbage = Random.nextBytes(Random.nextInt(0..128)) + val input = (jcaCert.encoded + garbage).iterator() + Asn1Element.parseFirst(input).let { parsed -> + parsed.derEncoded shouldBe jcaCert.encoded + input.asSequence().toList().toByteArray() shouldBe garbage + } } } diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt index f8d3f661..f564fa09 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt @@ -1,6 +1,8 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.io.ensureSize +import at.asitplus.signum.indispensable.asn1.encoding.parse import at.asitplus.signum.indispensable.pki.* import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull diff --git a/settings.gradle.kts b/settings.gradle.kts index 6cca1260..5829f594 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ pluginManagement { repositories { google() + mavenLocal() mavenCentral() maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //KOTEST snapshot gradlePluginPortal() @@ -15,6 +16,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + mavenLocal() } } diff --git a/supreme/build.gradle.kts b/supreme/build.gradle.kts index 5dfed4d8..8c225d76 100644 --- a/supreme/build.gradle.kts +++ b/supreme/build.gradle.kts @@ -156,8 +156,8 @@ signing { val signingKeyId: String? by project val signingKey: String? by project val signingPassword: String? by project - useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) - sign(publishing.publications) + // useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + // sign(publishing.publications) } diff --git a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/sign/VerifierImpl.kt b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/sign/VerifierImpl.kt index 208740ab..2d33bb57 100644 --- a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/sign/VerifierImpl.kt +++ b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/sign/VerifierImpl.kt @@ -5,7 +5,7 @@ import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.CryptoSignature import at.asitplus.signum.indispensable.ECCurve import at.asitplus.signum.indispensable.SignatureAlgorithm -import at.asitplus.signum.indispensable.asn1.ensureSize +import at.asitplus.signum.indispensable.io.ensureSize import at.asitplus.signum.indispensable.iosEncoded import at.asitplus.signum.indispensable.nativeDigest import at.asitplus.signum.indispensable.secKeyAlgorithmPreHashed