From 8f5cc2cd3d44615864f295ae1e5654edc7851c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Fri, 4 Oct 2024 11:31:35 +0200 Subject: [PATCH] API polishing (#150) * Add generic Tag assertion to Asn1Element * rework CSR * Introduce TBS CSR and TBS Cert signing shorthands * restore iOS attestation property * extract function to prepare digest input of iOS attestation client data * remove iOS legacy attestation Co-authored-by: Jakob Heher --- CHANGELOG.md | 8 +- .../kotlin/at/asitplus/cryptotest/App.kt | 43 +++-- docs/docs/indispensable.md | 7 +- docs/docs/supreme.md | 6 - .../signum/indispensable/Attestation.kt | 28 +-- .../signum/indispensable/asn1/Asn1Elements.kt | 24 +++ .../pki/Pkcs10CertificationRequest.kt | 11 +- .../Pkcs10CertificationRequestJvmTest.kt | 14 +- .../signum/indispensable/TagAssertionTest.kt | 22 +++ .../asitplus/signum/supreme/PkiExtensions.kt | 37 ++++ .../sign/EphemeralSignerCommonTests.kt | 161 +++++++++++++++++- .../signum/supreme/os/IosKeychainProvider.kt | 5 +- 12 files changed, 301 insertions(+), 65 deletions(-) create mode 100644 indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/TagAssertionTest.kt create mode 100644 supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/PkiExtensions.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a98d779..3d8980ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,13 @@ ## 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/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt index 2bb12aca..e41fdd3c 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,22 +112,30 @@ 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) } val currentKeyStr by getter { - currentKey?.fold(onSuccess = { - it.toString() - }, - onFailure = { - Napier.e("Key failed", it) - "${it::class.simpleName ?: ""}: ${it.message}" - }) ?: "" + currentKey?.fold( + onSuccess = { + it.toString() + }, + 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 { "Current Attestation: $it" } } ?: "" + } val signingPossible by getter { currentKey?.isSuccess == true } var signatureData by remember { mutableStateOf?>(null) } val signatureDataStr by getter { @@ -148,9 +156,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 @@ -305,6 +316,7 @@ internal fun App() { digests = setOf(alg.digest) } } + is SignatureAlgorithm.RSA -> { this@createSigningKey.rsa { digests = setOf(alg.digest) @@ -312,6 +324,7 @@ internal fun App() { bits = 1024 } } + else -> error("unreachable") } diff --git a/docs/docs/indispensable.md b/docs/docs/indispensable.md index 6424c978..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! @@ -272,6 +270,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/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 51e72e6d..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,28 +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 assertion: ByteArray): Attestation { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is IosLegacyHomebrewAttestation) 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() - return result - } -} - val StrictJson = Json { ignoreUnknownKeys = true; isLenient = false } @Serializable @@ -82,6 +60,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/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..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 @@ -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] + */ + infix 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/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 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 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..bec89af6 --- /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 create 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 + } + } + } + }) 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..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 @@ -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 { @@ -547,7 +548,7 @@ object IosKeychainProvider: PlatformSigningProviderI