Skip to content

Commit

Permalink
API polishing (#150)
Browse files Browse the repository at this point in the history
* 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 <jakob.heher@iaik.tugraz.at>
  • Loading branch information
JesusMcCloud and iaik-jheher authored Oct 4, 2024
1 parent 61d7c17 commit 8f5cc2c
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 65 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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!"
Expand All @@ -94,7 +94,7 @@ val SIGNER_CONFIG: (SignerConfiguration.()->Unit) = {

val context = newSingleThreadContext("crypto").also { Napier.base(DebugAntilog()) }

private class getter<T>(private val fn: ()->T) {
private class getter<T>(private val fn: () -> T) {
operator fun getValue(nothing: Nothing?, property: KProperty<*>): T = fn()
}

Expand All @@ -112,22 +112,30 @@ internal fun App() {
X509SignatureAlgorithm.RS1,
X509SignatureAlgorithm.RS256,
X509SignatureAlgorithm.RS384,
X509SignatureAlgorithm.RS512)
var keyAlgorithm by remember { mutableStateOf<SpecializedSignatureAlgorithm>(X509SignatureAlgorithm.ES256) }
X509SignatureAlgorithm.RS512
)
var keyAlgorithm by remember {
mutableStateOf<SpecializedSignatureAlgorithm>(
X509SignatureAlgorithm.ES256
)
}
var inputData by remember { mutableStateOf("Foo") }
var currentSigner by remember { mutableStateOf<KmmResult<Signer>?>(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 ?: "<unnamed>"}: ${it.message}"
}) ?: "<none>"
currentKey?.fold(
onSuccess = {
it.toString()
},
onFailure = {
Napier.e("Key failed", it)
"${it::class.simpleName ?: "<unnamed>"}: ${it.message}"
}) ?: "<none>"
}
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<KmmResult<CryptoSignature>?>(null) }
val signatureDataStr by getter {
Expand All @@ -148,9 +156,12 @@ internal fun App() {
var canGenerate by remember { mutableStateOf(true) }

var genTextOverride by remember { mutableStateOf<String?>(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
Expand Down Expand Up @@ -305,13 +316,15 @@ internal fun App() {
digests = setOf(alg.digest)
}
}

is SignatureAlgorithm.RSA -> {
this@createSigningKey.rsa {
digests = setOf(alg.digest)
paddings = RSAPadding.entries.toSet()
bits = 1024
}
}

else -> error("unreachable")
}

Expand Down
7 changes: 5 additions & 2 deletions docs/docs/indispensable.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down Expand Up @@ -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.
Expand Down
6 changes: 0 additions & 6 deletions docs/docs/supreme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -387,6 +392,25 @@ sealed class Asn1Element(
}
}

/**
* asserts that this element's tag matches [tag].
*
* @throws Asn1TagMismatchException on failure
*/
@Throws(Asn1TagMismatchException::class)
inline fun <reified T : Asn1Element> 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 <reified T : Asn1Element> T.assertTag(tagNumber: ULong): T = assertTag(tag withNumber tagNumber)

object Asn1EncodableSerializer : KSerializer<Asn1Element> {
override val descriptor = PrimitiveSerialDescriptor("Asn1Encodable", PrimitiveKind.STRING)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -113,15 +114,15 @@ data class Pkcs10CertificationRequest(
val tbsCsr: TbsCertificationRequest,
val signatureAlgorithm: X509SignatureAlgorithm,
@Serializable(with = ByteArrayBase64Serializer::class)
val signature: ByteArray
val signature: CryptoSignature
) : Asn1Encodable<Asn1Sequence> {


@Throws(Asn1Exception::class)
override fun encodeToTlv() = Asn1.Sequence {
+tbsCsr
+signatureAlgorithm
+BitString(signature)
+BitString(signature.encodeToDer())
}

override fun equals(other: Any?): Boolean {
Expand All @@ -132,15 +133,15 @@ 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
}

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
}

Expand All @@ -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))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Asn1TagMismatchException> {
seq.assertTag(it + 1uL)
}
}
}
})
Original file line number Diff line number Diff line change
@@ -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<X509Certificate> {
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<Pkcs10CertificationRequest> {
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)
}
}
Loading

0 comments on commit 8f5cc2c

Please sign in to comment.