From 3f40b1f8b979d8c7312b7aad4defd88b5539a33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 30 Sep 2024 21:16:25 +0200 Subject: [PATCH 1/8] Add generic Tag assertion to Asn1Element --- CHANGELOG.md | 3 +++ docs/docs/indispensable.md | 5 ++++ .../signum/indispensable/asn1/Asn1Elements.kt | 24 +++++++++++++++++++ .../signum/indispensable/TagAssertionTest.kt | 22 +++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagAssertionTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a98d779..7c20671f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### 3.10.0 NEXT +* Introduce generic tag assertion to `Asn1Element` + + ### 3.9.0 (Supreme 0.4.0) * Move `Attestation` from Supreme to Indispensable diff --git a/docs/docs/indispensable.md b/docs/docs/indispensable.md index 6424c978..78b5a039 100644 --- a/docs/docs/indispensable.md +++ b/docs/docs/indispensable.md @@ -272,6 +272,11 @@ Manually working on DER-encoded payloads is also supported through the following All of these functions throw an `Asn1Exception` when decoding fails. +Moreover, a generic tag assertion function is present on `Asn1Element`, which throws an `Asn1TagMisMatchException` on error +and returns the tag-asserted element on success: + +* `Asn1Element.assertTag()` takes either an `Asn1Element.Tag` or an `Ulong` tag number + ### Encoding Similarly to decoding function, encoding function also come as high-level and low-level ones. 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 6e6945d5..43b01a2d 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 @@ -244,6 +244,11 @@ sealed class Asn1Element( derEncoded.iterator().decodeTag().let { Triple(it.first, it.second.size, derEncoded) } ) + /** + * Creates a copy of this tag, overriding [tagValue], but keeping [isConstructed] and [tagClass] + */ + fun withNumber(number: ULong) = Tag(number, constructed = isConstructed, tagClass = tagClass) + constructor(tagValue: ULong, constructed: Boolean, tagClass: TagClass = TagClass.UNIVERSAL) : this( encode( tagClass, @@ -387,6 +392,25 @@ sealed class Asn1Element( } } +/** + * asserts that this element's tag matches [tag]. + * + * @throws Asn1TagMismatchException on failure + */ +@Throws(Asn1TagMismatchException::class) +inline fun T.assertTag(tag: Asn1Element.Tag): T { + if (this.tag != tag) throw Asn1TagMismatchException(tag, this.tag) + return this +} + +/** + * Asserts only the tag number, but neither class, nor CONSTRUCTED bit. + * @see assertTag + * @throws Asn1TagMismatchException on failure + */ +@Throws(Asn1TagMismatchException::class) +inline fun T.assertTag(tagNumber: ULong): T = assertTag(tag.withNumber(tagNumber)) + object Asn1EncodableSerializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor("Asn1Encodable", PrimitiveKind.STRING) diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagAssertionTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagAssertionTest.kt new file mode 100644 index 00000000..20bd35f9 --- /dev/null +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagAssertionTest.kt @@ -0,0 +1,22 @@ +package at.asitplus.signum.indispensable + +import at.asitplus.signum.indispensable.asn1.Asn1TagMismatchException +import at.asitplus.signum.indispensable.asn1.assertTag +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.property.Arb +import io.kotest.property.arbitrary.uLong +import io.kotest.property.checkAll + +class TagAssertionTest : FreeSpec({ + "Automated" - { + checkAll(iterations = 100000, Arb.uLong(max = ULong.MAX_VALUE - 2uL)) { + var seq = (Asn1.Sequence { } withImplicitTag it).asStructure() + seq.assertTag(it) + shouldThrow { + seq.assertTag(it + 1uL) + } + } + } +}) \ No newline at end of file From 68ce4167e5bf1f07ea9e8de660502e29398e6e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 30 Sep 2024 21:32:00 +0200 Subject: [PATCH 2/8] rework CSR --- CHANGELOG.md | 1 + .../pki/Pkcs10CertificationRequest.kt | 11 ++++++----- .../Pkcs10CertificationRequestJvmTest.kt | 14 +++++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c20671f..9be38e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### 3.10.0 NEXT * Introduce generic tag assertion to `Asn1Element` +* Change CSR to take an actual `CryptoSignature` instead of a ByteArray ### 3.9.0 (Supreme 0.4.0) 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 eabd432b..15aab8aa 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 @@ -1,6 +1,7 @@ package at.asitplus.signum.indispensable.pki 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.Asn1 @@ -113,7 +114,7 @@ data class Pkcs10CertificationRequest( val tbsCsr: TbsCertificationRequest, val signatureAlgorithm: X509SignatureAlgorithm, @Serializable(with = ByteArrayBase64Serializer::class) - val signature: ByteArray + val signature: CryptoSignature ) : Asn1Encodable { @@ -121,7 +122,7 @@ data class Pkcs10CertificationRequest( override fun encodeToTlv() = Asn1.Sequence { +tbsCsr +signatureAlgorithm - +BitString(signature) + +BitString(signature.encodeToDer()) } override fun equals(other: Any?): Boolean { @@ -132,7 +133,7 @@ data class Pkcs10CertificationRequest( if (tbsCsr != other.tbsCsr) return false if (signatureAlgorithm != other.signatureAlgorithm) return false - if (!signature.contentEquals(other.signature)) return false + if (signature != other.signature) return false return true } @@ -140,7 +141,7 @@ data class Pkcs10CertificationRequest( override fun hashCode(): Int { var result = tbsCsr.hashCode() result = 31 * result + signatureAlgorithm.hashCode() - result = 31 * result + signature.contentHashCode() + result = 31 * result + signature.hashCode() return result } @@ -152,7 +153,7 @@ data class Pkcs10CertificationRequest( val sigAlg = X509SignatureAlgorithm.decodeFromTlv(src.nextChild() as Asn1Sequence) val signature = (src.nextChild() as Asn1Primitive).asAsn1BitString() if (src.hasMoreChildren()) throw Asn1StructuralException("Superfluous structure in CSR Structure") - return Pkcs10CertificationRequest(tbsCsr, sigAlg, signature.rawBytes) + return Pkcs10CertificationRequest(tbsCsr, sigAlg, CryptoSignature.decodeFromDer(signature.rawBytes)) } } } 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 4e8d845f..5a92f04b 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt @@ -76,7 +76,7 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({ initSign(keyPair.private) update(tbsCsr.encodeToDer()) }.sign() - val csr = Pkcs10CertificationRequest(tbsCsr, signatureAlgorithm, signed) + val csr = Pkcs10CertificationRequest(tbsCsr, signatureAlgorithm, CryptoSignature.parseFromJca(signed,signatureAlgorithm)) val kotlinEncoded = csr.encodeToDer() @@ -135,7 +135,7 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({ initSign(keyPair.private) update(tbsCsr.encodeToTlv().derEncoded) }.sign() - val csr = Pkcs10CertificationRequest(tbsCsr, signatureAlgorithm, signed) + val csr = Pkcs10CertificationRequest(tbsCsr, signatureAlgorithm, CryptoSignature.parseFromJca(signed,signatureAlgorithm)) val kotlinEncoded = csr.encodeToTlv().derEncoded val jvmEncoded = bcCsr.encoded @@ -201,7 +201,7 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({ initSign(keyPair.private) update(tbsCsr.encodeToTlv().derEncoded) }.sign() - val csr = Pkcs10CertificationRequest(tbsCsr, signatureAlgorithm, signed) + val csr = Pkcs10CertificationRequest(tbsCsr, signatureAlgorithm, CryptoSignature.parseFromJca(signed,signatureAlgorithm)) val kotlinEncoded = csr.encodeToTlv().derEncoded val jvmEncoded = bcCsr.encoded @@ -331,10 +331,10 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({ update(tbsCsr2.encodeToDer()) }.sign() - val csr = Pkcs10CertificationRequest(tbsCsr1, signatureAlgorithm1, signed) - val csr1 = Pkcs10CertificationRequest(tbsCsr1, signatureAlgorithm1, signed1) - val csr11 = Pkcs10CertificationRequest(tbsCsr1, signatureAlgorithm2, signed11) - val csr2 = Pkcs10CertificationRequest(tbsCsr2, signatureAlgorithm1, signed2) + val csr = Pkcs10CertificationRequest(tbsCsr1, signatureAlgorithm1, CryptoSignature.parseFromJca(signed,signatureAlgorithm1)) + val csr1 = Pkcs10CertificationRequest(tbsCsr1, signatureAlgorithm1, CryptoSignature.parseFromJca(signed1,signatureAlgorithm1)) + val csr11 = Pkcs10CertificationRequest(tbsCsr1, signatureAlgorithm2, CryptoSignature.parseFromJca(signed11,signatureAlgorithm2)) + val csr2 = Pkcs10CertificationRequest(tbsCsr2, signatureAlgorithm1, CryptoSignature.parseFromJca(signed2,signatureAlgorithm1)) csr shouldNotBe csr1 csr1 shouldBe csr1 From 4543549170688d1f434551cfdc8527be43de9fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 30 Sep 2024 22:30:45 +0200 Subject: [PATCH 3/8] introduce tbscsr and tbscert signing shorthands --- CHANGELOG.md | 4 +- .../asitplus/signum/supreme/PkiExtensions.kt | 37 ++++ .../sign/EphemeralSignerCommonTests.kt | 161 +++++++++++++++++- 3 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be38e46..3d8980ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ ## 3.0 -### 3.10.0 NEXT +### 3.10.0 NEXT (Supreme 0.5.0 NEXT) * Introduce generic tag assertion to `Asn1Element` * Change CSR to take an actual `CryptoSignature` instead of a ByteArray +* Introduce shorthand to create CSR from TbsCSR +* Introduce shorthand to create certificate from TbsCertificate ### 3.9.0 (Supreme 0.4.0) diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt new file mode 100644 index 00000000..22461222 --- /dev/null +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt @@ -0,0 +1,37 @@ +package at.asitplus.signum.supreme + +import at.asitplus.KmmResult +import at.asitplus.signum.indispensable.asn1.Asn1StructuralException +import at.asitplus.signum.indispensable.equalsCryptographically +import at.asitplus.signum.indispensable.pki.Pkcs10CertificationRequest +import at.asitplus.signum.indispensable.pki.TbsCertificate +import at.asitplus.signum.indispensable.pki.TbsCertificationRequest +import at.asitplus.signum.indispensable.pki.X509Certificate +import at.asitplus.signum.indispensable.toX509SignatureAlgorithm +import at.asitplus.signum.supreme.sign.Signer + +/** + * Shorthand helper to create an [X509Certificate] by signing [tbsCertificate] + */ +suspend fun Signer.sign(tbsCertificate: TbsCertificate): KmmResult { + val toX509SignatureAlgorithm = + this.signatureAlgorithm.toX509SignatureAlgorithm().getOrElse { return KmmResult.failure(it) } + if (toX509SignatureAlgorithm != tbsCertificate.signatureAlgorithm) + return KmmResult.failure(Asn1StructuralException("The signer's signature algorithm does not match the TbsCertificate's.")) + return sign(tbsCertificate.encodeToDer()).asKmmResult().map { + X509Certificate(tbsCertificate, tbsCertificate.signatureAlgorithm, it) + } +} + +/** + * Shorthand helper to creat a [Pkcs10CertificationRequest] by signing [tbsCsr] + */ +suspend fun Signer.sign(tbsCsr: TbsCertificationRequest): KmmResult { + val toX509SignatureAlgorithm = + this.signatureAlgorithm.toX509SignatureAlgorithm().getOrElse { return KmmResult.failure(it) } + if (!tbsCsr.publicKey.equalsCryptographically(this.publicKey)) + return KmmResult.failure(Asn1StructuralException("The signer's public key does not match the TbsCSR's.")) + return sign(tbsCsr.encodeToDer()).asKmmResult().map { + Pkcs10CertificationRequest(tbsCsr, toX509SignatureAlgorithm, it) + } +} \ No newline at end of file diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt index 1d3d68a6..20ded8b1 100644 --- a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt +++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt @@ -1,10 +1,12 @@ package at.asitplus.signum.supreme.sign -import at.asitplus.signum.indispensable.Digest -import at.asitplus.signum.indispensable.ECCurve -import at.asitplus.signum.indispensable.RSAPadding -import at.asitplus.signum.indispensable.SignatureAlgorithm -import at.asitplus.signum.indispensable.nativeDigest +import at.asitplus.signum.indispensable.* +import at.asitplus.signum.indispensable.asn1.Asn1PrimitiveOctetString +import at.asitplus.signum.indispensable.asn1.Asn1String +import at.asitplus.signum.indispensable.asn1.Asn1Time +import at.asitplus.signum.indispensable.asn1.KnownOIDs +import at.asitplus.signum.indispensable.pki.* +import at.asitplus.signum.supreme.sign import at.asitplus.signum.supreme.signature import at.asitplus.signum.supreme.succeed import com.ionspin.kotlin.bignum.integer.Quadruple @@ -16,7 +18,9 @@ import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNot import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.datetime.Clock import kotlin.random.Random +import kotlin.time.Duration.Companion.days class EphemeralSignerCommonTests : FreeSpec({ "Functional" - { @@ -135,4 +139,151 @@ class EphemeralSignerCommonTests : FreeSpec({ } } } + + "Cert signing" - { + "RSA" - { + withData( + nameFn = { (pad, dig, bits, pre) -> "$dig/$pad/${bits}bit${if (pre) "/pre" else ""}" }, + sequence { + RSAPadding.entries.forEach { padding -> + Digest.entries.forEach { digest -> + when { + digest == Digest.SHA512 && padding == RSAPadding.PSS + -> listOf(2048, 3072, 4096) + + digest == Digest.SHA384 || digest == Digest.SHA512 || padding == RSAPadding.PSS + -> listOf(1024, 2048, 3072, 4096) + + else + -> listOf(512, 1024, 2048, 3072, 4096) + }.forEach { keySize -> + yield(Quadruple(padding, digest, keySize, false)) + yield(Quadruple(padding, digest, keySize, true)) + } + } + } + }) { (padding, digest, keySize, preHashed) -> + val data = Random.Default.nextBytes(64) + val signer: Signer + + + try { + signer = Signer.Ephemeral { + rsa { + digests = setOf(digest); paddings = setOf(padding); bits = keySize + } + }.getOrThrow() + signer.sign(SignatureInput(data).let { + if (preHashed) it.convertTo(digest).getOrThrow() else it + }).signature + } catch (x: UnsupportedOperationException) { + return@withData + } + + val csr = TbsCertificationRequest( + subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("client")))), + publicKey = signer.publicKey, + attributes = listOf( + Pkcs10CertificationRequestAttribute( + // No OID is assigned for this; choose one! + KnownOIDs.id_sMIME, + // ↓↓↓ contains challenge ↓↓↓ + Asn1String.UTF8("foo").encodeToTlv() + ) + ) + ) + if(digest == Digest.SHA1 && padding== RSAPadding.PSS) return@withData + val signedCSR = signer.sign(csr).getOrThrow() + + + val verifier = signer.makeVerifier().getOrThrow() + verifier.verify(signedCSR.tbsCsr.encodeToDer(), signedCSR.signature) should succeed + + + val tbsCrt = TbsCertificate( + serialNumber = Random.nextBytes(16), + signatureAlgorithm = signer.signatureAlgorithm.toX509SignatureAlgorithm().getOrThrow(), + issuerName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("Foo")))), + validFrom = Asn1Time( + Clock.System.now() + ), + validUntil = Asn1Time(Clock.System.now() + 356.days), + subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("client")))), + publicKey = signer.publicKey, + extensions = listOf( + X509CertificateExtension( + KnownOIDs.pkcs_12_OID, + critical = true, + Asn1PrimitiveOctetString(byteArrayOf()) + ) + ) + ) + val cert = signer.sign(tbsCrt).getOrThrow() + + verifier.verify(cert.tbsCertificate.encodeToDer(), cert.signature) should succeed + + } + } + + "ECDSA" - { + withData( + nameFn = { (crv, dig, pre) -> "$crv/$dig${if (pre) "/pre" else ""}" }, + sequence { + ECCurve.entries.forEach { curve -> + Digest.entries.filterNot { it == Digest.SHA1 }.forEach { digest -> + yield(Triple(curve, digest, false)) + yield(Triple(curve, digest, true)) + } + } + }) { (crv, digest, preHashed) -> + val signer = + Signer.Ephemeral { ec { curve = crv; digests = setOf(digest) } }.getOrThrow() + signer.signatureAlgorithm.shouldBeInstanceOf().let { + it.digest shouldBe digest + it.requiredCurve shouldBeIn setOf(null, crv) + } + val csr = TbsCertificationRequest( + subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("client")))), + publicKey = signer.publicKey, + attributes = listOf( + Pkcs10CertificationRequestAttribute( + // No OID is assigned for this; choose one! + KnownOIDs.id_sMIME, + // ↓↓↓ contains challenge ↓↓↓ + Asn1String.UTF8("foo").encodeToTlv() + ) + ) + ) + val signedCSR = signer.sign(csr).getOrThrow() + + + val verifier = signer.makeVerifier().getOrThrow() + verifier.verify(signedCSR.tbsCsr.encodeToDer(), signedCSR.signature) should succeed + + + val tbsCrt = TbsCertificate( + serialNumber = Random.nextBytes(16), + signatureAlgorithm = signer.signatureAlgorithm.toX509SignatureAlgorithm().getOrThrow(), + issuerName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("Foo")))), + validFrom = Asn1Time( + Clock.System.now() + ), + validUntil = Asn1Time(Clock.System.now() + 356.days), + subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("client")))), + publicKey = signer.publicKey, + extensions = listOf( + X509CertificateExtension( + KnownOIDs.pkcs_12_OID, + critical = true, + Asn1PrimitiveOctetString(byteArrayOf()) + ) + ) + ) + val cert = signer.sign(tbsCrt).getOrThrow() + + verifier.verify(cert.tbsCertificate.encodeToDer(), cert.signature) should succeed + } + } + } + }) From e05e4b3d069943008bea49fe3d8f08bd2d2d22fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 30 Sep 2024 22:38:15 +0200 Subject: [PATCH 4/8] add clientData to iOS legacy attestation --- .../kotlin/at/asitplus/signum/indispensable/Attestation.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt index 51e72e6d..492be5fb 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt @@ -40,11 +40,14 @@ data class IosLegacyHomebrewAttestation( @Serializable(with=ByteArrayBase64UrlSerializer::class) val attestation: ByteArray, @Serializable(with=ByteArrayBase64UrlSerializer::class) + val clientData: ByteArray, + @Serializable(with=ByteArrayBase64UrlSerializer::class) val assertion: ByteArray): Attestation { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is IosLegacyHomebrewAttestation) return false + if (!clientData.contentEquals(other.clientData)) return false if (!attestation.contentEquals(other.attestation)) return false return assertion.contentEquals(other.assertion) } @@ -52,6 +55,7 @@ data class IosLegacyHomebrewAttestation( override fun hashCode(): Int { var result = attestation.contentHashCode() result = 31 * result + assertion.contentHashCode() + result = 31 * result + clientData.contentHashCode() return result } } From fbf8df492f99ad0efbb797c9c6b8717e81c22b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 1 Oct 2024 14:36:47 +0200 Subject: [PATCH 5/8] restore iOS attestation property --- .../kotlin/at/asitplus/cryptotest/App.kt | 40 ++++++++++++------- .../signum/supreme/os/IosKeychainProvider.kt | 3 +- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt index 2bb12aca..f0df41e3 100644 --- a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt +++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt @@ -55,13 +55,13 @@ import at.asitplus.signum.supreme.sign.makeVerifier import at.asitplus.signum.supreme.sign.verify import at.asitplus.cryptotest.theme.AppTheme import at.asitplus.cryptotest.theme.LocalThemeIsDark +import at.asitplus.signum.indispensable.jsonEncoded import at.asitplus.signum.supreme.asKmmResult import at.asitplus.signum.supreme.os.PlatformSignerConfigurationBase import at.asitplus.signum.supreme.os.PlatformSigningKeyConfigurationBase import at.asitplus.signum.supreme.os.PlatformSigningProvider import at.asitplus.signum.supreme.os.SignerConfiguration import at.asitplus.signum.supreme.os.SigningProvider -import at.asitplus.signum.supreme.os.jsonEncoded import at.asitplus.signum.supreme.sign.Verifier import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier @@ -80,7 +80,7 @@ you don't need this workaround for ios/android, just use PlatformSigningProvider expect val Provider: SigningProvider const val ALIAS = "Bartschlüssel" -val SIGNER_CONFIG: (SignerConfiguration.()->Unit) = { +val SIGNER_CONFIG: (SignerConfiguration.() -> Unit) = { if (this is PlatformSignerConfigurationBase) { unlockPrompt { message = "We're signing a thing!" @@ -94,7 +94,7 @@ val SIGNER_CONFIG: (SignerConfiguration.()->Unit) = { val context = newSingleThreadContext("crypto").also { Napier.base(DebugAntilog()) } -private class getter(private val fn: ()->T) { +private class getter(private val fn: () -> T) { operator fun getValue(nothing: Nothing?, property: KProperty<*>): T = fn() } @@ -112,8 +112,13 @@ internal fun App() { X509SignatureAlgorithm.RS1, X509SignatureAlgorithm.RS256, X509SignatureAlgorithm.RS384, - X509SignatureAlgorithm.RS512) - var keyAlgorithm by remember { mutableStateOf(X509SignatureAlgorithm.ES256) } + X509SignatureAlgorithm.RS512 + ) + var keyAlgorithm by remember { + mutableStateOf( + X509SignatureAlgorithm.ES256 + ) + } var inputData by remember { mutableStateOf("Foo") } var currentSigner by remember { mutableStateOf?>(null) } val currentKey by getter { currentSigner?.mapCatching(Signer::publicKey) } @@ -121,13 +126,15 @@ internal fun App() { currentKey?.fold(onSuccess = { it.toString() }, - onFailure = { - Napier.e("Key failed", it) - "${it::class.simpleName ?: ""}: ${it.message}" - }) ?: "" + onFailure = { + Napier.e("Key failed", it) + "${it::class.simpleName ?: ""}: ${it.message}" + }) ?: "" } val currentAttestation by getter { (currentSigner?.getOrNull() as? Signer.Attestable<*>)?.attestation } - val currentAttestationStr by getter { currentAttestation?.jsonEncoded ?: "" } + val currentAttestationStr by getter { + currentAttestation?.jsonEncoded?.also { Napier.d { it } } ?: "" + } val signingPossible by getter { currentKey?.isSuccess == true } var signatureData by remember { mutableStateOf?>(null) } val signatureDataStr by getter { @@ -148,9 +155,12 @@ internal fun App() { var canGenerate by remember { mutableStateOf(true) } var genTextOverride by remember { mutableStateOf(null) } - val genText by getter { genTextOverride ?: "Generate"} + val genText by getter { genTextOverride ?: "Generate" } - Column(modifier = Modifier.fillMaxSize().verticalScroll(ScrollState(0), enabled = true).windowInsetsPadding(WindowInsets.safeDrawing)) { + Column( + modifier = Modifier.fillMaxSize().verticalScroll(ScrollState(0), enabled = true) + .windowInsetsPadding(WindowInsets.safeDrawing) + ) { Row( horizontalArrangement = Arrangement.Center @@ -300,11 +310,12 @@ internal fun App() { when (val alg = keyAlgorithm.algorithm) { is SignatureAlgorithm.ECDSA -> { this@createSigningKey.ec { - curve = alg.requiredCurve ?: - ECCurve.entries.find { it.nativeDigest == alg.digest }!! + curve = alg.requiredCurve + ?: ECCurve.entries.find { it.nativeDigest == alg.digest }!! digests = setOf(alg.digest) } } + is SignatureAlgorithm.RSA -> { this@createSigningKey.rsa { digests = setOf(alg.digest) @@ -312,6 +323,7 @@ internal fun App() { bits = 1024 } } + else -> error("unreachable") } diff --git a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/os/IosKeychainProvider.kt b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/os/IosKeychainProvider.kt index 3b48f3b1..dfbd721d 100644 --- a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/os/IosKeychainProvider.kt +++ b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/os/IosKeychainProvider.kt @@ -179,11 +179,12 @@ typealias IosSignerSigningConfiguration = PlatformSigningProviderSignerSigningCo sealed class IosSigner(final override val alias: String, private val metadata: IosKeyMetadata, private val signerConfig: IosSignerConfiguration) - : PlatformSigningProviderSigner { + : PlatformSigningProviderSigner, Signer.Attestable { override val mayRequireUserUnlock get() = needsAuthentication val needsAuthentication get() = metadata.needsUnlock val needsAuthenticationForEveryUse get() = metadata.needsUnlock && (metadata.unlockTimeout == Duration.ZERO) + override val attestation get() = metadata.attestation internal interface PrivateKeyManager { fun get(signingConfig: IosSignerSigningConfiguration): AutofreeVariable } internal val privateKeyManager = object : PrivateKeyManager { From f2480ce383640ee227d6067a652ed2a58a4c38d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 3 Oct 2024 14:08:03 +0200 Subject: [PATCH 6/8] extract function to prepare digest input of iOS attestation client data --- .../kotlin/at/asitplus/signum/indispensable/Attestation.kt | 6 ++++++ .../at/asitplus/signum/supreme/os/IosKeychainProvider.kt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt index 492be5fb..a1337289 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt @@ -86,6 +86,12 @@ data class IosHomebrewAttestation( internal fun assertValidity() { if (purpose != THE_PURPOSE) throw IllegalStateException("Invalid purpose") } + /** + * Computes the ByteArray that is used to compute the client data hash input for `DCAppAttest`. + * This is effectively the ByteArray-Representation of this data's JSON encoding. + */ + fun prepareDigestInput(): ByteArray = Json.encodeToString(this).encodeToByteArray() + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false diff --git a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/os/IosKeychainProvider.kt b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/os/IosKeychainProvider.kt index dfbd721d..d03b5a23 100644 --- a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/os/IosKeychainProvider.kt +++ b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/os/IosKeychainProvider.kt @@ -548,7 +548,7 @@ object IosKeychainProvider: PlatformSigningProviderI Date: Fri, 4 Oct 2024 09:12:55 +0200 Subject: [PATCH 7/8] minor polishing Co-authored-by: Jakob Heher --- .../commonMain/kotlin/at/asitplus/cryptotest/App.kt | 13 +++++++------ docs/docs/indispensable.md | 2 +- .../signum/indispensable/asn1/Asn1Elements.kt | 4 ++-- .../at/asitplus/signum/supreme/PkiExtensions.kt | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt index f0df41e3..e41fdd3c 100644 --- a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt +++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt @@ -123,9 +123,10 @@ internal fun App() { var currentSigner by remember { mutableStateOf?>(null) } val currentKey by getter { currentSigner?.mapCatching(Signer::publicKey) } val currentKeyStr by getter { - currentKey?.fold(onSuccess = { - it.toString() - }, + currentKey?.fold( + onSuccess = { + it.toString() + }, onFailure = { Napier.e("Key failed", it) "${it::class.simpleName ?: ""}: ${it.message}" @@ -133,7 +134,7 @@ internal fun App() { } val currentAttestation by getter { (currentSigner?.getOrNull() as? Signer.Attestable<*>)?.attestation } val currentAttestationStr by getter { - currentAttestation?.jsonEncoded?.also { Napier.d { it } } ?: "" + currentAttestation?.jsonEncoded?.also { Napier.d { "Current Attestation: $it" } } ?: "" } val signingPossible by getter { currentKey?.isSuccess == true } var signatureData by remember { mutableStateOf?>(null) } @@ -310,8 +311,8 @@ internal fun App() { when (val alg = keyAlgorithm.algorithm) { is SignatureAlgorithm.ECDSA -> { this@createSigningKey.ec { - curve = alg.requiredCurve - ?: ECCurve.entries.find { it.nativeDigest == alg.digest }!! + curve = alg.requiredCurve ?: + ECCurve.entries.find { it.nativeDigest == alg.digest }!! digests = setOf(alg.digest) } } diff --git a/docs/docs/indispensable.md b/docs/docs/indispensable.md index 78b5a039..242f5788 100644 --- a/docs/docs/indispensable.md +++ b/docs/docs/indispensable.md @@ -275,7 +275,7 @@ All of these functions throw an `Asn1Exception` when decoding fails. Moreover, a generic tag assertion function is present on `Asn1Element`, which throws an `Asn1TagMisMatchException` on error and returns the tag-asserted element on success: -* `Asn1Element.assertTag()` takes either an `Asn1Element.Tag` or an `Ulong` tag number +* `Asn1Element.assertTag()` takes either an `Asn1Element.Tag` or an `ULong` tag number ### Encoding 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 43b01a2d..cb9654b5 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 @@ -247,7 +247,7 @@ sealed class Asn1Element( /** * Creates a copy of this tag, overriding [tagValue], but keeping [isConstructed] and [tagClass] */ - fun withNumber(number: ULong) = Tag(number, constructed = isConstructed, tagClass = tagClass) + infix fun withNumber(number: ULong) = Tag(number, constructed = isConstructed, tagClass = tagClass) constructor(tagValue: ULong, constructed: Boolean, tagClass: TagClass = TagClass.UNIVERSAL) : this( encode( @@ -409,7 +409,7 @@ inline fun T.assertTag(tag: Asn1Element.Tag): T { * @throws Asn1TagMismatchException on failure */ @Throws(Asn1TagMismatchException::class) -inline fun T.assertTag(tagNumber: ULong): T = assertTag(tag.withNumber(tagNumber)) +inline fun T.assertTag(tagNumber: ULong): T = assertTag(tag withNumber tagNumber) object Asn1EncodableSerializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor("Asn1Encodable", PrimitiveKind.STRING) diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt index 22461222..bec89af6 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt @@ -24,7 +24,7 @@ suspend fun Signer.sign(tbsCertificate: TbsCertificate): KmmResult { val toX509SignatureAlgorithm = From a5c9f9f50a6dc2d6200de60ae8ff665a46a85a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 4 Oct 2024 09:47:57 +0200 Subject: [PATCH 8/8] remove iOS legacy attestation --- docs/docs/indispensable.md | 2 -- docs/docs/supreme.md | 6 ----- .../signum/indispensable/Attestation.kt | 26 ------------------- 3 files changed, 34 deletions(-) diff --git a/docs/docs/indispensable.md b/docs/docs/indispensable.md index 242f5788..50aa3ef2 100644 --- a/docs/docs/indispensable.md +++ b/docs/docs/indispensable.md @@ -68,8 +68,6 @@ It contains essentials such as: * `X509SignatureAlgorithm` enumeration of supported X.509 signature algorithms (maps to and from `SignatureAlgorithm`) * `Attestation` representing a container to convey attestation statements * `AndroidKeystoreAttestation` contains the certificate chain from Google's root certificate down to the attested key - * `IosLegacyHomebrewAttesation` contains an attestation and an assertion, conforming to the emulated key attestation scheme -currently supported by warden. * `IosHomebrewAttestation` contains the new iOS attestation format introduces in Supreme 0.2.0 (see the [Attestation](supreme.md#attestation) section of the _Supreme_ manual for details). * `SelfAttestation` is used on the JVM. It has no specific semantics, but could be used, if an attestation-supporting HSM is used on the JVM. WIP! diff --git a/docs/docs/supreme.md b/docs/docs/supreme.md index b3d65e0d..d31bef4e 100644 --- a/docs/docs/supreme.md +++ b/docs/docs/supreme.md @@ -281,12 +281,6 @@ The Supreme KMP crypto provider introduces a `digest()` extension function on th For a list of supported algorithms, check out the [feature matrix](features.md#supported-algorithms). ## Attestation -!!! info - All attestation types are serializable for transfer and are part of the _Indispensable_ module, so they are usable - on JVM-only back-ends, that may not wish to include the _Supreme_ KM crypto provider. - [_WARDEN_](https://github.com/a-sit-plus/warden) does not yet directly support this format, but will in the next release. - As of now, the encoded certificate chain of the `AndroidKeytoreAttestation` and an array containing `attestation` - followed by `assertion` from the `IosLegacyHomebrewAttestation` are supported WARDEN. The Android KeyStore offers key attestation certificates for hardware-backed keys. These certificates are exposed by the signer's `.attestation` property. diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt index a1337289..139f8b1b 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Attestation.kt @@ -34,32 +34,6 @@ data class AndroidKeystoreAttestation ( @SerialName("x5c") val certificateChain: CertificateChain) : Attestation -@Serializable -@SerialName("ios-appattest-assertion") -data class IosLegacyHomebrewAttestation( - @Serializable(with=ByteArrayBase64UrlSerializer::class) - val attestation: ByteArray, - @Serializable(with=ByteArrayBase64UrlSerializer::class) - val clientData: ByteArray, - @Serializable(with=ByteArrayBase64UrlSerializer::class) - val assertion: ByteArray): Attestation { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is IosLegacyHomebrewAttestation) return false - - if (!clientData.contentEquals(other.clientData)) return false - if (!attestation.contentEquals(other.attestation)) return false - return assertion.contentEquals(other.assertion) - } - - override fun hashCode(): Int { - var result = attestation.contentHashCode() - result = 31 * result + assertion.contentHashCode() - result = 31 * result + clientData.contentHashCode() - return result - } -} - val StrictJson = Json { ignoreUnknownKeys = true; isLenient = false } @Serializable