Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/api polishing #150

Merged
merged 8 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 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) {
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
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
)
}
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
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 {
iaik-jheher marked this conversation as resolved.
Show resolved Hide resolved
Pkcs10CertificationRequest(tbsCsr, toX509SignatureAlgorithm, it)
}
}
Loading
Loading