Skip to content

Commit b8341cf

Browse files
authored
Cleanup/alg specific foo (#46)
* move RsaSsaPssParams accessor to its companion * externalize EcdsaSigValue parsing (r/s) * implement PR feedback * clean up X509AlgorithmIdentifier * add kdoc + changelog + spotlessApply * indentation
1 parent 3079805 commit b8341cf

12 files changed

Lines changed: 124 additions & 76 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
## NEXT
44
* Allow `Asn1` builder unary `+` for transparent wrappers around `Asn1Element`/`Asn1Encodable`, and for serializable
55
values when a `Der` instance is in context.
6+
* Change the `X509AlgorithmIdentifier` constructor to take a single nullable `parameters` element.
7+
* Previously, it took a `List` that could only reasonably have 0-1 elements, enforced by the `parameters` getter. Deprecated that constructor variant.
8+
* Clean up algorithm-specific parsers and move them out of the generic element they parse
9+
* `RsaSsaPssParams`:
10+
* `X509AlgorithmIdentifier.rsaSsaPssParams` -> `RsaSsaPssParams.of(X509AlgorithmIdentifier)`
11+
* new extension on `RsaSsaPssParams` companion: `X509AlgorithmIdentifier.of(RsaSsaPssParams)`
12+
* `EcdsaSigValue`:
13+
* new class: `EcdsaSigValue` models `ECDSA-Sig-Value` from RFC 5480
14+
* `X509SignatureValue.decodeRS()` in class -> `X509SignatureValue.toEcdsaSigValue()` on `EcdsaSigValue` companion
15+
* `X509SignatureValue.fromRS()` -> `EcdsaSigValue.toX509SignatureValue()`
616

717
## 0.6.1
818
* **Fixes:**
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-FileCopyrightText: Copyright (c) A-SIT Plus GmbH
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package at.asitplus.awesn1.crypto
5+
6+
import at.asitplus.awesn1.Asn1Integer
7+
import at.asitplus.awesn1.serialization.DER
8+
import at.asitplus.awesn1.serialization.Der
9+
import at.asitplus.awesn1.serialization.decodeFromDer
10+
import kotlinx.serialization.Serializable
11+
import kotlinx.serialization.encodeToByteArray
12+
13+
/**
14+
* ECDSA-Sig-Value as specified by
15+
* [RFC 5480, Appendix A](https://www.rfc-editor.org/rfc/rfc5480.html#page-18):
16+
*
17+
* ```
18+
* ECDSA-Sig-Value ::= SEQUENCE {
19+
* r INTEGER,
20+
* s INTEGER
21+
* }
22+
* ```
23+
*/
24+
@Serializable
25+
data class EcdsaSigValue(val r: Asn1Integer, val s: Asn1Integer) {
26+
fun toX509SignatureValue(der: Der = DER) = X509SignatureValue(der.encodeToByteArray(this))
27+
companion object {
28+
fun X509SignatureValue.toEcdsaSigValue(der: Der = DER) = der.decodeFromDer<EcdsaSigValue>(rawBytes)
29+
}
30+
}
31+

crypto/src/commonMain/kotlin/at/asitplus/awesn1/crypto/Pkcs8PrivateKeyInfo.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ data class Pkcs8PrivateKeyInfo(
6464
fun rsa(privateKey: Pkcs1RsaPrivateKeyInfo, attributes: Set<Asn1Element>? = null): Pkcs8PrivateKeyInfo =
6565
Pkcs8PrivateKeyInfo(
6666
version = Version.V1,
67-
privateKeyAlgorithm = X509AlgorithmIdentifier(RSA_ENCRYPTION_OID, listOf(Asn1.Null())),
67+
privateKeyAlgorithm = X509AlgorithmIdentifier(RSA_ENCRYPTION_OID, Asn1.Null()),
6868
privateKey = Asn1.OctetStringEncapsulating { +DER.encodeToTlv(privateKey) },
6969
attributes = attributes,
7070
)
@@ -77,7 +77,7 @@ data class Pkcs8PrivateKeyInfo(
7777
version = Version.V1,
7878
privateKeyAlgorithm = X509AlgorithmIdentifier(
7979
EC_PUBLIC_KEY_OID,
80-
curveOid?.let { listOf(it.encodeToTlv()) }.orEmpty(),
80+
curveOid?.encodeToTlv(),
8181
),
8282
privateKey = Asn1.OctetStringEncapsulating { +DER.encodeToTlv(sec1Key) },
8383
attributes = attributes,

crypto/src/commonMain/kotlin/at/asitplus/awesn1/crypto/RsaSsaPssParams.kt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@
33

44
package at.asitplus.awesn1.crypto
55

6+
import at.asitplus.awesn1.Asn1Exception
67
import at.asitplus.awesn1.Asn1Integer
78
import at.asitplus.awesn1.ObjectIdentifier
89
import at.asitplus.awesn1.encoding.Asn1
10+
import at.asitplus.awesn1.runRethrowing
911
import at.asitplus.awesn1.serialization.Asn1Tag
12+
import at.asitplus.awesn1.serialization.DER
13+
import at.asitplus.awesn1.serialization.Der
1014
import at.asitplus.awesn1.serialization.ExplicitlyTagged
15+
import at.asitplus.awesn1.serialization.decodeFromTlv
16+
import at.asitplus.awesn1.serialization.encodeToTlv
1117
import at.asitplus.awesn1.serialization.getValue
1218
import at.asitplus.awesn1.toInt
1319
import kotlinx.serialization.Serializable
@@ -115,8 +121,36 @@ data class RsaSsaPssParams internal constructor(
115121
const val DEFAULT_SALT_LENGTH = 20
116122
const val DEFAULT_TRAILER_FIELD = 1
117123

118-
val SHA1_IDENTIFIER = X509AlgorithmIdentifier(SHA1_OID, listOf(Asn1.Null()))
119-
val MGF1_SHA1_IDENTIFIER = X509AlgorithmIdentifier(MGF1_OID, listOf(SHA1_IDENTIFIER.element))
124+
val SHA1_IDENTIFIER = X509AlgorithmIdentifier(SHA1_OID, Asn1.Null())
125+
val MGF1_SHA1_IDENTIFIER = X509AlgorithmIdentifier(MGF1_OID, SHA1_IDENTIFIER.element)
126+
127+
fun X509AlgorithmIdentifier.Companion.of(params: RsaSsaPssParams, der: Der = DER) = runRethrowing {
128+
X509AlgorithmIdentifier(
129+
RSA_SSA_PSS_OID,
130+
der.encodeToTlv(params)
131+
)
132+
}
133+
134+
@Deprecated(level = DeprecationLevel.WARNING, message = "prefer of(), which can take a `Der` object",
135+
replaceWith = ReplaceWith("RsaSsaPssParams.of(this)"))
136+
val X509AlgorithmIdentifier.rsaSsaPssParams get() = RsaSsaPssParams.of(this)
137+
/**
138+
* Asserts that this identifier uses the `id-RSASSA-PSS` OID,
139+
* then parses [parameters] as RSASSA-PSS parameters.
140+
*
141+
* This helper models [RFC 4055, section 3.1](https://www.rfc-editor.org/rfc/rfc4055.html#section-3.1).
142+
*
143+
* @throws Asn1Exception if this algorithm is RSA_SSA_PSS has no parameters, or the parameter element is
144+
* not a valid `RSASSA-PSS-params` SEQUENCE.
145+
*/
146+
fun of(algorithmIdentifier: X509AlgorithmIdentifier, der: Der = DER): RsaSsaPssParams = runRethrowing {
147+
require(algorithmIdentifier.oid == RSA_SSA_PSS_OID)
148+
der.decodeFromTlv<RsaSsaPssParams>(
149+
algorithmIdentifier.parameters?.asSequence() ?:
150+
throw Asn1Exception("RSASSA-PSS AlgorithmIdentifier has no parameters")
151+
)
152+
}
153+
120154
}
121155

122156
override fun equals(other: Any?): Boolean {

crypto/src/commonMain/kotlin/at/asitplus/awesn1/crypto/SubjectPublicKeyInfo.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ data class SubjectPublicKeyInfo(
6262
fun rsa(publicKey: Pkcs1RsaPublicKeyInfo): SubjectPublicKeyInfo = SubjectPublicKeyInfo(
6363
algorithmIdentifier = X509AlgorithmIdentifier(
6464
RSA_ENCRYPTION_OID,
65-
listOf(Asn1.Null())
65+
Asn1.Null()
6666
),
6767
subjectPublicKey = Asn1BitString(DER.encodeToTlv(publicKey).derEncoded)
6868
)
@@ -71,7 +71,7 @@ data class SubjectPublicKeyInfo(
7171
rsa(Pkcs1RsaPublicKeyInfo(modulus, exponent))
7272

7373
fun ec(curveOid: ObjectIdentifier, ansiX963Key: ByteArray): SubjectPublicKeyInfo = SubjectPublicKeyInfo(
74-
algorithmIdentifier = X509AlgorithmIdentifier(EC_PUBLIC_KEY_OID, listOf(curveOid.encodeToTlv())),
74+
algorithmIdentifier = X509AlgorithmIdentifier(EC_PUBLIC_KEY_OID, curveOid.encodeToTlv()),
7575
subjectPublicKey = Asn1BitString(ansiX963Key)
7676
)
7777
}

crypto/src/commonMain/kotlin/at/asitplus/awesn1/crypto/X509AlgorithmIdentifier.kt

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ package at.asitplus.awesn1.crypto
66
import at.asitplus.awesn1.*
77
import at.asitplus.awesn1.encoding.Asn1
88
import at.asitplus.awesn1.encoding.WrappedElement
9-
import at.asitplus.awesn1.serialization.DER
10-
import at.asitplus.awesn1.serialization.decodeFromTlv
119
import kotlinx.serialization.Serializable
1210
import kotlin.experimental.ExperimentalObjCRefinement
1311
import kotlin.jvm.JvmInline
@@ -34,28 +32,27 @@ value class X509AlgorithmIdentifier(override val element: Asn1Sequence) : Identi
3432
* Convenience constructor for creating an instance of `X509AlgorithmIdentifier`
3533
* using an `ObjectIdentifier` and a list of `Asn1Element` parameters.
3634
*
37-
* The passed [parameters] are unrolled, making construction of the algorithm identifier object work as follows:
38-
* ```
39-
* Asn1.Sequence {
40-
* +oid
41-
* parameters.forEach { +it }
42-
* }
43-
* ```
35+
* **Note that passing `null` as [parameters] is different from passing [Asn1Null] as [parameters].**
36+
* Passing `null` omits the second member from the sequence entirely. (e.g., ECDSA)
37+
* Passing [Asn1Null] encodes ASN.1 NULL as the second member of the sequence. (e.g., RSA/PKCS1)
4438
*
4539
* @param oid The object identifier representing the algorithm.
46-
* @param parameters A list of ASN.1 elements representing the algorithm parameters.
40+
* @param parameters The algorithm parameters element, if any.
4741
*/
4842
constructor(
4943
oid: ObjectIdentifier,
50-
parameters: List<Asn1Element>
44+
parameters: Asn1Element?
5145
) : this(Asn1.Sequence {
5246
+oid
53-
parameters.forEach { +it }
47+
parameters?.let { +it }
5448
})
5549

50+
@Deprecated(level = DeprecationLevel.WARNING, message = "parameters can only have 0 or 1 elements, use nullable ctor",
51+
replaceWith = ReplaceWith("X509AlgorithmIdentifier(oid, parameters.singleOrNull())"))
52+
constructor(oid: ObjectIdentifier, parameters: List<Asn1Element>) : this(oid, parameters.singleOrNull())
53+
5654
init {
57-
require(element.children.isNotEmpty()) { "AlgorithmIdentifier must not be an empty SEQUENCE" }
58-
oid //check that oid is present
55+
val _ = oid //check that oid is present
5956
}
6057

6158
//already throws during init, so no throws declaration here
@@ -84,36 +81,7 @@ value class X509AlgorithmIdentifier(override val element: Asn1Sequence) : Identi
8481
get() = when (element.children.size) {
8582
1 -> null
8683
2 -> element.children[1]
87-
else -> throw Asn1Exception("AlgorithmIdentifier has ${element.children.size} children")
88-
}
89-
90-
/**
91-
* Parses [parameters] as RSASSA-PSS parameters if this identifier uses the `id-RSASSA-PSS` OID.
92-
*
93-
* This helper models [RFC 4055, section 3.1](https://www.rfc-editor.org/rfc/rfc4055.html#section-3.1) without
94-
* making [X509AlgorithmIdentifier] itself enforce algorithm-specific parameter schemas during generic DER parsing.
95-
*
96-
* @return `null` if this algorithm is nor RSA_SSA_PSS
97-
*
98-
* @throws Asn1Exception if this algorithm is RSA_SSA_PSS has no parameters, or the parameter element is
99-
* not a valid `RSASSA-PSS-params` SEQUENCE.
100-
*
101-
* From Swift/Objective-C use the throwing `rsaSsaPssParams()` accessor (exported as a static
102-
* `rsaSsaPssParams(_:)`, since value classes are not bridged as Objective-C types).
103-
*/
104-
@OptIn(ExperimentalObjCRefinement::class)
105-
@Suppress("WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET")
106-
@get:Throws(Asn1Exception::class)
107-
@HiddenFromObjC
108-
@get:HiddenFromObjC
109-
val rsaSsaPssParams: RsaSsaPssParams?
110-
get() = runWrappingAs(a = ::Asn1Exception) {
111-
if (oid != RsaSsaPssParams.RSA_SSA_PSS_OID) {
112-
return null
113-
}
114-
DER.decodeFromTlv<RsaSsaPssParams>(
115-
parameters?.asSequence() ?: throw Asn1Exception("RSASSA-PSS AlgorithmIdentifier has no parameters")
116-
)
84+
else -> throw Asn1Exception("AlgorithmIdentifier has ${element.children.size} (> 2) children")
11785
}
11886

11987
override fun toString(): String {

crypto/src/commonMain/kotlin/at/asitplus/awesn1/crypto/X509SignatureValue.kt

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,4 @@ value class X509SignatureValue(val rawBitString: Asn1BitString): WrappedEncodabl
4949
constructor(rawBytes: ByteArray) : this(Asn1BitString(rawBytes))
5050

5151
val rawBytes: ByteArray get() = rawBitString.bitCarryingBytes
52-
53-
// runRethrowing: a malformed ECDSA-Sig-Value (fewer than two children, or non-positive integers) would
54-
// otherwise leak NoSuchElementException/ClassCastException instead of a catchable Asn1Exception.
55-
@Throws(Asn1Exception::class)
56-
fun decodeRS(): Pair<Asn1Integer.Positive, Asn1Integer.Positive> = runRethrowing {
57-
Asn1Element.parse(rawBytes).asSequence().decodeAs {
58-
next().asPrimitive().decodeToAsn1Integer() as Asn1Integer.Positive to
59-
next().asPrimitive().decodeToAsn1Integer() as Asn1Integer.Positive
60-
}
61-
}
62-
63-
companion object {
64-
fun fromRS(r: Asn1Integer.Positive, s: Asn1Integer.Positive) =
65-
X509SignatureValue(Asn1.Sequence { +r; +s }.derEncoded)
66-
}
6752
}
68-
69-
fun X509SignatureValue.decodeRsOrNull() = catchingUnwrapped { decodeRS() }.getOrNull()

crypto/src/commonTest/kotlin/at/asitplus/awesn1/crypto/CryptoDerRoundTripTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private fun randomBitStringSignatureValue(random: Random) =
8484
X509SignatureValue(randomBytes(random))
8585

8686
private fun randomEcdsaSignatureValue(random: Random) =
87-
X509SignatureValue.fromRS(positiveAsn1Integer(random), positiveAsn1Integer(random))
87+
EcdsaSigValue(positiveAsn1Integer(random), positiveAsn1Integer(random)).toX509SignatureValue()
8888

8989
private fun randomEcPrivateKey(random: Random) = Sec1EcPrivateKeyInfo(
9090
privateKey = randomBytes(random, 32),
@@ -95,7 +95,7 @@ private fun randomEcPrivateKey(random: Random) = Sec1EcPrivateKeyInfo(
9595
private fun randomEncryptedPrivateKeyInfo(random: Random) = EncryptedPrivateKeyInfo(
9696
encryptionAlgorithm = X509AlgorithmIdentifier(
9797
oid = randomOid(random),
98-
parameters = randomRawElement(random).takeIf { random.nextBoolean() }?.let { listOf(it) }?:listOf(),
98+
parameters = randomRawElement(random).takeIf { random.nextBoolean() },
9999
),
100100
encryptedData = if (random.nextBoolean()) Asn1EncapsulatingOctetString(
101101
listOf(Asn1OctetString(randomBytes(random, 32)))
@@ -128,7 +128,7 @@ private fun randomRsaPublicKey(random: Random) = Pkcs1RsaPublicKeyInfo(
128128

129129
private fun randomSignatureAlgorithmIdentifier(random: Random) = X509AlgorithmIdentifier(
130130
oid = randomOid(random),
131-
parameters = List(random.nextInt(0, 3)) { randomRawElement(random) },
131+
parameters = randomRawElement(random),
132132
)
133133

134134
private fun randomSubjectPublicKeyInfo(random: Random): SubjectPublicKeyInfo =
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package at.asitplus.awesn1.crypto
2+
3+
import at.asitplus.awesn1.Asn1Integer
4+
import at.asitplus.awesn1.crypto.EcdsaSigValue.Companion.toEcdsaSigValue
5+
import at.asitplus.awesn1.serialization.DER
6+
import at.asitplus.awesn1.serialization.decodeFromDer
7+
import at.asitplus.testballoon.matrix.matrixSuite
8+
import io.kotest.matchers.shouldBe
9+
import kotlin.io.encoding.Base64
10+
11+
val EcdsaSigValueTest by matrixSuite {
12+
"Test" {
13+
val sigValue = Base64.UrlSafe.decode("A0kAMEYCIQCnXiAKLwJP0uXBKtTmJccBu" +
14+
"yddhFFVTz-J0DNHBi21lgIhAI1SUoIYXXqdZMrKox4_HBTEmuxvG9sloAoDH5rfsyd4")
15+
.let { DER.decodeFromDer<X509SignatureValue>(it) }
16+
sigValue.toEcdsaSigValue().let {
17+
it.r.toString() shouldBe "75702550467927847687504835259358957068594330833158195591194084637648375100822"
18+
it.s.toString() shouldBe "63921562560111841846899158949481573510824775155107323853077162713843342976888"
19+
}
20+
}
21+
}

crypto/src/commonTest/kotlin/at/asitplus/awesn1/crypto/LegacyRegression.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import at.asitplus.awesn1.Asn1Time
44
import at.asitplus.awesn1.Asn1Element
55
import at.asitplus.awesn1.Asn1Integer
66
import at.asitplus.awesn1.Asn1Integer.Sign
7+
import at.asitplus.awesn1.crypto.X509AlgorithmIdentifier
78
import at.asitplus.awesn1.encoding.parse
89
import at.asitplus.awesn1.crypto.legacy.EcPrivateKeyInfo as LegacyEcPrivateKeyInfo
910
import at.asitplus.awesn1.crypto.legacy.EncryptedPrivateKeyInfo as LegacyEncryptedPrivateKeyInfo
@@ -86,7 +87,7 @@ private fun LegacyEncryptedPrivateKeyInfo.toCurrent() =
8687
private fun LegacyPkcs8PrivateKeyInfo.toCurrent() =
8788
Pkcs8PrivateKeyInfo(
8889
Pkcs8PrivateKeyInfo.Version.V1,
89-
privateKeyAlgorithm = X509AlgorithmIdentifier(algorithmOid, algorithmParameters),
90+
privateKeyAlgorithm = X509AlgorithmIdentifier(algorithmOid, algorithmParameters.singleOrNull()),
9091
privateKey = privateKey,
9192
attributes = attributes?.toSet(),
9293
)
@@ -121,14 +122,14 @@ private fun LegacyRsaPublicKeyInfo.toCurrent() =
121122
private fun LegacySignatureAlgorithmIdentifier.toCurrent() =
122123
X509AlgorithmIdentifier(
123124
oid = oid,
124-
parameters = parameters,
125+
parameters = parameters.singleOrNull(),
125126
)
126127

127128
private fun LegacySignatureValue.toCurrent() = X509SignatureValue(rawBitString)
128129

129130
private fun LegacySubjectPublicKeyInfo.toCurrent() =
130131
SubjectPublicKeyInfo(
131-
algorithmIdentifier = X509AlgorithmIdentifier(algorithmOid, algorithmParameters),
132+
algorithmIdentifier = X509AlgorithmIdentifier(algorithmOid, algorithmParameters.singleOrNull()),
132133
subjectPublicKey = subjectPublicKey,
133134
)
134135

0 commit comments

Comments
 (0)