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;