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/constistent api #130

Merged
merged 20 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
* `Iterable<Byte>.decodeAsn1VarUInt()`
* `ByteArray.decodeAsn1VarUInt()`
* Revamp implicit tagging
* Consume only the first `Asn1Element.parse()` only consumes the first parsable element and
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
`Asn1Element.parserWithRemainder()` additionally returns the remaining bytes for convenience
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
* More consistent low-level encoding and decoding function names:
* `encodeToAsn1Primitive` to produce an `Asn1Primitive` that can directly be DER-encoded
* `encodeToAsn1ContentBytes` to produce the content bytes of a TLV primitive (the _V_ in TLV)
* `decodeToXXX` to be invoked on an `Asn1Primitive` to decode a DER-encoded primitive into the target type
* `decodeFromAsn1ContentBytes` to be invoked on the companion of the target type to decode the content bytes of a TLV primitive (the _V_ in TLV)
* Update conventions -> Coroutines 1.0.9

## 3.0

Expand Down
59 changes: 43 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

</div>

Expand Down Expand Up @@ -384,46 +387,70 @@ 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
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
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
allows for
A tandem of helper functions is available for primitives (numbers, booleans, string, bigints):

* `encodeToAsn1Primitive` to produce an `Asn1Primitive` that can directly be DER-encoded
* `encodeToAsn1ContentBytes` to produce the content bytes of a TLV primitive (the _V_ in TLV)

Variations of these exist for `Instant` and `ByteArray`.

* directly parsing DER-encoded byte arrays by calling `.decodeFromDer(bytes)`
* processing an `Asn1Element` by calling `.fromTlv(src)`
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
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved

Various helper functions exist to facilitate decoding the values contained in `Asn1Primitives`, such as `decodeInt()`,
for example.
Various helper functions exist to facilitate decoding the values contained in `Asn1Primitives`, such as `readInt()`,
for example. To also support decoding more complex structures, the companion objects of complex classes (such as certificates, CSRs, …)
implement `Asn1Decodable`, which allows for:

* directly parsing DER-encoded byte arrays by calling `.decodeFromDer(bytes)` and `.decodeFromDerHexString`
* processing an `Asn1Element` by calling `.decodefromTlv(src)`

Both encoding and decoding functions come in two _safe_ (i.e. non-throwing) variants:
* `…Safe()` which returns a [KmmResult](https://github.com/a-sit-plus/kmmresult)
* `…orNull()` which returns null on error

Similarly to encoding, a tandem of decoding functions exists for primitives:
* `decodeToXXX` to be invoked on an `Asn1Primitive` to decode a DER-encoded primitive into the target type
* `decodeFromAsn1ContentBytes` to be invoked on the companion of the target type to decode the content bytes of a TLV primitive (the _V_ in TLV)

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

```kotlin
inline fun <reified T> Asn1Primitive.decode(tag: UByte, decode: (content: ByteArray) -> T)
inline fun <reified T> Asn1Primitive.decode(tag: Asn1Element.Tag, decode: (content: ByteArray) -> T)
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
```

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
9 changes: 7 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import org.jetbrains.dokka.gradle.DokkaMultiModuleTask

plugins {
id("at.asitplus.gradle.conventions") version "2.0.20+20240829"
id("at.asitplus.gradle.conventions") version "2.0.20+20240917"
id("com.android.library") version "8.2.2" apply (false)
}
group = "at.asitplus.signum"

//work around nexus publish bug
val artifactVersion: String by extra
version = artifactVersion
//end work around nexus publish bug


//access dokka plugin from conventions plugin's classpath in root project → no need to specify version
apply(plugin = "org.jetbrains.dokka")
Expand All @@ -26,7 +31,7 @@ tasks.getByName("dokkaHtmlMultiModule") {
"josef-light.png",
"signum-light-large.png",
"signum-dark-large.png",
).files.forEach { it.copyTo(File("build/dokka/${it.name}"), overwrite = true) }
).files.forEach { it.copyTo(File("build/dokka/${it.name}"), overwrite = true) }
}
}

Expand Down
4 changes: 2 additions & 2 deletions indispensable-cosef/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
// sign(publishing.publications)
}
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
4 changes: 2 additions & 2 deletions indispensable-josef/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
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
4 changes: 2 additions & 2 deletions indispensable/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
// sign(publishing.publications)
}
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
Loading
Loading