diff --git a/.github/workflows/publish-dry-run.yml b/.github/workflows/publish-dry-run.yml index cba56a5b6..fb54f50cd 100644 --- a/.github/workflows/publish-dry-run.yml +++ b/.github/workflows/publish-dry-run.yml @@ -38,6 +38,6 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install mkdocs-material + - run: pip install mkdocs-material==9.5.40 - name: Build Dokka HTML run: ./gradlew mkDocsBuild \ No newline at end of file diff --git a/.github/workflows/publish-pages-only.yml b/.github/workflows/publish-pages-only.yml index dffce8b9b..27953123d 100644 --- a/.github/workflows/publish-pages-only.yml +++ b/.github/workflows/publish-pages-only.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install mkdocs-material + - run: pip install mkdocs-material==9.5.40 - name: Build Dokka HTML run: ./gradlew mkDocsBuild - name: Setup Pages diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b12bfdf11..499c57d6b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,7 +41,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install mkdocs-material + - run: pip install mkdocs-material==9.5.40 - name: Build Dokka HTML run: ./gradlew mkDocsBuild - name: Setup Pages diff --git a/CHANGELOG.md b/CHANGELOG.md index d3957ead7..bddc96b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,15 @@ Better safe than sorry! * Base OIDs on BigInteger instead of UInt * Directly support UUID-based OID creation * Implement hash-to-curve and hash-to-scalar as per RFC9380 +* `JwsSignes.plainsignatureInput` is now a raw ByteArray + * `JwsSigned.prepareSignatureInput` now returns a raw ByteArray * Use kotlinx-io as primary source for parsing - * Base number encoding/decoding on koltinx-io + * Base number encoding/decoding on kotlinx-io * Remove parsing from iterator * Base ASN.1 encoding and decoding on kotlinx-io * Remove single element decoding from Iterator +* Introduce `prepareDigestInput()` to `IosHomebrewAttestation` +* Remove Legacy iOS Attestation ### 3.9.0 (Supreme 0.4.0) diff --git a/README.md b/README.md index d8f314ab6..b55ba080f 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ The very first bit means that you can verify signatures on the JVM, Android and ### Do check out the full manual with examples and API docs [here](https://a-sit-plus.github.io/signum/)! This README provides just an overview. -The full manual is more comprehensive, has separate sections for each module, and provides a full API documentation. +The full manual is more comprehensive, has separate sections for each module, provides examples, and a full API documentation. ## Using it in your Projects 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 e41fdd3cc..f331090ed 100644 --- a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt +++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt @@ -312,7 +312,7 @@ internal fun App() { is SignatureAlgorithm.ECDSA -> { this@createSigningKey.ec { curve = alg.requiredCurve ?: - ECCurve.entries.find { it.nativeDigest == alg.digest }!! + ECCurve.entries.find { it.nativeDigest == alg.digest }!! digests = setOf(alg.digest) } } diff --git a/docs/docs/examples.md b/docs/docs/examples.md new file mode 100644 index 000000000..82aca051a --- /dev/null +++ b/docs/docs/examples.md @@ -0,0 +1,317 @@ +# Signum Examples + +This page demonstrates how to accomplish common tasks using _Signum_. + +## Creating a Signed JSON Web Signature Object (`JwsSigned`) + +!!! info inline end + This example requires the _Supreme_ KMP crypto provider and _Indispensable Josef_. + +In this example, we'll start with an ephemeral P-256 signer: + +```kotlin +val signer = Signer.Ephemeral { + ec { curve = ECCurve.SECP_256_R_1 } +}.getOrThrow() //TODO handle error +``` + +Next up, we'll create a header and payload: + +```kotlin +val header = JwsHeader( + algorithm = signer.signatureAlgorithm.toJwsAlgorithm().getOrThrow(), + jsonWebKey = signer.publicKey.toJsonWebKey() +) +val payload = byteArrayOf(1, 3, 3, 7) +``` + +Since both header and payload are fed into the signature, we need to prepare this signature input: + +```kotlin +val plainSignatureInput = JwsSigned.prepareJwsSignatureInput(header, payload) +``` + +Now, everything is ready to be signed: + +```kotlin +val signature = signer.sign(plainSignatureInput).signature //TODO: handle error +JwsSigned(header, payload, signature, plainSignatureInput).serialize() // this we can verify on jwt.io +``` + +As can be seen, a `JwsSigned` takes header, payload, signature, and the plain signature input as parameters. +The reason for keeping this fourth parameter is convenience and efficiency: For one, you need this input to serialize a +`JwsSigned`, so it would be a waste to discard it. After parsing a `JswSigned` from its serialized form, you also need the +`plainSignatureInput` to verify everything was signed correctly. + + +## Creating a `CoseSigned` Object + +!!! info inline end + This example requires the _Supreme_ KMP crypto provider and _Indispensable Cosef_. + +In this example, we'll again start with an ephemeral P-256 signer: + +```kotlin +val signer = Signer.Ephemeral { + ec { curve = ECCurve.SECP_256_R_1 } +}.getOrThrow() //TODO handle error properly +``` + +Next up, we'll create a header and payload: + +```kotlin +//set KID + algorithm +val protectedHeader = CoseHeader( + algorithm = signer.signatureAlgorithm.toCoseAlgorithm().getOrElse { TODO() }, + kid = signer.publicKey.didEncoded.encodeToByteArray() +) + +val payload = byteArrayOf(0xC, 0xA, 0xF, 0xE) +``` + +Both of these are signature inputs, so we'll construct a `CoseSignatureInput` to sign. + +```kotlin +val signatureInput = CoseSignatureInput( + contextString = "Signature1", + protectedHeader = ByteStringWrapper(protectedHeader), + externalAad = byteArrayOf(), + payload = payload, +).serialize() +``` + + +Now, everything is ready to be signed: + +```kotlin +val signature = signer.sign(signatureInput).signature //TODO handle error + +val coseSigned = CoseSigned( + ByteStringWrapper(protectedHeader), + unprotectedHeader = null, + payload, + signature +).serialize() // sadly, there's no cwt.io, but you can use cbor.me to explore the signed data +``` + +## Create and Parse a Custom-Tagged ASN.1 Structure + +!!! info inline end + This example requires only the _Indispensable_ module. + +This example illustrates how to encapsulate a custom ASN.1 encoding scheme to make it reusable and composable. + +### Definitions + +Let's say you are using ASN.1 as your wire format for interoperability with different frameworks and languages. +This particular example demonstrates how log messages, i.e. the status of an operation, maybe from a smartcard, are sent off-device. + +!!! note inline end + Such constraints may seem artificial, but when bandwidth is low, a compact representation is key. + +A log message is an implicitly tagged ASN.1 structure with APPLICATION tag `26` and sequence semantics. +It contains the number of times an operation was run, and a timestamp, which can be either relative (in whole seconds since the last operation) +or absolute (UTC Time). +This relative/absolute flag uses the implicit APPLICATION tag `42` and the tuple of flag and time +is encoded into an ANS.1 OCTET STRING. This allows for two possible encodings, as illustrated below: + + + + + + + + + + + +
+Absolute Time + +Relative Time +
+ +```asn1 +Application 26 (2 elem) + INTEGER 1 + OCTET STRING (19 byte) + Application 42 (1 byte) 00 + UTCTime 2024-09-30 18:11:59 UTC +``` + + + +```asn1 +Application 26 (2 elem) + INTEGER 3 + OCTET STRING (7 byte) + Application 42 (1 byte) FF + INTEGER 39 +``` + +
+ +### Encoding + +We'll be assuming absolute time to keep things simple. +Hence, the structure containing an absolute time can be created using the _Indispensable_ ASN.1 engine as follows: + +```kotlin +val TAG_TIME_RELATIVE = 42uL withClass TagClass.APPLICATION + +Asn1.Sequence { + +Asn1.Int(1) + +OctetStringEncapsulating { + +(Bool(false) withImplicitTag TAG_TIME_RELATIVE) + +Asn1Time(Clock.System.now()) + } +} withImplicitTag (26uL withClass TagClass.APPLICATION) +// ↑ in reality this would be a constant ↑ +``` + +The HEX-equivalent of this structure (which can be obtained by calling `.toDerHexString()`) is +[7F8A391802010104135F2A0100170D3234303933303138313135395A](https://lapo.it/asn1js/#f4o5GAIBAQQTXyoBABcNMjQwOTMwMTgxMTU5Wg). + +### Parsing and Validating Tags + +Basic parsing is straight-forward: You have DER-encoded bytes, and feed them into `AsnElement.parse()`. +In this example, you examine the first child to get the number of times the operation was carried out; +then, you decode the first child of the OCTET STRING that follows to decide how to decode the second child. + +Usually, though (and especially when using implicit tags), you really want to verify those tags too. +Hence, parsing and properly validating is a bit more elaborate: + +```kotlin linenums="1" +Asn1Element.parse(customSequence.derEncoded).asStructure().let { root -> + + //↓↓↓ In reality, this would be a global constant; the same as in the previous snippet ↓↓↓ + val rootTag = Asn1Element.Tag(26uL, tagClass = TagClass.APPLICATION, constructed = true) + root.assertTag(rootTag) //throws on tag mismatch + + val numberOfOps = root.nextChild().asPrimitive().decodeToUInt() + root.nextChild().asEncapsulatingOctetString().let { timestamp -> + val isRelative = timestamp.nextChild().asPrimitive() + .decodeToBoolean(TAG_TIME_RELATIVE) + + val time = if (isRelative) timestamp.nextChild().asPrimitive().decodeToUInt() + else timestamp.nextChild().asPrimitive().decodeToInstant() + + if (timestamp.hasMoreChildren() || root.hasMoreChildren()) + throw Asn1StructuralException("Superfluous Content") + + // Everything is parsed and validated + TODO("Create domain object from $numberOfOps, $isRelative, and $time") + } +} +``` + +The above snippet performs the following validations: + +1. Line 5 asserts the tag of the root structure +2. Line 7 ensures that the first child is an ASN.1 primitive tagged as INT, containing an unsigned integer +3. Line 8 execution successfully guarantees that the second child is indeed an ASN.1 OCTET STRING encapsulating +another ASN.1 structure. +4. Lines 9-10 verify that the first child contained in the ASN.1 OCTET STRING + * is an ASN.1 primitive + * tagged with `TAG_TIME_RELATIVE` + * containing an ASN.1 boolean +5. Line 12 ensures that the next child is an ASN.1 primitive, encoding an unsigned integer (in case an `UInt` is expected) +6. Line 13 tackles the alternative and ensures that the next child contains a properly encoded ASN.1 time +7. Lastly, lines 15-16 make sure no additional content is present, thus fully verifying the structure as a whole. + + +## Issuing Binding Certificates + +!!! info inline end + This example requires the _Supreme_ KMP crypto provider. Only _Signum_-specifics are illustrated using code snippets. + +We'll assume a JVM backend using _[WARDEN](https://github.com/a-sit-plus/warden)_ and trust anchors all set up correctly +on the client apps. +A common pattern in a mobile client setting in the context of banking or eID are so-called _binding certificates_ (or binding keys, but we'll stick to certificates here). +Just assume a bank with a mobile client application: Customers are typically issued an activation token out-of-band +(via mail, by the teller, …). This token is used to activate the app and transactions can then be authorized using biometrics. + +In settings as critical as eID and banking, the service operator typically wants to ensure that only uncompromised clients +may access a service. To ensure this, the example described here relies on attestation. + +This process works more or less as follows: + +1. The client contacts the back-end to start the binding process +2. The back-end authenticates the binding request, identifying the customer. This could be a traditional authentication process, some out-of-band personalized token, etc. +3. The user enters this information into the client app and the app transmits this information to the back-end. +4. The back-end sends a challenge to the client +5. The client creates a new public-private key pair, using the challenge to also attest app, key, and the biometric authorization requirement (see [Attestation](supreme.md#attestation)). +```kotlin +val signer = PlatformSigningProvider.createSigningKey("binding") { + ec { curve = ECCurve.SECP_256_R_1 } + hardware { + backing = REQUIRED + attestation { challenge = challengeFromServer } + protection { + factors { biometry = true } + } + } +}.getOrElse { TODO("Handle error") } +``` +6. The client creates and signs a CSR for the key, which includes the challenge and an attestation proof +```kotlin +val tbsCSR = TbsCertificationRequest( + subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("client")))), + publicKey = signer.publicKey, + attributes = listOf( + Pkcs10CertificationRequestAttribute( + // No OID is assigned for this; choose one! + attestationOid, + // ↓↓↓ contains challenge ↓↓↓ + Asn1String.UTF8(signer.attestation!!.jsonEncoded).encodeToTlv() + ) + ) +) + +//extension function producing a signed CSR +val csr = signer.sign(tbsCSR).getOrElse { TODO("handle error") } +``` +7. The back-end verifies the signature of the CSR, and validates the challenge and attestation information +```kotlin +X509SignatureAlgorithm.ES256.verifierFor(csr.tbsCsr.publicKey) + .getOrElse { TODO("Handle error") } + .verify( + csr.tbsCsr.encodeToDer(), + CryptoSignature.decodeFromDer(csr.signature) + ).getOrElse { TODO("Abort here!") } + +val attestation = + csr.tbsCsr.attributes.firstOrNull { it.oid == attestationOid } + ?.value?.first() ?: TODO("Abort here!") +//TODO: feed attestation to WARDEN for verification +``` +8. The back-end issues and signs a _binding certificate_ for the CSR, and transmits it to the client. +```kotlin +val tbsCrt = TbsCertificate( + serialNumber = Random.nextBytes(16), + signatureAlgorithm = signer.signatureAlgorithm.toX509SignatureAlgorithm().getOrThrow(), + issuerName = backendIssuerName, + validFrom = Asn1Time(Clock.System.now()), + validUntil = Asn1Time(Clock.System.now() + VALIDITY), + subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8("client")))), + publicKey = csr.tbsCsr.publicKey, //client public key + extensions = listOf( + // we want to indicate, that this client passed attestation checks + X509CertificateExtension( + attestedClientOid, + critical = true, + Asn1PrimitiveOctetString(byteArrayOf()) + ) + ) +) + +val clientCertificate = signer.sign(tbsCrt).getOrElse { TODO("handle error") } +``` +9. The client stores the certificate. + +To recap: This example shows how to +* instantiate a signer for a hardware-backed, biometry-protected, attested key +* instantiate a verifier +* create, sign and verify CSRs with a custom attribute +* extract a custom attribute from a CSR +* create, and sign a certificate with a custom critical extension \ No newline at end of file diff --git a/docs/docs/indispensable.md b/docs/docs/indispensable.md index 5181e7e3a..beca971c8 100644 --- a/docs/docs/indispensable.md +++ b/docs/docs/indispensable.md @@ -225,7 +225,7 @@ The base decoding function is called `decode()` and has the following signature: fun Asn1Primitive.decode(assertTag: ULong, transform: (content: ByteArray) -> T): T ``` An alternative exists, taking a `Tag` instead of an `Ulong`. in both cases a tag to assert and a user-defined transformation function is expected, which operates on -the content of the ASN.1 primitive. Moreover, npn-throwing `decodeOrNull` variant is present. +the content of the ASN.1 primitive. Moreover, non-throwing `decodeOrNull` variant is present. In addition, the following self-describing shorthands are defined: * `Asn1Primitive.decodeToBoolean()` throws on error diff --git a/docs/docs/stylesheets/extra.css b/docs/docs/stylesheets/extra.css index fb144e995..fb4b2a53d 100644 --- a/docs/docs/stylesheets/extra.css +++ b/docs/docs/stylesheets/extra.css @@ -67,6 +67,20 @@ font-feature-settings: "kern", "liga"; } +.linenodiv { + padding-top: .12rem; +} + + .linenodiv .normal { + font-size: .8rem; + font-feature-settings: "kern", "liga"; + } + +.md-typeset h2 code { + font-size: 1.2rem; + font-feature-settings: "kern", "liga"; +} + @media screen and (max-width: 76.2344em) { .md-nav--primary .md-nav__title { height: 3.9rem; diff --git a/docs/docs/supreme.md b/docs/docs/supreme.md index d31bef4e2..0672b0ce7 100644 --- a/docs/docs/supreme.md +++ b/docs/docs/supreme.md @@ -9,7 +9,6 @@ types and functionality related to crypto and PKI applications: * **Multiplatform ECDSA and RSA Signer and Verifier** → Check out the included [CMP demo App](https://github.com/a-sit-plus/signum/tree/main/demoapp) to see it in action -* Supports Attestation on iOS and Android * Biometric Authentication on Android and iOS without Callbacks or Activity Passing** (✨Magic!✨) * Support Attestation on Android and iOS diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 794a6706d..8f75d29d3 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -71,6 +71,7 @@ nav: - Manual: - Overview: index.md - CHANGELOG.md + - Examples: examples.md - Modules: - Indispensable: indispensable.md - Indispensable Josef: indispensable-josef.md diff --git a/indispensable-josef/build.gradle.kts b/indispensable-josef/build.gradle.kts index e57d3e9d7..619222cf2 100644 --- a/indispensable-josef/build.gradle.kts +++ b/indispensable-josef/build.gradle.kts @@ -39,6 +39,7 @@ kotlin { jvmTest { dependencies { implementation(libs.jose) + implementation(project(":supreme")) } } } diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt index 0f67c0f4a..99b9eb05d 100644 --- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt +++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt @@ -16,11 +16,11 @@ data class JwsSigned( val header: JwsHeader, val payload: ByteArray, val signature: CryptoSignature.RawByteEncodable, - val plainSignatureInput: String, + val plainSignatureInput: ByteArray, ) { fun serialize(): String { - return "${plainSignatureInput}.${signature.rawByteArray.encodeToString(Base64UrlStrict)}" + return "${plainSignatureInput.decodeToString()}.${signature.rawByteArray.encodeToString(Base64UrlStrict)}" } override fun equals(other: Any?): Boolean { @@ -45,7 +45,7 @@ data class JwsSigned( return "JwsSigned(header=$header" + ", payload=${payload.encodeToString(Base64UrlStrict)}" + ", signature=$signature" + - ", plainSignatureInput='$plainSignatureInput')" + ", plainSignatureInput='${plainSignatureInput.decodeToString()}')" } @@ -65,19 +65,18 @@ data class JwsSigned( else -> CryptoSignature.EC.fromRawBytes(curve, bytes) } } - val plainSignatureInput = stringList[0] + "." + stringList[1] + val plainSignatureInput = (stringList[0] + "." + stringList[1]).encodeToByteArray() JwsSigned(header, payload, signature, plainSignatureInput) } - /** * Called by JWS signing implementations to get the string that will be * used as the input for signature calculation */ @Suppress("unused") - fun prepareJwsSignatureInput(header: JwsHeader, payload: ByteArray): String = - "${header.serialize().encodeToByteArray().encodeToString(Base64UrlStrict)}" + - ".${payload.encodeToString(Base64UrlStrict)}" + fun prepareJwsSignatureInput(header: JwsHeader, payload: ByteArray): ByteArray = + ("${header.serialize().encodeToByteArray().encodeToString(Base64UrlStrict)}" + + ".${payload.encodeToString(Base64UrlStrict)}").encodeToByteArray() } } diff --git a/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/JwsSignedTest.kt b/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/JwsSignedTest.kt index db93991ca..71c1c5722 100644 --- a/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/JwsSignedTest.kt +++ b/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/JwsSignedTest.kt @@ -1,7 +1,10 @@ package at.asitplus.signum.indispensable.josef import at.asitplus.signum.indispensable.CryptoPublicKey +import at.asitplus.signum.indispensable.ECCurve import at.asitplus.signum.indispensable.getJcaPublicKey +import at.asitplus.signum.supreme.sign.Signer +import at.asitplus.signum.supreme.signature import com.nimbusds.jose.JWSObject import com.nimbusds.jose.crypto.ECDSAVerifier import com.nimbusds.jose.crypto.RSASSAVerifier @@ -30,4 +33,21 @@ class JwsSignedTest : FreeSpec({ result.shouldBeTrue() } } + + "JWS example" { + val signer = Signer.Ephemeral { + ec { curve = ECCurve.SECP_256_R_1 } + }.getOrThrow() //TODO handle error + + val header = JwsHeader( + algorithm = signer.signatureAlgorithm.toJwsAlgorithm().getOrThrow(), + jsonWebKey = signer.publicKey.toJsonWebKey() + ) + val payload = byteArrayOf(1, 3, 3, 7) + + val plainSignatureInput = JwsSigned.prepareJwsSignatureInput(header, payload) + + val signature = signer.sign(plainSignatureInput).signature //TODO: handle error + println(JwsSigned(header, payload, signature, plainSignatureInput).serialize())// this we can verify on jwt.io + } }) 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 fccd4a541..0d661bbf0 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 @@ -480,7 +480,8 @@ sealed class Asn1Structure( fun hasMoreChildren() = children.size > index /** - * Returns the current child (useful when iterating over this structure's children) + * Returns the current child or `null`, if there are no children left + * (useful when iterating over this structure's children). */ fun peek() = if (!hasMoreChildren()) null else children[index] diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt index dbaa2d6ab..14c68c626 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt @@ -169,65 +169,82 @@ private fun Asn1Element.Tag.isSequence() = (this == Asn1Element.Tag.SEQUENCE) /** - * decodes this [Asn1Primitive]'s content into an [Boolean] + * decodes this [Asn1Primitive]'s content into an [Boolean]. [assertTag] defaults to [Asn1Element.Tag.BOOL], but can be + * overridden (for implicitly tagged booleans, for example) * @throws [Asn1Exception] all sorts of exceptions on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.decodeToBoolean() = - runRethrowing { decode(Asn1Element.Tag.BOOL) { Boolean.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToBoolean(assertTag: Asn1Element.Tag = Asn1Element.Tag.BOOL) = + runRethrowing { decode(assertTag) { Boolean.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [decodeToBoolean] */ -fun Asn1Primitive.decodeToBooleanOrNull() = catching { decodeToBoolean() }.getOrNull() +fun Asn1Primitive.decodeToBooleanOrNull(assertTag: Asn1Element.Tag = Asn1Element.Tag.BOOL) = + catching { decodeToBoolean(assertTag) }.getOrNull() /** - * decodes this [Asn1Primitive]'s content into an [Int] + * decodes this [Asn1Primitive]'s content into an [Int]. [assertTag] defaults to [Asn1Element.Tag.INT], but can be + * overridden (for implicitly tagged integers, for example) * @throws [Asn1Exception] on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.decodeToInt() = runRethrowing { decode(Asn1Element.Tag.INT) { Int.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToInt(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + runRethrowing { decode(assertTag) { Int.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [decodeToInt] */ -fun Asn1Primitive.decodeToIntOrNull() = catching { decodeToInt() }.getOrNull() +fun Asn1Primitive.decodeToIntOrNull(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + catching { decodeToInt(assertTag) }.getOrNull() /** - * decodes this [Asn1Primitive]'s content into a [Long] + * decodes this [Asn1Primitive]'s content into a [Long]. [assertTag] defaults to [Asn1Element.Tag.INT], but can be + * overridden (for implicitly tagged longs, for example) * @throws [Asn1Exception] on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.decodeToLong() = runRethrowing { decode(Asn1Element.Tag.INT) { Long.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToLong(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + runRethrowing { decode(assertTag) { Long.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [decodeToLong] */ -inline fun Asn1Primitive.decodeToLongOrNull() = catching { decodeToLong() }.getOrNull() +inline fun Asn1Primitive.decodeToLongOrNull(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + catching { decodeToLong(assertTag) }.getOrNull() /** - * decodes this [Asn1Primitive]'s content into an [UInt] + * decodes this [Asn1Primitive]'s content into an [UInt]√. [assertTag] defaults to [Asn1Element.Tag.INT], but can be + * overridden (for implicitly tagged unsigned integers, for example) * @throws [Asn1Exception] on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.decodeToUInt() = runRethrowing { decode(Asn1Element.Tag.INT) { UInt.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToUInt(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + runRethrowing { decode(assertTag) { UInt.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [decodeToUInt] */ -inline fun Asn1Primitive.decodeToUIntOrNull() = catching { decodeToUInt() }.getOrNull() +inline fun Asn1Primitive.decodeToUIntOrNull(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + catching { decodeToUInt(assertTag) }.getOrNull() /** - * decodes this [Asn1Primitive]'s content into an [ULong] + * decodes this [Asn1Primitive]'s content into an [ULong]. [assertTag] defaults to [Asn1Element.Tag.INT], but can be + * overridden (for implicitly tagged unsigned longs, for example) * @throws [Asn1Exception] on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.decodeToULong() = - runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToULong(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + runRethrowing { decode(assertTag) { ULong.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [decodeToULong] */ -inline fun Asn1Primitive.decodeToULongOrNull() = catching { decodeToULong() }.getOrNull() +inline fun Asn1Primitive.decodeToULongOrNull(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + catching { decodeToULong(assertTag) }.getOrNull() -/** Decode the [Asn1Primitive] as a [BigInteger] - * @throws [Asn1Exception] on invalid input */ +/** + * Decode the [Asn1Primitive] as a [BigInteger]. [assertTag] defaults to [Asn1Element.Tag.INT], but can be + * overridden (for implicitly tagged integers, for example) + * @throws [Asn1Exception] on invalid input + */ @Throws(Asn1Exception::class) -fun Asn1Primitive.decodeToBigInteger() = - runRethrowing { decode(Asn1Element.Tag.INT) { BigInteger.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToBigInteger(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + runRethrowing { decode(assertTag) { BigInteger.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [decodeToBigInteger] */ -inline fun Asn1Primitive.decodeToBigIntegerOrNull() = catching { decodeToBigInteger() }.getOrNull() +inline fun Asn1Primitive.decodeToBigIntegerOrNull(assertTag: Asn1Element.Tag = Asn1Element.Tag.INT) = + catching { decodeToBigInteger(assertTag) }.getOrNull() /** * transforms this [Asn1Primitive] into an [Asn1String] subtype based on its tag 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 b3c02df9b..40f44ca3f 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 @@ -25,7 +25,7 @@ data class TbsCertificationRequest( val version: Int = 0, val subjectName: List, val publicKey: CryptoPublicKey, - val attributes: List? = null + val attributes: List = listOf() ) : Asn1Encodable { /** @@ -57,7 +57,7 @@ data class TbsCertificationRequest( //subject Public Key +publicKey - +ExplicitlyTagged(0u) { attributes?.map { +it } } + +ExplicitlyTagged(0u) { attributes.map { +it } } } @@ -79,7 +79,7 @@ data class TbsCertificationRequest( var result = version result = 31 * result + subjectName.hashCode() result = 31 * result + publicKey.hashCode() - result = 31 * result + (attributes?.hashCode() ?: 0) + result = 31 * result + (attributes.hashCode()) return result } diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/ImplicitTaggingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/ImplicitTaggingTest.kt index 50373a59c..811225a4c 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/ImplicitTaggingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/ImplicitTaggingTest.kt @@ -1,9 +1,11 @@ package at.asitplus.signum.indispensable.asn1 -import at.asitplus.signum.indispensable.asn1.TagClass.* import at.asitplus.signum.indispensable.asn1.Asn1Element.Tag.Template.Companion.withClass import at.asitplus.signum.indispensable.asn1.Asn1Element.Tag.Template.Companion.without -import at.asitplus.signum.indispensable.asn1.encoding.parse +import at.asitplus.signum.indispensable.asn1.TagClass.CONTEXT_SPECIFIC +import at.asitplus.signum.indispensable.asn1.TagClass.UNIVERSAL +import at.asitplus.signum.indispensable.asn1.encoding.* +import com.ionspin.kotlin.bignum.integer.BigInteger import io.kotest.core.spec.style.FreeSpec import io.kotest.datatest.withData import io.kotest.matchers.booleans.shouldBeFalse @@ -66,6 +68,17 @@ class ImplicitTaggingTest : FreeSpec({ (primitive withImplicitTag tagNum).tag.tagClass shouldBe CONTEXT_SPECIFIC + "convenience $tagNum" { + val tag = Asn1Element.Tag(tagNum, constructed = false) + (Asn1.Bool(true) withImplicitTag tag).asPrimitive().decodeToBooleanOrNull(tag) shouldBe true + (Asn1.Int(1337) withImplicitTag tag).asPrimitive().decodeToIntOrNull(tag) shouldBe 1337 + (Asn1.Int(1337u) withImplicitTag tag).asPrimitive().decodeToUIntOrNull(tag) shouldBe 1337u + (Asn1.Int(1337L) withImplicitTag tag).asPrimitive().decodeToLongOrNull(tag) shouldBe 1337L + (Asn1.Int(1337uL) withImplicitTag tag).asPrimitive().decodeToULongOrNull(tag) shouldBe 1337uL + (Asn1.Int(BigInteger(1337)) withImplicitTag tag).asPrimitive().decodeToBigIntegerOrNull(tag) shouldBe BigInteger(1337) + } + + withData(nameFn = { "$tagNum $it" }, TagClass.entries) { tagClass -> val newTagValue = tagNum / 2uL;