From 05a0a443878b494b0c73e51abf3962b2c1b385a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 16 Sep 2024 17:39:04 +0200 Subject: [PATCH 01/20] Consume only the first ASN.1 Element when parsing --- CHANGELOG.md | 2 ++ .../signum/indispensable/asn1/Asn1Decoding.kt | 35 ++++++++++++++++--- .../indispensable/X509CertParserTest.kt | 32 +++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d72dc1..9ff46e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ * `Iterable.decodeAsn1VarUInt()` * `ByteArray.decodeAsn1VarUInt()` * Revamp implicit tagging +* Consume only the first `Asn1Element.parse()` only consumes the first parsable element and + `Asn1Element.parserWithRemainder()` additionally returns the remaining bytes for convenience ## 3.0 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 index 8bb3e1e2..d384b4ec 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Decoding.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Decoding.kt @@ -15,6 +15,20 @@ import kotlinx.datetime.Instant import kotlin.experimental.and import kotlin.math.ceil +/** + * Result of parsing a single, toplevel [Asn1Element] from a bytearray + */ +typealias Asn1Parsed = Pair + +/** + * The parsed [Asn1Element] + */ +val Asn1Parsed.element get() = first + +/** + * The remainder of the underlying bytearray (empty if, everything was consumed) + */ +val Asn1Parsed.remainingBytes get() = second /** * Parses the provides [input] into a single [Asn1Element] @@ -23,17 +37,30 @@ import kotlin.math.ceil * @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 { +fun Asn1Element.Companion.parse(input: ByteArray): Asn1Element = Asn1Reader(input).doParse(single = true).let { if (it.size != 1) throw Asn1StructuralException("Multiple ASN.1 structures found") it.first() } -private class Asn1Reader(input: ByteArray) { +/** + * Parses the provides [input] into a single [Asn1Element] + * @return the [Asn1Parsed] containing an element and remaining bytes + * + * @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.parseWithRemainder(input: ByteArray): Asn1Parsed = parse(input).let { + it to input.drop(it.overallLength).toByteArray() +} + + +private class Asn1Reader(private val input: ByteArray) { private var rest = input @Throws(Asn1Exception::class) - fun doParse(): List = runRethrowing { + fun doParse(single: Boolean = false): List = runRethrowing { val result = mutableListOf() while (rest.isNotEmpty()) { val tlv = read() @@ -54,7 +81,7 @@ private class Asn1Reader(input: ByteArray) { } 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)) - + if (single) return result } return result } 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..e4e1a7a3 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt @@ -22,6 +22,8 @@ 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 @@ -34,6 +36,12 @@ 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)) + Asn1Element.parseWithRemainder(derBytes + garbage).let { (parsed, remainder) -> + parsed.derEncoded shouldBe derBytes + remainder shouldBe garbage + } } @@ -52,6 +60,12 @@ class X509CertParserTest : FreeSpec({ cert.encodeToTlv().derEncoded shouldBe jcaCert.encoded cert shouldBe X509Certificate.decodeFromByteArray(certBytes) + + val garbage = Random.nextBytes(Random.nextInt(0..128)) + Asn1Element.parseWithRemainder(certBytes + garbage).let { (parsed, remainder) -> + parsed.derEncoded shouldBe certBytes + remainder shouldBe garbage + } } } } @@ -106,6 +120,12 @@ class X509CertParserTest : FreeSpec({ ) { own shouldBe crt.encoded parsed shouldBe X509Certificate.decodeFromByteArray(crt.encoded) + + val garbage = Random.nextBytes(Random.nextInt(0..128)) + Asn1Element.parseWithRemainder(crt.encoded + garbage).let { (parsed, remainder) -> + parsed.derEncoded shouldBe own + remainder shouldBe garbage + } } } } @@ -122,6 +142,12 @@ 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)) + Asn1Element.parseWithRemainder(it.second + garbage).let { (parsed, remainder) -> + parsed.derEncoded shouldBe it.second + remainder shouldBe garbage + } } } "Faulty certs should glitch out" - { @@ -159,6 +185,12 @@ class X509CertParserTest : FreeSpec({ jcaCert.encoded shouldBe encodedSrc cert.encodeToTlv().derEncoded shouldBe encodedSrc + + val garbage = Random.nextBytes(Random.nextInt(0..128)) + Asn1Element.parseWithRemainder(jcaCert.encoded + garbage).let { (parsed, remainder) -> + parsed.derEncoded shouldBe jcaCert.encoded + remainder shouldBe garbage + } } } From 1a15120f57cb448c248ea23a1db6a22385be4ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 17 Sep 2024 13:25:22 +0200 Subject: [PATCH 02/20] documentation fixes --- README.md | 30 +++++++++++++------ .../signum/indispensable/asn1/Asn1Elements.kt | 3 +- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 495fcde6..3b385a11 100644 --- a/README.md +++ b/README.md @@ -384,31 +384,43 @@ Which results in the following output: 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`. +`Asn1Element`. +In addition, `Asn1Element.parseWithRemainder(derBytes)` returns both the parsed ASN.1 element from the passed bytes' start +and the remaining bytes. 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. **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 +To also support going the other way, the companion objects of these complex classes implement `Asn1Decodable`, which allows for -* directly parsing DER-encoded byte arrays by calling `.decodeFromDer(bytes)` -* processing an `Asn1Element` by calling `.fromTlv(src)` +* 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 #### Decoding Values 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..124996f3 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 @@ -41,7 +41,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) From bf22c10baf31bb4de7705b8e839aaf1e3718424f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 16 Sep 2024 22:32:04 +0200 Subject: [PATCH 03/20] initial refactorings --- README.md | 24 +- .../signum/indispensable/cosef/CoseKey.kt | 2 +- .../indispensable/cosef/CoseKeyParams.kt | 4 +- .../signum/indispensable/josef/JsonWebKey.kt | 6 +- .../indispensable/josef/JwsExtensions.kt | 13 +- .../indispensable/josef/JsonWebKeyJvmTest.kt | 4 +- .../signum/indispensable/CryptoPublicKey.kt | 10 +- .../signum/indispensable/CryptoSignature.kt | 6 +- .../asitplus/signum/indispensable/ECPoint.kt | 2 +- .../indispensable/X509SignatureAlgorithm.kt | 6 +- .../signum/indispensable/asn1/Asn1Elements.kt | 2 + .../indispensable/asn1/Asn1Encodable.kt | 1 + .../signum/indispensable/asn1/Asn1Encoding.kt | 641 ------------------ .../signum/indispensable/asn1/Asn1String.kt | 3 +- .../signum/indispensable/asn1/Asn1Time.kt | 7 +- .../signum/indispensable/asn1/BERTags.kt | 1 + .../indispensable/asn1/ObjectIdentifier.kt | 3 + .../asn1/{ => encoding}/Asn1Decoding.kt | 231 +++---- .../asn1/encoding/Asn1Encoding.kt | 398 +++++++++++ .../asn1/encoding/NumberEncoding.kt | 335 +++++++++ .../signum/indispensable/io/Encoding.kt | 14 + .../indispensable/pki/AlternativeNames.kt | 1 + .../pki/Pkcs10CertificationRequest.kt | 9 +- .../Pkcs10CertificationRequestAttribute.kt | 1 + .../pki/RelativeDistinguishedName.kt | 4 +- .../indispensable/pki/X509Certificate.kt | 1 + .../pki/X509CertificateExtension.kt | 3 +- .../signum/indispensable/Asn1EncodingTest.kt | 23 +- .../signum/indispensable/Asn1IntegerTest.kt | 7 +- .../signum/indispensable/CastingTest.kt | 2 +- .../indispensable/CryptoSignatureTest.kt | 2 +- .../signum/indispensable/CustomTaggedTest.kt | 1 + .../indispensable/ImplicitTaggingTest.kt | 1 + .../Pkcs10CertificationRequestJvmTest.kt | 14 +- .../signum/indispensable/PublicKeyTest.kt | 2 +- .../signum/indispensable/TagEncodingTest.kt | 3 +- .../signum/indispensable/TagSortingTest.kt | 1 - .../signum/indispensable/UVarIntTest.kt | 6 +- .../asitplus/signum/indispensable/UtilTest.kt | 2 +- .../indispensable/X509CertParserTest.kt | 1 + .../indispensable/X509CertificateJvmTest.kt | 2 + .../signum/supreme/sign/VerifierImpl.kt | 2 +- 42 files changed, 949 insertions(+), 852 deletions(-) delete mode 100644 indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Encoding.kt rename indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/{ => encoding}/Asn1Decoding.kt (61%) create mode 100644 indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Encoding.kt create mode 100644 indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt diff --git a/README.md b/README.md index 3b385a11..2e551daa 100644 --- a/README.md +++ b/README.md @@ -422,20 +422,34 @@ Both encoding and decoding functions come in two _safe_ (i.e. non-throwing) vari * `…Safe()` which returns a [KmmResult](https://github.com/a-sit-plus/kmmresult) * `…orNull()` which returns null on error +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) + +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()`, +Various helper functions exist to facilitate decoding the values contained in `Asn1Primitives`, such as `readInt()`, for example. + +Similarly to encoding, a tandem of decoding functions exists for primitives: +* `readXXX` 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(tag: 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/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/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/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/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt index 5fff0e63..bd017efc 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).readAsn1BitString() 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,7 +128,7 @@ 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).readAsn1BitString() 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() 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..0ab6f5a2 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,7 +93,7 @@ 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() @@ -209,7 +211,7 @@ sealed interface CryptoSignature : Asn1Encodable { @Throws(Asn1Exception::class) fun decodeFromTlvBitString(src: Asn1Primitive): EC.IndefiniteLength = runRethrowing { - decodeFromDer(src.readBitString().rawBytes) + decodeFromDer(src.readAsn1BitString().rawBytes) } override fun doDecode(src: Asn1Element): EC.IndefiniteLength { 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..ecbd475d 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.readInt import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind 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 124996f3..c7aa2c32 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,8 @@ 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.asn1.encoding.decodeTag import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray 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..234d82e0 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.readAsn1String 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.readAsn1String() } } \ 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..4da9d701 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.readInstant import kotlinx.datetime.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -35,8 +38,8 @@ class Asn1Time(instant: Instant, formatOverride: Format? = null) : Asn1Encodable 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/Asn1Decoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt similarity index 61% rename from indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Decoding.kt rename to indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt index d384b4ec..eb270232 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Decoding.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt @@ -1,6 +1,7 @@ -package at.asitplus.signum.indispensable.asn1 +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 @@ -105,23 +106,14 @@ private class Asn1Reader(private val input: ByteArray) { * @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!") - } - } -} +fun Asn1Primitive.readBool() = runRethrowing { decode(Asn1Element.Tag.BOOL) { Boolean.decodeFromAsn1ContentBytes(it) } } /** * 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) } } +fun Asn1Primitive.readInt() = runRethrowing { decode(Asn1Element.Tag.INT) { Int.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [readInt] */ fun Asn1Primitive.readIntOrNull() = runCatching { readInt() }.getOrNull() @@ -131,7 +123,7 @@ fun Asn1Primitive.readIntOrNull() = runCatching { readInt() }.getOrNull() * @throws [Asn1Exception] on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.readLong() = runRethrowing { decode(Asn1Element.Tag.INT) { Long.decodeFromDerValue(it) } } +fun Asn1Primitive.readLong() = runRethrowing { decode(Asn1Element.Tag.INT) { Long.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [readLong] */ inline fun Asn1Primitive.readLongOrNull() = runCatching { readLong() }.getOrNull() @@ -141,7 +133,7 @@ inline fun Asn1Primitive.readLongOrNull() = runCatching { readLong() }.getOrNull * @throws [Asn1Exception] on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.readUInt() = runRethrowing { decode(Asn1Element.Tag.INT) { UInt.decodeFromDerValue(it) } } +fun Asn1Primitive.readUInt() = runRethrowing { decode(Asn1Element.Tag.INT) { UInt.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [readUInt] */ inline fun Asn1Primitive.readUIntOrNull() = runCatching { readUInt() }.getOrNull() @@ -151,7 +143,7 @@ inline fun Asn1Primitive.readUIntOrNull() = runCatching { readUInt() }.getOrNull * @throws [Asn1Exception] on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.readULong() = runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromDerValue(it) } } +fun Asn1Primitive.readULong() = runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [readULong] */ inline fun Asn1Primitive.readULongOrNull() = runCatching { readULong() }.getOrNull() @@ -159,35 +151,42 @@ inline fun Asn1Primitive.readULongOrNull() = runCatching { readULong() }.getOrNu /** 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) } } +fun Asn1Primitive.readBigInteger() = + runRethrowing { decode(Asn1Element.Tag.INT) { BigInteger.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [readBigInteger] */ inline fun Asn1Primitive.readBigIntegerOrNull() = runCatching { readBigInteger() }.getOrNull() /** - * decodes this [Asn1Primitive]'s content into an [Asn1String] + * 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.readString(): Asn1String = runRethrowing { +fun Asn1Primitive.readAsn1String(): 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()) + 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") } } /** - * Exception-free version of [readString] + * Decodes this [Asn1Primitive]'s content into a String. + * @throws [Asn1Exception] all sorts of exceptions on invalid input */ -fun Asn1Primitive.readStringOrNull() = catching { readString() }.getOrNull() +fun Asn1Primitive.readString() = runRethrowing {readAsn1String().value} + +/** + * Exception-free version of [readAsn1String] + */ +fun Asn1Primitive.readAsn1StringOrNull() = catching { readAsn1String() }.getOrNull() /** @@ -198,10 +197,10 @@ fun Asn1Primitive.readStringOrNull() = catching { readString() }.getOrNull() @Throws(Asn1Exception::class) fun Asn1Primitive.readInstant() = when (tag) { - Asn1Element.Tag.TIME_UTC -> decode(Asn1Element.Tag.TIME_UTC, Instant.Companion::decodeUtcTimeFromDer) + Asn1Element.Tag.TIME_UTC -> decode(Asn1Element.Tag.TIME_UTC, Instant.Companion::decodeUtcTimeFromAsn1ContentBytes) Asn1Element.Tag.TIME_GENERALIZED -> decode( Asn1Element.Tag.TIME_GENERALIZED, - Instant.Companion::decodeGeneralizedTimeFromDer + Instant.Companion::decodeGeneralizedTimeFromAsn1ContentBytes ) else -> TODO("Support time tag $tag") @@ -214,17 +213,16 @@ fun Asn1Primitive.readInstantOrNull() = catching { readInstant() }.getOrNull() /** - * decodes this [Asn1Primitive]'s content into a [ByteArray], assuming it was encoded as BIT STRING - * + * Transforms this [Asn1Primitive]' into an [Asn1BitString], assuming it was encoded as BIT STRING * @throws Asn1Exception on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.readBitString() = Asn1BitString.decodeFromTlv(this) +fun Asn1Primitive.readAsn1BitString() = Asn1BitString.decodeFromTlv(this, Asn1Element.Tag.BIT_STRING) /** - * Exception-free version of [readBitString] + * Exception-free version of [readAsn1BitString] */ -fun Asn1Primitive.readBitStringOrNull() = catching { readBitString() }.getOrNull() +fun Asn1Primitive.readAsn1BitStringOrNull() = catching { readAsn1BitString() }.getOrNull() /** @@ -241,7 +239,6 @@ fun Asn1Primitive.readNull() = decode(Asn1Element.Tag.NULL) {} fun Asn1Primitive.readNullOrNull() = catching { readNull() }.getOrNull() - /** * Generic decoding function. Verifies that this [Asn1Primitive]'s tag matches [tag] * and transforms its content as per [transform] @@ -270,8 +267,12 @@ inline fun Asn1Primitive.decode(tag: Asn1Element.Tag, transform: (co 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) -private fun Instant.Companion.decodeUtcTimeFromDer(input: ByteArray): Instant = runRethrowing { +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() @@ -286,10 +287,14 @@ private fun Instant.Companion.decodeUtcTimeFromDer(input: ByteArray): Instant = 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) -private fun Instant.Companion.decodeGeneralizedTimeFromDer(input: ByteArray): Instant = runRethrowing { - val s = input.decodeToString() - if (s.length != 15) throw IllegalArgumentException("Input too short: $input") +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 @@ -300,26 +305,63 @@ private fun Instant.Companion.decodeGeneralizedTimeFromDer(input: ByteArray): In return parse(isoString) } -/** @throws Asn1Exception if the byte array is out of bounds for a signed int */ +/** + * 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.decodeFromDerValue(bytes: ByteArray): Int = runRethrowing { fromTwosComplementByteArray(bytes) } +fun Int.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): Int = + runRethrowing { fromTwosComplementByteArray(bytes) } -/** @throws Asn1Exception if the byte array is out of bounds for a signed long */ +/** + * 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.decodeFromDerValue(bytes: ByteArray): Long = runRethrowing { fromTwosComplementByteArray(bytes) } +fun Long.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): Long = + runRethrowing { fromTwosComplementByteArray(bytes) } -/** @throws Asn1Exception if the byte array is out of bounds for an unsigned int */ +/** + * 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.decodeFromDerValue(bytes: ByteArray): UInt = runRethrowing { fromTwosComplementByteArray(bytes) } +fun UInt.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): UInt = + runRethrowing { fromTwosComplementByteArray(bytes) } -/** @throws Asn1Exception if the byte array is out of bounds for an unsigned long */ +/** + * 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.decodeFromDerValue(bytes: ByteArray): ULong = runRethrowing { fromTwosComplementByteArray(bytes) } +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.decodeFromDerValue(bytes: ByteArray): BigInteger = +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() + @Throws(Asn1Exception::class) private fun ByteArray.readTlv(): TLV = runRethrowing { @@ -367,94 +409,3 @@ internal fun ByteIterator.decodeTag(): Pair = } } } - - -/** - * 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/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..2e6a37bf 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 @@ -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..d9dda812 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.readAsn1BitString +import at.asitplus.signum.indispensable.asn1.encoding.readInt import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer import kotlinx.serialization.Serializable @@ -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).readAsn1BitString() 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..df9cd6ed 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.readAsn1String 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).readAsn1String() } 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..013cee68 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 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..d0e99c38 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 @@ -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 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..3b766aff 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.readBigInteger import com.ionspin.kotlin.bignum.integer.BigInteger import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe @@ -8,12 +11,12 @@ 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" { 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 e4e1a7a3..aa60422a 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,7 @@ package at.asitplus.signum.indispensable import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.parse import at.asitplus.signum.indispensable.pki.X509Certificate import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue 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/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 From 9c034d3f9f35cec6ba7562fa935b4fd79ed4b1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 16 Sep 2024 22:41:39 +0200 Subject: [PATCH 04/20] refactor decoding functions --- README.md | 2 +- .../signum/indispensable/CryptoPublicKey.kt | 8 +-- .../signum/indispensable/CryptoSignature.kt | 6 +- .../indispensable/X509SignatureAlgorithm.kt | 4 +- .../signum/indispensable/asn1/Asn1String.kt | 4 +- .../signum/indispensable/asn1/Asn1Time.kt | 4 +- .../asn1/encoding/Asn1Decoding.kt | 61 +++++++++---------- .../pki/Pkcs10CertificationRequest.kt | 8 +-- .../pki/RelativeDistinguishedName.kt | 4 +- .../indispensable/pki/X509Certificate.kt | 2 +- .../signum/indispensable/Asn1EncodingTest.kt | 26 ++++---- .../signum/indispensable/Asn1IntegerTest.kt | 6 +- 12 files changed, 65 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 2e551daa..9b3c7e42 100644 --- a/README.md +++ b/README.md @@ -438,7 +438,7 @@ Various helper functions exist to facilitate decoding the values contained in `A for example. Similarly to encoding, a tandem of decoding functions exists for primitives: -* `readXXX` to be invoked on an `Asn1Primitive` to decode a DER-encoded primitive into the target type +* `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 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 bd017efc..ff4d4fe6 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt @@ -117,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).readAsn1BitString() + 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() @@ -128,10 +128,10 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { Rsa.oid -> { (keyInfo.nextChild() as Asn1Primitive).readNull() - val bitString = (src.nextChild() as Asn1Primitive).readAsn1BitString() + 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) } @@ -285,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 0ab6f5a2..582df721 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt @@ -211,13 +211,13 @@ sealed interface CryptoSignature : Asn1Encodable { @Throws(Asn1Exception::class) fun decodeFromTlvBitString(src: Asn1Primitive): EC.IndefiniteLength = runRethrowing { - decodeFromDer(src.readAsn1BitString().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/X509SignatureAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt index ecbd475d..7b2c8d1b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt @@ -5,7 +5,7 @@ import at.asitplus.signum.indispensable.asn1.* 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.readInt +import at.asitplus.signum.indispensable.asn1.encoding.decodeToInt import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -152,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/Asn1String.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1String.kt index 234d82e0..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,6 +1,6 @@ package at.asitplus.signum.indispensable.asn1 -import at.asitplus.signum.indispensable.asn1.encoding.readAsn1String +import at.asitplus.signum.indispensable.asn1.encoding.asAsn1String import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -120,6 +120,6 @@ sealed class Asn1String : Asn1Encodable { companion object : Asn1Decodable { @Throws(Asn1Exception::class) - override fun doDecode(src: Asn1Primitive): Asn1String = src.readAsn1String() + 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 4da9d701..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 @@ -2,7 +2,7 @@ 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.readInstant +import at.asitplus.signum.indispensable.asn1.encoding.decodeToInstant import kotlinx.datetime.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -33,7 +33,7 @@ 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 = 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 index eb270232..b909c6bc 100644 --- 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 @@ -14,7 +14,6 @@ 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 /** * Result of parsing a single, toplevel [Asn1Element] from a bytearray @@ -106,56 +105,59 @@ private class Asn1Reader(private val input: ByteArray) { * @throws [Asn1Exception] all sorts of exceptions on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.readBool() = runRethrowing { decode(Asn1Element.Tag.BOOL) { Boolean.decodeFromAsn1ContentBytes(it) } } +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.readInt() = runRethrowing { decode(Asn1Element.Tag.INT) { Int.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToInt() = runRethrowing { decode(Asn1Element.Tag.INT) { Int.decodeFromAsn1ContentBytes(it) } } -/** Exception-free version of [readInt] */ -fun Asn1Primitive.readIntOrNull() = runCatching { readInt() }.getOrNull() +/** 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.readLong() = runRethrowing { decode(Asn1Element.Tag.INT) { Long.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToLong() = runRethrowing { decode(Asn1Element.Tag.INT) { Long.decodeFromAsn1ContentBytes(it) } } -/** Exception-free version of [readLong] */ -inline fun Asn1Primitive.readLongOrNull() = runCatching { readLong() }.getOrNull() +/** 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.readUInt() = runRethrowing { decode(Asn1Element.Tag.INT) { UInt.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToUInt() = runRethrowing { decode(Asn1Element.Tag.INT) { UInt.decodeFromAsn1ContentBytes(it) } } -/** Exception-free version of [readUInt] */ -inline fun Asn1Primitive.readUIntOrNull() = runCatching { readUInt() }.getOrNull() +/** 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.readULong() = runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToULong() = runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromAsn1ContentBytes(it) } } -/** Exception-free version of [readULong] */ -inline fun Asn1Primitive.readULongOrNull() = runCatching { readULong() }.getOrNull() +/** 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.readBigInteger() = +fun Asn1Primitive.decodeToBigInteger() = runRethrowing { decode(Asn1Element.Tag.INT) { BigInteger.decodeFromAsn1ContentBytes(it) } } -/** Exception-free version of [readBigInteger] */ -inline fun Asn1Primitive.readBigIntegerOrNull() = runCatching { readBigInteger() }.getOrNull() +/** Exception-free version of [decodeToBigInteger] */ +inline fun Asn1Primitive.decodeToBigIntegerOrNull() = runCatching { decodeToBigInteger() }.getOrNull() /** * transforms this [Asn1Primitive] into an [Asn1String] subtype based on its tag @@ -163,7 +165,7 @@ inline fun Asn1Primitive.readBigIntegerOrNull() = runCatching { readBigInteger() * @throws [Asn1Exception] all sorts of exceptions on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.readAsn1String(): Asn1String = runRethrowing { +fun Asn1Primitive.asAsn1String(): Asn1String = runRethrowing { when (tag.tagValue) { UTF8_STRING.toULong() -> Asn1String.UTF8(String.decodeFromAsn1ContentBytes(content)) UNIVERSAL_STRING.toULong() -> Asn1String.Universal(String.decodeFromAsn1ContentBytes(content)) @@ -181,12 +183,11 @@ fun Asn1Primitive.readAsn1String(): Asn1String = runRethrowing { * Decodes this [Asn1Primitive]'s content into a String. * @throws [Asn1Exception] all sorts of exceptions on invalid input */ -fun Asn1Primitive.readString() = runRethrowing {readAsn1String().value} +fun Asn1Primitive.decodeToString() = runRethrowing {asAsn1String().value} + +/** Exception-free version of [decodeToString] */ +fun Asn1Primitive.decodeToStringOrNull() = runCatching { decodeToString() }.getOrNull() -/** - * Exception-free version of [readAsn1String] - */ -fun Asn1Primitive.readAsn1StringOrNull() = catching { readAsn1String() }.getOrNull() /** @@ -195,7 +196,7 @@ fun Asn1Primitive.readAsn1StringOrNull() = catching { readAsn1String() }.getOrNu * @throws Asn1Exception on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.readInstant() = +fun Asn1Primitive.decodeToInstant() = when (tag) { Asn1Element.Tag.TIME_UTC -> decode(Asn1Element.Tag.TIME_UTC, Instant.Companion::decodeUtcTimeFromAsn1ContentBytes) Asn1Element.Tag.TIME_GENERALIZED -> decode( @@ -207,9 +208,9 @@ fun Asn1Primitive.readInstant() = } /** - * Exception-free version of [readInstant] + * Exception-free version of [decodeToInstant] */ -fun Asn1Primitive.readInstantOrNull() = catching { readInstant() }.getOrNull() +fun Asn1Primitive.decodeToInstantOrNull() = catching { decodeToInstant() }.getOrNull() /** @@ -217,13 +218,7 @@ fun Asn1Primitive.readInstantOrNull() = catching { readInstant() }.getOrNull() * @throws Asn1Exception on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.readAsn1BitString() = Asn1BitString.decodeFromTlv(this, Asn1Element.Tag.BIT_STRING) - -/** - * Exception-free version of [readAsn1BitString] - */ -fun Asn1Primitive.readAsn1BitStringOrNull() = catching { readAsn1BitString() }.getOrNull() - +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 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 d9dda812..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 @@ -6,8 +6,8 @@ import at.asitplus.signum.indispensable.asn1.* 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.readAsn1BitString -import at.asitplus.signum.indispensable.asn1.encoding.readInt +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 @@ -82,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) } @@ -150,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).readAsn1BitString() + 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/RelativeDistinguishedName.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt index df9cd6ed..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 @@ -3,7 +3,7 @@ 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.readAsn1String +import at.asitplus.signum.indispensable.asn1.encoding.asAsn1String import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -125,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).readAsn1String() } + 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 013cee68..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 @@ -146,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/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1EncodingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1EncodingTest.kt index d0e99c38..a2334d56 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1EncodingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1EncodingTest.kt @@ -33,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 } } @@ -105,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 @@ -128,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 @@ -150,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 @@ -172,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 3b766aff..2e4cbbf7 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1IntegerTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1IntegerTest.kt @@ -3,7 +3,7 @@ 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.readBigInteger +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 @@ -22,13 +22,13 @@ class Asn1IntegerTest : FreeSpec({ "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) } }) From f575050664559c3b8b606c2310df88823df7c409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 17 Sep 2024 12:37:55 +0200 Subject: [PATCH 05/20] fix compilation bugs --- build.gradle.kts | 2 +- .../signum/indispensable/josef/io/JwsCertificateSerializer.kt | 2 +- .../kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt | 2 +- .../kotlin/at/asitplus/signum/indispensable/io/Encoding.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e7270272..be68fef8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ 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+20240905" id("com.android.library") version "8.2.2" apply (false) } group = "at.asitplus.signum" 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..ddd282b6 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 ) 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 582df721..c469f63e 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt @@ -95,7 +95,7 @@ sealed interface CryptoSignature : Asn1Encodable { 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. 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 2e6a37bf..444a6443 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 ) /** De-/serializes a public key as a Base64Url-encoded IOS encoding public key */ From 7dc3a030171901449286d38b5519fe8e73d9c876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 17 Sep 2024 12:41:11 +0200 Subject: [PATCH 06/20] compilation fixes + readme --- CHANGELOG.md | 5 +++++ .../indispensable/josef/io/JwsCertificateSerializer.kt | 2 +- .../kotlin/at/asitplus/signum/indispensable/io/Encoding.kt | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff46e68..a7cd5f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,11 @@ * Revamp implicit tagging * Consume only the first `Asn1Element.parse()` only consumes the first parsable element and `Asn1Element.parserWithRemainder()` additionally returns the remaining bytes for convenience +* 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) ## 3.0 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 ddd282b6..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(it) } //workaround iOS compilation bug + decodeAs = { X509Certificate.decodeFromDer(it) } //workaround iOS compilation bug KT-71498 ) 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 444a6443..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(it) } // workaround iOS compilation bug + decodeAs = { X509Certificate.decodeFromDer(it) } // workaround iOS compilation bug KT-71498 ) /** De-/serializes a public key as a Base64Url-encoded IOS encoding public key */ From 5cec5a57bf9b64a3031af9f4e2fcd70b66de8bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 17 Sep 2024 15:04:54 +0200 Subject: [PATCH 07/20] readme+dependency updates --- CHANGELOG.md | 1 + README.md | 20 +++++++++---------- build.gradle.kts | 2 +- .../indispensable/X509CertParserTest.kt | 1 + 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7cd5f94..04984a7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ * `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 9b3c7e42..6fb8b9b4 100644 --- a/README.md +++ b/README.md @@ -412,16 +412,6 @@ Any complex data structure (such as CSR, public key, certificate, …) implement * encapsulate it into an ASN.1 Tree by calling `.encodeToTlv()` * directly get a DER-encoded byte array through the `.encodetoDer()` function -To also support going the other way, the companion objects of these complex classes 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 - A tandem of helper functions is available for primitives (numbers, booleans, string, bigints): * `encodeToAsn1Primitive` to produce an `Asn1Primitive` that can directly be DER-encoded @@ -435,7 +425,15 @@ list of helper functions. #### Decoding Values Various helper functions exist to facilitate decoding the values contained in `Asn1Primitives`, such as `readInt()`, -for example. +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 diff --git a/build.gradle.kts b/build.gradle.kts index be68fef8..b1d87dd2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.dokka.gradle.DokkaMultiModuleTask plugins { - id("at.asitplus.gradle.conventions") version "2.0.20+20240905" + id("at.asitplus.gradle.conventions") version "2.0.20+20240917" id("com.android.library") version "8.2.2" apply (false) } group = "at.asitplus.signum" 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 aa60422a..05dd76e8 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt @@ -2,6 +2,7 @@ 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.parseWithRemainder import at.asitplus.signum.indispensable.pki.X509Certificate import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue From 4ef4e88efd320a649c69bad34703f5d6ba945dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 17 Sep 2024 15:18:33 +0200 Subject: [PATCH 08/20] force snapshot publishing --- build.gradle.kts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index b1d87dd2..32c0d9d6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,10 +26,14 @@ 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) } } } +nexusPublishing { + useStaging = true //TODO: why is this needed? +} + allprojects { apply(plugin = "org.jetbrains.dokka") group = rootProject.group From 74c2fab9245fc1f0d05a2606b4ed0210044f007c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 17 Sep 2024 15:24:57 +0200 Subject: [PATCH 09/20] remove non-functional config --- build.gradle.kts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 32c0d9d6..33aedbc8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,10 +30,6 @@ tasks.getByName("dokkaHtmlMultiModule") { } } -nexusPublishing { - useStaging = true //TODO: why is this needed? -} - allprojects { apply(plugin = "org.jetbrains.dokka") group = rootProject.group From de8384fc8a94d1d402ec83816553e3359f61bd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 17 Sep 2024 21:55:11 +0200 Subject: [PATCH 10/20] add snapshot version to README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6fb8b9b4..29df7c07 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/) From 605d27760d47468f7401b155c3461d2a42a4cc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 17 Sep 2024 22:13:57 +0200 Subject: [PATCH 11/20] work around nexus publish bug --- build.gradle.kts | 5 +++++ indispensable-cosef/build.gradle.kts | 4 ++-- indispensable-josef/build.gradle.kts | 4 ++-- indispensable/build.gradle.kts | 4 ++-- settings.gradle.kts | 2 ++ supreme/build.gradle.kts | 4 ++-- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 33aedbc8..45dfce17 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,11 @@ plugins { } 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") diff --git a/indispensable-cosef/build.gradle.kts b/indispensable-cosef/build.gradle.kts index f2be9fb0..f3057fd6 100644 --- a/indispensable-cosef/build.gradle.kts +++ b/indispensable-cosef/build.gradle.kts @@ -107,6 +107,6 @@ 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/indispensable-josef/build.gradle.kts b/indispensable-josef/build.gradle.kts index 41484642..41e91829 100644 --- a/indispensable-josef/build.gradle.kts +++ b/indispensable-josef/build.gradle.kts @@ -114,6 +114,6 @@ 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/indispensable/build.gradle.kts b/indispensable/build.gradle.kts index 32879141..9936bb24 100644 --- a/indispensable/build.gradle.kts +++ b/indispensable/build.gradle.kts @@ -263,6 +263,6 @@ 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/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) } From e44bc55427475068bd69d9d99b191575bcfae163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 19 Sep 2024 17:09:25 +0200 Subject: [PATCH 12/20] address pr feedback --- CHANGELOG.md | 2 +- indispensable-cosef/build.gradle.kts | 4 ++-- indispensable-josef/build.gradle.kts | 4 ++-- indispensable/build.gradle.kts | 4 ++-- .../indispensable/asn1/encoding/Asn1Decoding.kt | 14 +++++++------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04984a7c..ea3d9a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ * `ByteArray.decodeAsn1VarUInt()` * Revamp implicit tagging * Consume only the first `Asn1Element.parse()` only consumes the first parsable element and - `Asn1Element.parserWithRemainder()` additionally returns the remaining bytes for convenience + `Asn1Element.parseWithRemainder()` additionally returns the remaining bytes for convenience * 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) diff --git a/indispensable-cosef/build.gradle.kts b/indispensable-cosef/build.gradle.kts index f3057fd6..f2be9fb0 100644 --- a/indispensable-cosef/build.gradle.kts +++ b/indispensable-cosef/build.gradle.kts @@ -107,6 +107,6 @@ 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/indispensable-josef/build.gradle.kts b/indispensable-josef/build.gradle.kts index 41e91829..41484642 100644 --- a/indispensable-josef/build.gradle.kts +++ b/indispensable-josef/build.gradle.kts @@ -114,6 +114,6 @@ 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/indispensable/build.gradle.kts b/indispensable/build.gradle.kts index 9936bb24..32879141 100644 --- a/indispensable/build.gradle.kts +++ b/indispensable/build.gradle.kts @@ -263,6 +263,6 @@ 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/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 index b909c6bc..aaef583a 100644 --- 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 @@ -235,24 +235,24 @@ fun Asn1Primitive.readNullOrNull() = catching { readNull() }.getOrNull() /** - * Generic decoding function. Verifies that this [Asn1Primitive]'s tag matches [tag] + * 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(tag: ULong, transform: (content: ByteArray) -> T): T = - decode(Asn1Element.Tag(tag, constructed = false), transform) +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 [tag] + * 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(tag: Asn1Element.Tag, transform: (content: ByteArray) -> T) = +inline fun Asn1Primitive.decode(assertTag: 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) + if (assertTag.isConstructed) throw IllegalArgumentException("A primitive cannot have a CONSTRUCTED tag") + if (assertTag != this.tag) throw Asn1TagMismatchException(assertTag, this.tag) transform(content) } From 43108e699ddba39b90aff3494d91ccb2400e7a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 19 Sep 2024 20:56:39 +0200 Subject: [PATCH 13/20] revamp api some more --- CHANGELOG.md | 7 +- README.md | 17 ++-- build.gradle.kts | 2 +- gradle/libs.versions.toml | 1 + indispensable-cosef/build.gradle.kts | 1 + indispensable-josef/build.gradle.kts | 1 + indispensable/build.gradle.kts | 1 + .../signum/indispensable/CryptoPublicKey.kt | 2 +- .../signum/indispensable/asn1/Asn1Elements.kt | 1 - .../asn1/encoding/Asn1Decoding.kt | 84 +++++++++---------- .../signum/indispensable/Asn1ParserTest.kt | 67 +++++++++++++++ .../indispensable/X509CertParserTest.kt | 28 ++++--- 12 files changed, 146 insertions(+), 66 deletions(-) create mode 100644 indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3d9a37..6878fe7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,8 +28,11 @@ * `Iterable.decodeAsn1VarUInt()` * `ByteArray.decodeAsn1VarUInt()` * Revamp implicit tagging -* Consume only the first `Asn1Element.parse()` only consumes the first parsable element and - `Asn1Element.parseWithRemainder()` additionally returns the remaining bytes for convenience +* Revamp `Asn1Element.parse()` + * `Asn1Element.parse()` semantics remain the same + * `Asn1Element.parse()` alternative introduced, which takes a `ByteIterator` + * `Asn1Element.parseAll()` introduced, which consumes all bytes and returns a list of all ASN.1 elements (if parsing works) + * `Asn1Element.parseFirst()` introduced, which tries to only parse a single ASN.1 element from the input and leaves the rest untouched. * 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) diff --git a/README.md b/README.md index 29df7c07..7e3937c3 100644 --- a/README.md +++ b/README.md @@ -386,12 +386,17 @@ 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`. -In addition, `Asn1Element.parseWithRemainder(derBytes)` returns both the parsed ASN.1 element from the passed bytes' start -and the remaining bytes. -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(derBytes)`, which will consume all bytes and return the first parsed ASN.1 element. +This method throws if more than a single toplevel ASN.1 Element it found or if any parsing errors occur. +* `Asn1Element.parseFirst(byteIterator)`, 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 frist parsed element. +* `Asn1Element.parseAll(byteIterator)`, wich consumes all bytes, parses all toplevel ASN.1 elements, and returns them as list. +Throws on any parsing error. + +And parsed ASN.1 element 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, just as manually constructed ones can. **Note that decoding operations will throw exceptions if invalid data is provided!** diff --git a/build.gradle.kts b/build.gradle.kts index 45dfce17..fa0544d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.dokka.gradle.DokkaMultiModuleTask plugins { - id("at.asitplus.gradle.conventions") version "2.0.20+20240917" + id("at.asitplus.gradle.conventions") version "2.0.20+20240920" id("com.android.library") version "8.2.2" apply (false) } group = "at.asitplus.signum" 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-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/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 ff4d4fe6..aec09d90 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt @@ -129,7 +129,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { Rsa.oid -> { (keyInfo.nextChild() as Asn1Primitive).readNull() val bitString = (src.nextChild() as Asn1Primitive).asAsn1BitString() - val rsaSequence = Asn1Element.parse(bitString.rawBytes) as Asn1Sequence + val rsaSequence = Asn1Element.parse(bitString.rawBytes.iterator()) as Asn1Sequence val n = (rsaSequence.nextChild() as Asn1Primitive).decode(Asn1Element.Tag.INT) { it } val e = (rsaSequence.nextChild() as Asn1Primitive).decodeToInt() if (rsaSequence.hasMoreChildren()) throw Asn1StructuralException("Superfluous data in SPKI!") 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 c7aa2c32..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 @@ -3,7 +3,6 @@ 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.asn1.encoding.decodeTag import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray 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 index aaef583a..e2bb0fdb 100644 --- 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 @@ -15,71 +15,73 @@ import com.ionspin.kotlin.bignum.integer.util.fromTwosComplementByteArray import kotlinx.datetime.Instant import kotlin.experimental.and -/** - * Result of parsing a single, toplevel [Asn1Element] from a bytearray - */ -typealias Asn1Parsed = Pair /** - * The parsed [Asn1Element] + * 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] */ -val Asn1Parsed.element get() = first - +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parse(source: ByteIterator): Asn1Element = parseAll(source).let{ + if (it.size != 1) throw Asn1StructuralException("Multiple ASN.1 structures found") + it.first() +} /** - * The remainder of the underlying bytearray (empty if, everything was consumed) + * Convenience wrapper around [parse], taking a [ByteArray] as [source] + * @see parse */ -val Asn1Parsed.remainingBytes get() = second +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parse(source: ByteArray): Asn1Element = parse(source.iterator()) /** - * Parses the provides [input] into a single [Asn1Element] - * @return the parsed [Asn1Element] + * 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.parse(input: ByteArray): Asn1Element = Asn1Reader(input).doParse(single = true).let { - if (it.size != 1) throw Asn1StructuralException("Multiple ASN.1 structures found") - it.first() -} +fun Asn1Element.Companion.parseAll(input: ByteIterator): List = Asn1Reader(input).doParse(single = false) + /** - * Parses the provides [input] into a single [Asn1Element] - * @return the [Asn1Parsed] containing an element and remaining bytes + * 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.parseWithRemainder(input: ByteArray): Asn1Parsed = parse(input).let { - it to input.drop(it.overallLength).toByteArray() +fun Asn1Element.Companion.parseFirst(input: ByteIterator): Asn1Element = Asn1Reader(input).doParse(single = true).let { + if (it.isEmpty()) throw Asn1StructuralException("No ASN.1 structures found") + it.first() } -private class Asn1Reader(private val input: ByteArray) { +private class Asn1Reader(private val input: ByteIterator) { - private var rest = input @Throws(Asn1Exception::class) fun doParse(single: Boolean = false): 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())) + while (input.hasNext()) { + val tlv = input.readTlv() + if (tlv.isSequence()) result.add(Asn1Sequence(Asn1Reader(tlv.content.iterator()).doParse())) + else if (tlv.isSet()) result.add(Asn1Set.fromPresorted(Asn1Reader(tlv.content.iterator()).doParse())) else if (tlv.isExplicitlyTagged()) result.add( Asn1ExplicitlyTagged( tlv.tag.tagValue, - Asn1Reader(tlv.content).doParse() + Asn1Reader(tlv.content.iterator()).doParse() ) ) else if (tlv.tag == Asn1Element.Tag.OCTET_STRING) { catching { - result.add(Asn1EncapsulatingOctetString(Asn1Reader(tlv.content).doParse())) + result.add(Asn1EncapsulatingOctetString(Asn1Reader(tlv.content.iterator()).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)) + result.add(Asn1CustomStructure(Asn1Reader(tlv.content.iterator()).doParse(), tlv.tag.tagValue, tlv.tagClass)) } else result.add(Asn1Primitive(tlv.tag, tlv.content)) if (single) return result } @@ -89,15 +91,6 @@ private class Asn1Reader(private val input: ByteArray) { 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 - } } /** @@ -359,15 +352,18 @@ fun String.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray) = bytes.decode @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()) +private fun ByteIterator.readTlv(): TLV = runRethrowing { + if (!hasNext()) throw IllegalArgumentException("Can't read TLV, input empty") + + + + val tag = decodeTag() + + if (!hasNext()) return TLV(Asn1Element.Tag(decodeTag().second), byteArrayOf()) - val iterator = iterator() - val tag = iterator.decodeTag() - val length = iterator.decodeLength() + val length = decodeLength() require(length < 1024 * 1024) { "Heap space" } - val value = with(iterator) { + val value = with(this) { ByteArray(length) { require(hasNext()) { "Out of bytes to decode" } next() 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..d0ce7558 --- /dev/null +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt @@ -0,0 +1,67 @@ +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) + 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) + + 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/X509CertParserTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt index 05dd76e8..8c10059d 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt @@ -2,7 +2,7 @@ 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.parseWithRemainder +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 @@ -28,6 +28,7 @@ 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 } @@ -40,9 +41,10 @@ class X509CertParserTest : FreeSpec({ X509Certificate.decodeFromDer(derBytes) val garbage = Random.nextBytes(Random.nextInt(0..128)) - Asn1Element.parseWithRemainder(derBytes + garbage).let { (parsed, remainder) -> + val input = (derBytes + garbage).iterator() + Asn1Element.parseFirst(input).let { parsed -> parsed.derEncoded shouldBe derBytes - remainder shouldBe garbage + input.toByteArray() shouldBe garbage } } @@ -64,9 +66,10 @@ class X509CertParserTest : FreeSpec({ cert shouldBe X509Certificate.decodeFromByteArray(certBytes) val garbage = Random.nextBytes(Random.nextInt(0..128)) - Asn1Element.parseWithRemainder(certBytes + garbage).let { (parsed, remainder) -> + val input = (certBytes + garbage).iterator() + Asn1Element.parseFirst(input).let { parsed -> parsed.derEncoded shouldBe certBytes - remainder shouldBe garbage + input.toByteArray() shouldBe garbage } } } @@ -124,9 +127,10 @@ class X509CertParserTest : FreeSpec({ parsed shouldBe X509Certificate.decodeFromByteArray(crt.encoded) val garbage = Random.nextBytes(Random.nextInt(0..128)) - Asn1Element.parseWithRemainder(crt.encoded + garbage).let { (parsed, remainder) -> + val bytes = (crt.encoded + garbage).iterator() + Asn1Element.parseFirst(bytes).let { parsed -> parsed.derEncoded shouldBe own - remainder shouldBe garbage + bytes.toByteArray() shouldBe garbage } } } @@ -146,9 +150,10 @@ class X509CertParserTest : FreeSpec({ decoded shouldBe X509Certificate.decodeFromByteArray(it.second) val garbage = Random.nextBytes(Random.nextInt(0..128)) - Asn1Element.parseWithRemainder(it.second + garbage).let { (parsed, remainder) -> + val bytes = (it.second + garbage).iterator() + Asn1Element.parseFirst(bytes).let { parsed -> parsed.derEncoded shouldBe it.second - remainder shouldBe garbage + bytes.toByteArray() shouldBe garbage } } } @@ -189,9 +194,10 @@ class X509CertParserTest : FreeSpec({ cert.encodeToTlv().derEncoded shouldBe encodedSrc val garbage = Random.nextBytes(Random.nextInt(0..128)) - Asn1Element.parseWithRemainder(jcaCert.encoded + garbage).let { (parsed, remainder) -> + val input = (jcaCert.encoded + garbage).iterator() + Asn1Element.parseFirst(input).let { parsed -> parsed.derEncoded shouldBe jcaCert.encoded - remainder shouldBe garbage + input.asSequence().toList().toByteArray() shouldBe garbage } } } From 0915a8e20fb1c3fc651d4bd1141db8af718ab729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 19 Sep 2024 20:58:20 +0200 Subject: [PATCH 14/20] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e3937c3..c9730300 100644 --- a/README.md +++ b/README.md @@ -395,7 +395,7 @@ Any remaining bytes can still be consumed from the iterator, as it will only be * `Asn1Element.parseAll(byteIterator)`, wich consumes all bytes, parses all toplevel ASN.1 elements, and returns them as list. Throws on any parsing error. -And parsed ASN.1 element can be re-encoded (and yes, it is a true re-encoding, since the original bytes are discarded after decoding) by +And parsed ASN.1 element can be re-encoded (this is a true re-encoding, since the original bytes are discarded after decoding) by accessing the lazily evaluated `.derEncoded` property, just as manually constructed ones can. **Note that decoding operations will throw exceptions if invalid data is provided!** From 8a3b0c52614f48b7f6b2466a0492db9e417b0f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 19 Sep 2024 21:59:35 +0200 Subject: [PATCH 15/20] refactor parsing function --- .../asn1/encoding/Asn1Decoding.kt | 65 +++++++------------ 1 file changed, 24 insertions(+), 41 deletions(-) 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 index e2bb0fdb..b27691a3 100644 --- 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 @@ -41,7 +41,7 @@ fun Asn1Element.Companion.parse(source: ByteArray): Asn1Element = parse(source.i * @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 = Asn1Reader(input).doParse(single = false) +fun Asn1Element.Companion.parseAll(input: ByteIterator): List = Asn1Reader(input).doParseAll() /** @@ -52,42 +52,32 @@ fun Asn1Element.Companion.parseAll(input: ByteIterator): List = Asn */ //this only makes sense until we switch to kotlinx.io @Throws(Asn1Exception::class) -fun Asn1Element.Companion.parseFirst(input: ByteIterator): Asn1Element = Asn1Reader(input).doParse(single = true).let { - if (it.isEmpty()) throw Asn1StructuralException("No ASN.1 structures found") - it.first() -} +fun Asn1Element.Companion.parseFirst(input: ByteIterator): Asn1Element = Asn1Reader(input).doParseSingle() private class Asn1Reader(private val input: ByteIterator) { - @Throws(Asn1Exception::class) - fun doParse(single: Boolean = false): List = runRethrowing { + fun doParseAll(): List = runRethrowing { val result = mutableListOf() - while (input.hasNext()) { - val tlv = input.readTlv() - if (tlv.isSequence()) result.add(Asn1Sequence(Asn1Reader(tlv.content.iterator()).doParse())) - else if (tlv.isSet()) result.add(Asn1Set.fromPresorted(Asn1Reader(tlv.content.iterator()).doParse())) - else if (tlv.isExplicitlyTagged()) result.add( - Asn1ExplicitlyTagged( - tlv.tag.tagValue, - Asn1Reader(tlv.content.iterator()).doParse() - ) - ) - else if (tlv.tag == Asn1Element.Tag.OCTET_STRING) { - catching { - result.add(Asn1EncapsulatingOctetString(Asn1Reader(tlv.content.iterator()).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.iterator()).doParse(), tlv.tag.tagValue, tlv.tagClass)) - } else result.add(Asn1Primitive(tlv.tag, tlv.content)) - if (single) return result - } + while (input.hasNext()) result += doParseSingle() return result } + fun doParseSingle(): Asn1Element = runRethrowing { + val tlv = input.readTlv() + if (tlv.isSequence()) Asn1Sequence(Asn1Reader(tlv.content.iterator()).doParseAll()) + else if (tlv.isSet()) Asn1Set.fromPresorted(Asn1Reader(tlv.content.iterator()).doParseAll()) + else if (tlv.isExplicitlyTagged()) + Asn1ExplicitlyTagged(tlv.tag.tagValue, Asn1Reader(tlv.content.iterator()).doParseAll()) + else if (tlv.tag == Asn1Element.Tag.OCTET_STRING) catching { + Asn1EncapsulatingOctetString(Asn1Reader(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(Asn1Reader(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 @@ -351,37 +341,30 @@ fun Boolean.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): Boolean { fun String.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray) = bytes.decodeToString() -@Throws(Asn1Exception::class) private fun ByteIterator.readTlv(): TLV = runRethrowing { if (!hasNext()) throw IllegalArgumentException("Can't read TLV, input empty") - - val tag = decodeTag() - - if (!hasNext()) return TLV(Asn1Element.Tag(decodeTag().second), byteArrayOf()) - val length = decodeLength() require(length < 1024 * 1024) { "Heap space" } - val value = with(this) { - ByteArray(length) { + val value = ByteArray(length) { require(hasNext()) { "Out of bytes to decode" } - next() + nextByte() } - } + return TLV(Asn1Element.Tag(tag.second), value) } @Throws(IllegalArgumentException::class) private fun ByteIterator.decodeLength() = - next().let { firstByte -> + 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 + (next().toUByte().toInt() shl Byte.SIZE_BITS * (numberOfLengthOctets - index - 1)) + acc + (nextByte().toUByte().toInt() shl Byte.SIZE_BITS * (numberOfLengthOctets - index - 1)) } } } @@ -391,7 +374,7 @@ 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 -> + nextByte().let { firstByte -> (firstByte byteMask 0x1F).let { tagNumber -> if (tagNumber <= 30U) { tagNumber.toULong() to byteArrayOf(firstByte) From f900b595182a8892de3080ab5455df7f49f81c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 20 Sep 2024 13:22:55 +0200 Subject: [PATCH 16/20] address pr feedback --- CHANGELOG.md | 10 ++-- README.md | 14 +++--- indispensable/build.gradle.kts | 1 + .../signum/indispensable/CryptoPublicKey.kt | 2 +- .../asn1/encoding/Asn1Decoding.kt | 48 ++++++++++++------- .../signum/indispensable/Asn1ParserTest.kt | 9 ++++ 6 files changed, 57 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6878fe7b..9948c10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,11 +28,15 @@ * `Iterable.decodeAsn1VarUInt()` * `ByteArray.decodeAsn1VarUInt()` * Revamp implicit tagging -* Revamp `Asn1Element.parse()` - * `Asn1Element.parse()` semantics remain the same - * `Asn1Element.parse()` alternative introduced, which takes a `ByteIterator` +* 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` + * Variant 2 takes a `ByteArray` * 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) diff --git a/README.md b/README.md index c9730300..aa6db22f 100644 --- a/README.md +++ b/README.md @@ -388,14 +388,14 @@ Which results in the following output: 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 either: -* `Asn1Element.parse(derBytes)`, which will consume all bytes and return the first parsed ASN.1 element. -This method throws if more than a single toplevel ASN.1 Element it found or if any parsing errors occur. -* `Asn1Element.parseFirst(byteIterator)`, 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 frist parsed element. -* `Asn1Element.parseAll(byteIterator)`, wich consumes all bytes, parses all toplevel ASN.1 elements, and returns them as list. +* `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. -And parsed ASN.1 element can be re-encoded (this is a true re-encoding, since the original bytes are discarded after decoding) by +Any parsed ASN.1 element can be re-encoded (this is a true re-encoding, since the original bytes are discarded after decoding) by accessing the lazily evaluated `.derEncoded` property, just as manually constructed ones can. **Note that decoding operations will throw exceptions if invalid data is provided!** @@ -451,7 +451,7 @@ However, anything can be decoded and tagged at will. Therefore, a generic decodi following signature: ```kotlin -inline fun Asn1Primitive.decode(tag: Asn1Element.Tag, decode: (content: ByteArray) -> T) +inline fun Asn1Primitive.decode(assertTag: Asn1Element.Tag, decode: (content: ByteArray) -> T) ``` Check out [Asn1Decoding.kt](indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt) for a full diff --git a/indispensable/build.gradle.kts b/indispensable/build.gradle.kts index a3d1ba24..70609f9d 100644 --- a/indispensable/build.gradle.kts +++ b/indispensable/build.gradle.kts @@ -182,6 +182,7 @@ kotlin { commonTest { dependencies { implementation(kotest("property")) + api("io.arrow-kt:arrow-core:1.2.4") } } 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 aec09d90..ff4d4fe6 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt @@ -129,7 +129,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { Rsa.oid -> { (keyInfo.nextChild() as Asn1Primitive).readNull() val bitString = (src.nextChild() as Asn1Primitive).asAsn1BitString() - val rsaSequence = Asn1Element.parse(bitString.rawBytes.iterator()) as Asn1Sequence + 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).decodeToInt() if (rsaSequence.hasMoreChildren()) throw Asn1StructuralException("Superfluous data in SPKI!") 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 index b27691a3..506ef68b 100644 --- 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 @@ -23,10 +23,11 @@ import kotlin.experimental.and * @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(source: ByteIterator): Asn1Element = parseAll(source).let{ - if (it.size != 1) throw Asn1StructuralException("Multiple ASN.1 structures found") - it.first() +fun Asn1Element.Companion.parse(input: ByteIterator): Asn1Element = parseFirst(input).let { + if (input.hasNext()) throw Asn1StructuralException("Trailing bytes found after the fist ASN.1 element") + it } + /** * Convenience wrapper around [parse], taking a [ByteArray] as [source] * @see parse @@ -41,7 +42,14 @@ fun Asn1Element.Companion.parse(source: ByteArray): Asn1Element = parse(source.i * @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 = Asn1Reader(input).doParseAll() +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()) /** @@ -52,36 +60,44 @@ fun Asn1Element.Companion.parseAll(input: ByteIterator): List = Asn */ //this only makes sense until we switch to kotlinx.io @Throws(Asn1Exception::class) -fun Asn1Element.Companion.parseFirst(input: ByteIterator): Asn1Element = Asn1Reader(input).doParseSingle() +fun Asn1Element.Companion.parseFirst(input: ByteIterator): Asn1Element = input.doParseSingle() -private class Asn1Reader(private val input: ByteIterator) { +/** + * Convenience wrapper around [parseFirst], taking a [ByteArray] as [source]. + * @return a pari of the fist 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) - fun doParseAll(): List = runRethrowing { + private fun ByteIterator.doParseAll(): List = runRethrowing { val result = mutableListOf() - while (input.hasNext()) result += doParseSingle() + while (hasNext()) result += doParseSingle() return result } - fun doParseSingle(): Asn1Element = runRethrowing { - val tlv = input.readTlv() - if (tlv.isSequence()) Asn1Sequence(Asn1Reader(tlv.content.iterator()).doParseAll()) - else if (tlv.isSet()) Asn1Set.fromPresorted(Asn1Reader(tlv.content.iterator()).doParseAll()) +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, Asn1Reader(tlv.content.iterator()).doParseAll()) + Asn1ExplicitlyTagged(tlv.tag.tagValue, tlv.content.iterator().doParseAll()) else if (tlv.tag == Asn1Element.Tag.OCTET_STRING) catching { - Asn1EncapsulatingOctetString(Asn1Reader(tlv.content.iterator()).doParseAll()) as Asn1Element + 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(Asn1Reader(tlv.content.iterator()).doParseAll(), tlv.tag.tagValue, tlv.tagClass) + 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] diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt index d0ce7558..668bc549 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Asn1ParserTest.kt @@ -35,6 +35,10 @@ class Asn1ParserTest : FreeSpec({ 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 @@ -54,6 +58,11 @@ class Asn1ParserTest : FreeSpec({ 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() } From 9f6a09472339b0dfb2cadaa355f4929fb80eb307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 20 Sep 2024 13:57:24 +0200 Subject: [PATCH 17/20] Update CHANGELOG.md Co-authored-by: Jakob Heher --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9948c10a..ad048849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ * 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` + * Variant 1 takes a `ByteIterator` and returns the element; the `ByteIterator` is advanced accordingly * Variant 2 takes a `ByteArray` * More consistent low-level encoding and decoding function names: * `encodeToAsn1Primitive` to produce an `Asn1Primitive` that can directly be DER-encoded From be9ed1f9b6ffe3dbed4e4eca91d9c9fdaa140881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 20 Sep 2024 13:58:44 +0200 Subject: [PATCH 18/20] Update CHANGELOG.md Co-authored-by: Jakob Heher --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad048849..4ecc3338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ * 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` + * 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) From dc3f628b29e182038643d0df0a60ed89cae404b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 20 Sep 2024 13:59:11 +0200 Subject: [PATCH 19/20] Update README.md Co-authored-by: Jakob Heher --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa6db22f..5ab8cd46 100644 --- a/README.md +++ b/README.md @@ -395,8 +395,8 @@ Any remaining bytes can still be consumed from the iterator, as it will only be * `Asn1Element.parseAll()`, wich consumes all bytes, parses all toplevel ASN.1 elements, and returns them as list. Throws on any parsing error. -Any parsed ASN.1 element can be re-encoded (this is a true re-encoding, since the original bytes are discarded after decoding) by -accessing the lazily evaluated `.derEncoded` property, just as manually constructed ones can. +`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!** From dd924783152bccedd6535f73ab725ffefe0bd81e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 20 Sep 2024 14:06:34 +0200 Subject: [PATCH 20/20] address more pr feedback --- indispensable/build.gradle.kts | 1 - .../asn1/encoding/Asn1Decoding.kt | 24 +++++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/indispensable/build.gradle.kts b/indispensable/build.gradle.kts index 70609f9d..a3d1ba24 100644 --- a/indispensable/build.gradle.kts +++ b/indispensable/build.gradle.kts @@ -182,7 +182,6 @@ kotlin { commonTest { dependencies { implementation(kotest("property")) - api("io.arrow-kt:arrow-core:1.2.4") } } 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 index 506ef68b..01a59b75 100644 --- 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 @@ -23,9 +23,8 @@ import kotlin.experimental.and * @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).let { - if (input.hasNext()) throw Asn1StructuralException("Trailing bytes found after the fist ASN.1 element") - it +fun Asn1Element.Companion.parse(input: ByteIterator): Asn1Element = parseFirst(input).also { + if (input.hasNext()) throw Asn1StructuralException("Trailing bytes found after the first ASN.1 element") } /** @@ -65,20 +64,19 @@ fun Asn1Element.Companion.parseFirst(input: ByteIterator): Asn1Element = input.d /** * Convenience wrapper around [parseFirst], taking a [ByteArray] as [source]. - * @return a pari of the fist parsed [Asn1Element] mapped to the remaining bytes + * @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 - } +@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() @@ -364,9 +362,9 @@ private fun ByteIterator.readTlv(): TLV = runRethrowing { val length = decodeLength() require(length < 1024 * 1024) { "Heap space" } val value = ByteArray(length) { - require(hasNext()) { "Out of bytes to decode" } + require(hasNext()) { "Out of bytes to decode" } nextByte() - } + } return TLV(Asn1Element.Tag(tag.second), value) }