From b599996a1ad4be079c2d90fa3af1b983b27dd328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Sat, 21 Oct 2023 21:39:29 +0200 Subject: [PATCH] ASN.1 Overhaul: * weed out legacy stuff in Asn1Encoder * better explicit tagging * better recursive OCTET STRINGS * more consitent API use --- .../crypto/datatypes/CryptoPublicKey.kt | 20 +-- .../asitplus/crypto/datatypes/JwsAlgorithm.kt | 15 ++- .../crypto/datatypes/asn1/Asn1Elements.kt | 1 + .../crypto/datatypes/asn1/Asn1Encoder.kt | 122 ++++++++++++------ .../crypto/datatypes/pki/DistinguishedName.kt | 4 +- .../pki/Pkcs10CertificationRequest.kt | 12 +- .../Pkcs10CertificationRequestAttribute.kt | 4 +- .../crypto/datatypes/pki/X509Certificate.kt | 26 ++-- .../datatypes/pki/X509CertificateExtension.kt | 4 +- .../src/jvmTest/kotlin/Asn1EncodingTest.kt | 15 ++- 10 files changed, 129 insertions(+), 94 deletions(-) diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/CryptoPublicKey.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/CryptoPublicKey.kt index f70ede8f..c880abd4 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/CryptoPublicKey.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/CryptoPublicKey.kt @@ -36,8 +36,8 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { override fun encodeToTlv() = when (this) { is Ec -> asn1Sequence { sequence { - oid { oid } - oid { curve.oid } + append(oid) + append(curve.oid) } bitString { (byteArrayOf( @@ -51,18 +51,10 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { is Rsa -> { asn1Sequence { sequence { - oid { oid } + append(oid) asn1null() } - bitString(asn1Sequence { - append { - Asn1Primitive( - BERTags.INTEGER, - n.ensureSize(bits.number / 8u) - .let { if (it.first() == 0x00.toByte()) it else byteArrayOf(0x00, *it) }) - } - int { e } - }) + bitString { iosEncoded } } } } @@ -192,11 +184,11 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable { */ @Transient override val iosEncoded = asn1Sequence { - append { + append( Asn1Primitive(BERTags.INTEGER, n.ensureSize(bits.number / 8u) .let { if (it.first() == 0x00.toByte()) it else byteArrayOf(0x00, *it) }) - } + ) int { e } }.derEncoded diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/JwsAlgorithm.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/JwsAlgorithm.kt index 4363adf3..4b58ffe1 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/JwsAlgorithm.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/JwsAlgorithm.kt @@ -40,26 +40,27 @@ enum class JwsAlgorithm(val identifier: String, override val oid: ObjectIdentifi } override fun encodeToTlv() = when (this) { - ES256 -> asn1Sequence { oid { oid } } - ES384 -> asn1Sequence { oid { oid } } - ES512 -> asn1Sequence { oid { oid } } + ES256 -> asn1Sequence { append(oid) } + ES384 -> asn1Sequence { append(oid) } + ES512 -> asn1Sequence { append(oid) } + RS256 -> asn1Sequence { - oid { oid } + append(oid) asn1null() } RS384 -> asn1Sequence { - oid { oid } + append(oid) asn1null() } RS512 -> asn1Sequence { - oid { oid } + append(oid) asn1null() } NON_JWS_SHA1_WITH_RSA -> asn1Sequence { - oid { oid } + append(oid) asn1null() } diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Elements.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Elements.kt index 17406a47..58ba8717 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Elements.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Elements.kt @@ -169,6 +169,7 @@ sealed class Asn1Structure(tag: UByte, children: List?) : * @param tag the ASN.1 Tag to be used * @param children the child nodes to be contained in this tag */ +//TODO check if explicitly tagged class Asn1Tagged(tag: UByte, children: List) : Asn1Structure(tag, children) { /** diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Encoder.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Encoder.kt index 71a2a276..680df143 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Encoder.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/asn1/Asn1Encoder.kt @@ -9,6 +9,7 @@ import at.asitplus.crypto.datatypes.asn1.BERTags.INTEGER import at.asitplus.crypto.datatypes.asn1.BERTags.NULL import at.asitplus.crypto.datatypes.asn1.BERTags.OBJECT_IDENTIFIER import at.asitplus.crypto.datatypes.asn1.BERTags.UTC_TIME +import at.asitplus.crypto.datatypes.asn1.DERTags.toExplicitTag import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString @@ -63,100 +64,118 @@ class Asn1TreeBuilder { /** * appends a single [Asn1Element] to this ASN.1 structure */ - fun append(child: () -> Asn1Element) = apply { elements += child() } + fun append(child: Asn1Element) { + elements += child + } /** - * EXPLICITLY tags a single [Asn1Element] and adds it to this ASN.1 structure + * appends a single [Asn1Encodable] to this ASN.1 structure */ - fun tagged(tag: UByte, child: () -> Asn1Element) = apply { elements += Asn1Tagged(tag, child()) } + fun append(child: Asn1Encodable<*>) = append(child.encodeToTlv()) /** - * Adds a BOOL [Asn1Primitive] to this ASN.1 structure + * EXPLICITLY tags and encapsulates the result of [init] + *
+ * **NOTE:** automatically calls [at.asitplus.crypto.datatypes.asn1.DERTags.toExplicitTag] on [tag] */ - fun bool(block: () -> Boolean) = apply { elements += block().encodeToTlv() } + fun tagged(tag: UByte, init: Asn1TreeBuilder.() -> Unit) { + val seq = Asn1TreeBuilder() + seq.init() + elements += Asn1Tagged(tag.toExplicitTag(), seq.elements) + } /** - * Adds an INTEGER [Asn1Primitive] to this ASN.1 structure + * Adds a BOOL [Asn1Primitive] to this ASN.1 structure */ - fun int(block: () -> Int) = apply { elements += block().encodeToTlv() } + fun bool(block: () -> Boolean) { + elements += block().encodeToTlv() + } /** * Adds an INTEGER [Asn1Primitive] to this ASN.1 structure */ - fun long(block: () -> Long) = apply { elements += block().encodeToTlv() } + fun int(block: () -> Int) { + elements += block().encodeToTlv() + } /** - * Adds the passed bytes as OCTET STRING [Asn1Primitive] to this ASN.1 structure + * Adds an INTEGER [Asn1Primitive] to this ASN.1 structure */ - fun octetString(block: () -> ByteArray) = apply { elements += block().encodeToTlvOctetString() } + fun long(block: () -> Long) { + elements += block().encodeToTlv() + } /** - * Adds passed [Asn1Element] as OCTET STRING [Asn1Primitive] to this ASN.1 structure + * Adds the passed bytes as OCTET STRING [Asn1Element] to this ASN.1 structure */ - fun octetString(child: Asn1Element) = apply { octetString(block = { child.derEncoded }) } + fun octetString(block: () -> ByteArray) = apply { elements += block().encodeToTlvOctetString() } /** * Adds the passed bytes as BIT STRING [Asn1Primitive] to this ASN.1 structure */ - fun bitString(block: () -> ByteArray) = apply { elements += block().encodeToTlvBitString() } - - /** - * Adds the passed [Asn1Element] as BIT STRING [Asn1Primitive] to this ASN.1 structure - */ - fun bitString(child: Asn1Element) = apply { bitString(block = { child.derEncoded }) } - - /** - * Adds the passed [ObjectIdentifier] as OBJECT IDENTIFIER to this ASN.1 structure - */ - fun oid(block: () -> ObjectIdentifier) = apply { elements += block().encodeToTlv() } + fun bitString(block: () -> ByteArray) { + elements += block().encodeToTlvBitString() + } /** * Shorthand method taking a HEX representation of an OID value, adding it as an OBJECT IDENTIFIER to this ASN.1 structure. * Really only useful for quick debugging against other ASN.1 decoders, such as https://lapo.it/asn1js/, so we're keeping it for now */ @Deprecated("Used only for quick debugging. May be removed in the future") - fun hexEncoded(block: () -> String) = apply { elements += block().encodeTolvOid() } - + fun hexEncoded(block: () -> String) { + elements += block().encodeTolvOid() + } /** * Adds the passed string as UTF8 STRING to this ASN.1 structure */ - fun utf8String(block: () -> String) = apply { elements += Asn1String.UTF8(block()).encodeToTlv() } + fun utf8String(block: () -> String) { + elements += Asn1String.UTF8(block()).encodeToTlv() + } /** * Adds the passed string as PRINTABLE STRING to this ASN.1 structure */ - fun printableString(block: () -> String) = apply { elements += Asn1String.Printable(block()).encodeToTlv() } + fun printableString(block: () -> String) { + elements += Asn1String.Printable(block()).encodeToTlv() + } /** * Adds the passed [Asn1String] to this ASN.1 structure */ - fun string(block: () -> Asn1String) = apply { + fun string(block: () -> Asn1String) { val str = block() - append { str.encodeToTlv() } + str.encodeToTlv() } /** * Adds a NULL to this ASN.1 structure */ - fun asn1null() = apply { elements += Asn1Primitive(NULL, byteArrayOf()) } + fun asn1null() { + elements += Asn1Primitive(NULL, byteArrayOf()) + } /** * Adds the passed instant as UTC TIME to this ASN.1 structure */ - fun utcTime(block: () -> Instant) = apply { elements += block().encodeToAsn1UtcTime() } + fun utcTime(block: () -> Instant) { + elements += block().encodeToAsn1UtcTime() + } /** * Adds the passed instant as GENERALIZED TIME to this ASN.1 structure */ - fun generalizedTime(block: () -> Instant) = - apply { elements += block().encodeToAsn1GeneralizedTime() } + fun generalizedTime(block: () -> Instant) { + elements += block().encodeToAsn1GeneralizedTime() + } - private fun nest(type: CollectionType, init: Asn1TreeBuilder.() -> Unit) = apply { + private fun nest(type: CollectionType, init: Asn1TreeBuilder.() -> Unit) { val seq = Asn1TreeBuilder() seq.init() - elements += if (type == CollectionType.SEQUENCE) Asn1Sequence(seq.elements) else Asn1Set(seq.elements.let { + elements += if (type == CollectionType.SEQUENCE) Asn1Sequence(seq.elements) + else if (type == CollectionType.OCTET_STRING) Asn1EncapsulatingOctetString(seq.elements) + else Asn1Set(seq.elements.let { if (type == CollectionType.SET) it.sortedBy { it.tag } else { if (it.any { elem -> elem.tag != it.first().tag }) throw IllegalArgumentException("SET_OF must only contain elements of the same tag") @@ -172,11 +191,11 @@ class Asn1TreeBuilder { * ```kotlin * set { * sequence { - * setOf { //note: DER encoding enfoces sorting here, so the result switches those + * setOf { //note: DER encoding enforces sorting here, so the result switches those * printableString { "World" } * printableString { "Hello" } * } - * set { //note: DER encoding enfoces sorting by tags, so the order changes in the ourput + * set { //note: DER encoding enforces sorting by tags, so the order changes in the output * printableString { "World" } * printableString { "Hello" } * utf8String { "!!!" } @@ -194,11 +213,11 @@ class Asn1TreeBuilder { * ```kotlin * set { * sequence { - * setOf { //note: DER encoding enfoces sorting here, so the result switches those + * setOf { //note: DER encoding enforces sorting here, so the result switches those * printableString { "World" } * printableString { "Hello" } * } - * set { //note: DER encoding enfoces sorting by tags, so the order changes in the ourput + * set { //note: DER encoding enforces sorting by tags, so the order changes in the output * printableString { "World" } * printableString { "Hello" } * utf8String { "!!!" } @@ -216,11 +235,11 @@ class Asn1TreeBuilder { * ```kotlin * set { * sequence { - * setOf { //note: DER encoding enfoces sorting here, so the result switches those + * setOf { //note: DER encoding enforces sorting here, so the result switches those * printableString { "World" } * printableString { "Hello" } * } - * set { //note: DER encoding enfoces sorting by tags, so the order changes in the ourput + * set { //note: DER encoding enforces sorting by tags, so the order changes in the output * printableString { "World" } * printableString { "Hello" } * utf8String { "!!!" } @@ -231,12 +250,31 @@ class Asn1TreeBuilder { */ fun setOf(init: Asn1TreeBuilder.() -> Unit) = nest(CollectionType.SET_OF, init) + /** + * OCTET STRING builder. The result of [init] is encapsulated into an ASN.1 OCTET STRING and then added to this ASN.1 structure + * ```kotlin + * set { + * octetString { + * printableString { "Hello" } + * printableString { "World" } + * sequence { + * printableString { "World" } + * printableString { "Hello" } + * utf8String { "!!!" } + * } + * } + * } + * ``` + */ + fun octetStringEncapsulated(init: Asn1TreeBuilder.() -> Unit) = nest(CollectionType.OCTET_STRING, init) + } private enum class CollectionType { SET, SEQUENCE, - SET_OF + SET_OF, + OCTET_STRING } /** diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/DistinguishedName.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/DistinguishedName.kt index c06e2357..05f59d29 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/DistinguishedName.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/DistinguishedName.kt @@ -73,8 +73,8 @@ sealed class DistinguishedName : Asn1Encodable, Identifiable { override fun encodeToTlv() = asn1Set { sequence { - oid { oid } - append { value } + append(oid) + append(value) } } diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/Pkcs10CertificationRequest.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/Pkcs10CertificationRequest.kt index f18b11eb..647953cd 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/Pkcs10CertificationRequest.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/Pkcs10CertificationRequest.kt @@ -38,21 +38,19 @@ data class TbsCertificationRequest( version: Int = 0, attributes: List? = null, ) : this(version, subjectName, publicKey, mutableListOf().also { attrs -> - if(extensions.isEmpty()) throw IllegalArgumentException("No extensions provided!") + if (extensions.isEmpty()) throw IllegalArgumentException("No extensions provided!") attributes?.let { attrs.addAll(it) } attrs.add(Pkcs10CertificationRequestAttribute(KnownOIDs.extensionRequest, asn1Sequence { - extensions.forEach { - append { it.encodeToTlv() } - } + extensions.forEach { append(it) } })) }) override fun encodeToTlv() = asn1Sequence { int { version } - sequence { subjectName.forEach { append { it.encodeToTlv() } } } + sequence { subjectName.forEach { append(it) } } subjectPublicKey { publicKey } - append { - Asn1Tagged(0u.toExplicitTag(), attributes?.map { it.encodeToTlv() } ?: listOf()) + tagged(0u) { + attributes?.map { append(it) } } } diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/Pkcs10CertificationRequestAttribute.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/Pkcs10CertificationRequestAttribute.kt index 9c84f036..d69ad5c9 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/Pkcs10CertificationRequestAttribute.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/Pkcs10CertificationRequestAttribute.kt @@ -11,8 +11,8 @@ data class Pkcs10CertificationRequestAttribute( constructor(id: ObjectIdentifier, value: Asn1Element) : this(id, listOf(value)) override fun encodeToTlv() = asn1Sequence { - oid { oid } - set { value.forEach { append { it } } } + append ( oid ) + set { value.forEach { append(it) } } } override fun equals(other: Any?): Boolean { diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/X509Certificate.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/X509Certificate.kt index 6c761520..ba4171d7 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/X509Certificate.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/X509Certificate.kt @@ -35,31 +35,29 @@ data class TbsCertificate( override fun encodeToTlv() = asn1Sequence { version { version } - append { Asn1Primitive(BERTags.INTEGER, serialNumber) } + append(Asn1Primitive(BERTags.INTEGER, serialNumber)) sigAlg { signatureAlgorithm } - sequence { issuerName.forEach { append { it.encodeToTlv() } } } + sequence { issuerName.forEach { append(it.encodeToTlv()) } } sequence { - append { validFrom.asn1Object } - append { validUntil.asn1Object } + append(validFrom.asn1Object) + append(validUntil.asn1Object) } - sequence { subjectName.forEach { append { it.encodeToTlv() } } } + sequence { subjectName.forEach { append(it) } } subjectPublicKey { publicKey } - issuerUniqueID?.let { append { Asn1Primitive(1u.toImplicitTag(), it.encodeToBitString()) } } - subjectUniqueID?.let { append { Asn1Primitive(2u.toImplicitTag(), it.encodeToBitString()) } } + issuerUniqueID?.let { append(Asn1Primitive(1u.toImplicitTag(), it.encodeToBitString())) } + subjectUniqueID?.let { append(Asn1Primitive(2u.toImplicitTag(), it.encodeToBitString())) } extensions?.let { if (it.isNotEmpty()) { - append { - Asn1Tagged(3u.toExplicitTag(), - asn1Sequence { - it.forEach { ext -> - append { ext.encodeToTlv() } - } + tagged(3u) { + sequence { + it.forEach { ext -> + append(ext.encodeToTlv()) } - ) + } } } } diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/X509CertificateExtension.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/X509CertificateExtension.kt index e65272ec..5d6ff951 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/X509CertificateExtension.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/pki/X509CertificateExtension.kt @@ -27,9 +27,9 @@ data class X509CertificateExtension private constructor( ) : this(oid, value, critical) override fun encodeToTlv() = asn1Sequence { - oid { oid } + append(oid) if (critical) bool { true } - append { value } + append(value) } companion object : Asn1Decodable { diff --git a/datatypes/src/jvmTest/kotlin/Asn1EncodingTest.kt b/datatypes/src/jvmTest/kotlin/Asn1EncodingTest.kt index 37b84db2..e1a0f245 100644 --- a/datatypes/src/jvmTest/kotlin/Asn1EncodingTest.kt +++ b/datatypes/src/jvmTest/kotlin/Asn1EncodingTest.kt @@ -18,8 +18,15 @@ class Asn1EncodingTest : FreeSpec({ "OCTET STRING Test" { val seq = asn1Sequence { - octetString { asn1Sequence { utf8String { "foo" } }.derEncoded } + octetStringEncapsulated { + sequence { utf8String { "foo" } } + set { utf8String { "bar" } } + append(Asn1String.Printable("a").encodeToTlv()) + } octetString { byteArrayOf(17) } + + + octetString { asn1Set { int { 99 } @@ -39,7 +46,7 @@ class Asn1EncodingTest : FreeSpec({ } }.derEncoded } - tagged(10u) { Clock.System.now().encodeToAsn1UtcTime() } + tagged(9u) { Clock.System.now().encodeToAsn1UtcTime() } octetString { byteArrayOf(17, -43, 23, -12, 8, 65, 90) } bool { false } bool { true } @@ -90,7 +97,7 @@ class Asn1EncodingTest : FreeSpec({ val instant = Clock.System.now() val sequence = asn1Sequence { - tagged(31u) { + tagged(1u) { Asn1Primitive(BERTags.BOOLEAN, byteArrayOf(0x00)) } set { @@ -109,7 +116,7 @@ class Asn1EncodingTest : FreeSpec({ } asn1null() - oid { ObjectIdentifier("1.2.603.624.97") } + append(ObjectIdentifier("1.2.603.624.97")) utf8String { "Foo" } printableString { "Bar" }