Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/consistent encoding api #129

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
* `Iterable<Byte>.decodeAsn1VarUInt()`
* `ByteArray.decodeAsn1VarUInt()`
* Revamp implicit tagging
* 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

Expand Down
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,26 +404,40 @@ 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 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)`

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:
* `decodeToXXX` to be invoked on an `Asn1Primitive` to decode a DER-encoded primitive into the target type
* `decodeFromAsn1ContentBytes` to be invoked on the companion of the target type to decode the content bytes of a TLV primitive (the _V_ in TLV)

However, anything can be decoded and tagged at will. Therefore, a generic decoding function exists, which has the
following signature:

```kotlin
inline fun <reified T> Asn1Primitive.decode(tag: UByte, decode: (content: ByteArray) -> T)
inline fun <reified T> 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

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -164,7 +164,7 @@ sealed class CoseKeyParams : SpecializedCryptoPublicKey {
override fun toCryptoPublicKey(): KmmResult<CryptoPublicKey> = 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"))
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import at.asitplus.signum.indispensable.pki.X509Certificate
object JwsCertificateSerializer : TransformingSerializerTemplate<X509Certificate, ByteArray>(
parent = ByteArrayBase64Serializer,
encodeAs = X509Certificate::encodeToDer,
decodeAs = X509Certificate::decodeFromDer
decodeAs = { X509Certificate.decodeFromDer(it) } //workaround iOS compilation bug KT-71498
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -115,7 +117,7 @@ sealed class CryptoPublicKey : Asn1Encodable<Asn1Sequence>, Identifiable {
val curve = ECCurve.entries.find { it.oid == curveOid }
?: throw Asn1Exception("Curve not supported: $curveOid")

val bitString = (src.nextChild() as Asn1Primitive).readBitString()
val bitString = (src.nextChild() as Asn1Primitive).asAsn1BitString()
if (!bitString.rawBytes.hasPrefix(ANSIECPrefix.UNCOMPRESSED)) throw Asn1Exception("EC key not prefixed with 0x04")
val xAndY = bitString.rawBytes.drop(1)
val coordLen = curve.coordinateLength.bytes.toInt()
Expand All @@ -126,10 +128,10 @@ sealed class CryptoPublicKey : Asn1Encodable<Asn1Sequence>, Identifiable {

Rsa.oid -> {
(keyInfo.nextChild() as Asn1Primitive).readNull()
val bitString = (src.nextChild() as Asn1Primitive).readBitString()
val bitString = (src.nextChild() as Asn1Primitive).asAsn1BitString()
val rsaSequence = Asn1Element.parse(bitString.rawBytes) as Asn1Sequence
val n = (rsaSequence.nextChild() as Asn1Primitive).decode(Asn1Element.Tag.INT) { it }
val e = (rsaSequence.nextChild() as Asn1Primitive).readInt()
val e = (rsaSequence.nextChild() as Asn1Primitive).decodeToInt()
if (rsaSequence.hasMoreChildren()) throw Asn1StructuralException("Superfluous data in SPKI!")
return Rsa(n, e)
}
Expand Down Expand Up @@ -283,7 +285,7 @@ sealed class CryptoPublicKey : Asn1Encodable<Asn1Sequence>, 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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -91,9 +93,9 @@ sealed interface CryptoSignature : Asn1Encodable<Asn1Element> {
require(s.isPositive) { "s must be positive" }
}

override val signature: Asn1Element = Asn1.Sequence { +r.encodeToTlv(); +s.encodeToTlv() }
override val signature: Asn1Element = Asn1.Sequence { +r.encodeToAsn1Primitive(); +s.encodeToAsn1Primitive() }

override fun encodeToTlvBitString(): Asn1Element = encodeToDer().encodeToTlvBitString()
override fun encodeToTlvBitString(): Asn1Element = encodeToDer().encodeToAsn1BitStringPrimitive()

/**
* Two signatures are considered equal if `r` and `s` are equal.
Expand Down Expand Up @@ -209,13 +211,13 @@ sealed interface CryptoSignature : Asn1Encodable<Asn1Element> {

@Throws(Asn1Exception::class)
fun decodeFromTlvBitString(src: Asn1Primitive): EC.IndefiniteLength = runRethrowing {
decodeFromDer(src.readBitString().rawBytes)
decodeFromDer(src.asAsn1BitString().rawBytes)
}

override fun doDecode(src: Asn1Element): EC.IndefiniteLength {
src as Asn1Sequence
val r = (src.nextChild() as Asn1Primitive).readBigInteger()
val s = (src.nextChild() as Asn1Primitive).readBigInteger()
val r = (src.nextChild() as Asn1Primitive).decodeToBigInteger()
val s = (src.nextChild() as Asn1Primitive).decodeToBigInteger()
if (src.hasMoreChildren()) throw Asn1Exception("Illegal Signature Format")
return fromRS(r, s)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package at.asitplus.signum.indispensable

import at.asitplus.catching
import at.asitplus.signum.indispensable.asn1.*
import at.asitplus.signum.indispensable.asn1.Asn1.Null
import at.asitplus.signum.indispensable.asn1.Asn1.ExplicitlyTagged
import at.asitplus.signum.indispensable.asn1.encoding.Asn1
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Null
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.ExplicitlyTagged
import at.asitplus.signum.indispensable.asn1.encoding.decodeToInt
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
Expand Down Expand Up @@ -150,7 +152,7 @@ enum class X509SignatureAlgorithm(
)

val last = (seq.nextChild() as Asn1ExplicitlyTagged).verifyTag(2u).single() as Asn1Primitive
val saltLen = last.readInt()
val saltLen = last.decodeToInt()

return sigAlg.let {
when (it) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading