Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: Copyright (c) A-SIT Plus GmbH
// SPDX-License-Identifier: Apache-2.0

package at.asitplus.awesn1.crypto

import at.asitplus.awesn1.Asn1Integer
import at.asitplus.awesn1.serialization.DER
import at.asitplus.awesn1.serialization.Der
import at.asitplus.awesn1.serialization.decodeFromDer
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToByteArray

@Serializable
data class EcdsaSigValue(val r: Asn1Integer, val s: Asn1Integer) {
Comment thread
JesusMcCloud marked this conversation as resolved.
fun toX509SignatureValue(der: Der = DER) = X509SignatureValue(der.encodeToByteArray(this))
companion object {
fun X509SignatureValue.toEcdsaSigValue(der: Der = DER) = der.decodeFromDer<EcdsaSigValue>(rawBytes)
}
}
Comment thread
JesusMcCloud marked this conversation as resolved.

Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ data class Pkcs8PrivateKeyInfo(
fun rsa(privateKey: Pkcs1RsaPrivateKeyInfo, attributes: Set<Asn1Element>? = null): Pkcs8PrivateKeyInfo =
Pkcs8PrivateKeyInfo(
version = Version.V1,
privateKeyAlgorithm = X509AlgorithmIdentifier(RSA_ENCRYPTION_OID, listOf(Asn1.Null())),
privateKeyAlgorithm = X509AlgorithmIdentifier(RSA_ENCRYPTION_OID, Asn1.Null()),
privateKey = Asn1.OctetStringEncapsulating { +DER.encodeToTlv(privateKey) },
attributes = attributes,
)
Expand All @@ -77,7 +77,7 @@ data class Pkcs8PrivateKeyInfo(
version = Version.V1,
privateKeyAlgorithm = X509AlgorithmIdentifier(
EC_PUBLIC_KEY_OID,
curveOid?.let { listOf(it.encodeToTlv()) }.orEmpty(),
curveOid?.encodeToTlv(),
),
privateKey = Asn1.OctetStringEncapsulating { +DER.encodeToTlv(sec1Key) },
attributes = attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@

package at.asitplus.awesn1.crypto

import at.asitplus.awesn1.Asn1Exception
import at.asitplus.awesn1.Asn1Integer
import at.asitplus.awesn1.ObjectIdentifier
import at.asitplus.awesn1.encoding.Asn1
import at.asitplus.awesn1.runRethrowing
import at.asitplus.awesn1.serialization.Asn1Tag
import at.asitplus.awesn1.serialization.DER
import at.asitplus.awesn1.serialization.Der
import at.asitplus.awesn1.serialization.ExplicitlyTagged
import at.asitplus.awesn1.serialization.decodeFromTlv
import at.asitplus.awesn1.serialization.encodeToTlv
import at.asitplus.awesn1.serialization.getValue
import at.asitplus.awesn1.toInt
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -115,8 +121,36 @@ data class RsaSsaPssParams internal constructor(
const val DEFAULT_SALT_LENGTH = 20
const val DEFAULT_TRAILER_FIELD = 1

val SHA1_IDENTIFIER = X509AlgorithmIdentifier(SHA1_OID, listOf(Asn1.Null()))
val MGF1_SHA1_IDENTIFIER = X509AlgorithmIdentifier(MGF1_OID, listOf(SHA1_IDENTIFIER.element))
val SHA1_IDENTIFIER = X509AlgorithmIdentifier(SHA1_OID, Asn1.Null())
val MGF1_SHA1_IDENTIFIER = X509AlgorithmIdentifier(MGF1_OID, SHA1_IDENTIFIER.element)

fun X509AlgorithmIdentifier.Companion.of(params: RsaSsaPssParams, der: Der = DER) = runRethrowing {
X509AlgorithmIdentifier(
RSA_SSA_PSS_OID,
der.encodeToTlv(params)
)
}

@Deprecated(level = DeprecationLevel.WARNING, message = "prefer of(), which can take a `Der` object",
replaceWith = ReplaceWith("RsaSsaPssParams.of(this)"))
val X509AlgorithmIdentifier.rsaSsaPssParams get() = RsaSsaPssParams.of(this)
/**
* Asserts that this identifier uses the `id-RSASSA-PSS` OID,
* then parses [parameters] as RSASSA-PSS parameters.
*
* This helper models [RFC 4055, section 3.1](https://www.rfc-editor.org/rfc/rfc4055.html#section-3.1).
*
* @throws Asn1Exception if this algorithm is RSA_SSA_PSS has no parameters, or the parameter element is
* not a valid `RSASSA-PSS-params` SEQUENCE.
*/
fun of(algorithmIdentifier: X509AlgorithmIdentifier, der: Der = DER): RsaSsaPssParams = runRethrowing {
require(algorithmIdentifier.oid == RSA_SSA_PSS_OID)
der.decodeFromTlv<RsaSsaPssParams>(
algorithmIdentifier.parameters?.asSequence() ?:
throw Asn1Exception("RSASSA-PSS AlgorithmIdentifier has no parameters")
)
}

}

override fun equals(other: Any?): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ data class SubjectPublicKeyInfo(
fun rsa(publicKey: Pkcs1RsaPublicKeyInfo): SubjectPublicKeyInfo = SubjectPublicKeyInfo(
algorithmIdentifier = X509AlgorithmIdentifier(
RSA_ENCRYPTION_OID,
listOf(Asn1.Null())
Asn1.Null()
),
subjectPublicKey = Asn1BitString(DER.encodeToTlv(publicKey).derEncoded)
)
Expand All @@ -71,7 +71,7 @@ data class SubjectPublicKeyInfo(
rsa(Pkcs1RsaPublicKeyInfo(modulus, exponent))

fun ec(curveOid: ObjectIdentifier, ansiX963Key: ByteArray): SubjectPublicKeyInfo = SubjectPublicKeyInfo(
algorithmIdentifier = X509AlgorithmIdentifier(EC_PUBLIC_KEY_OID, listOf(curveOid.encodeToTlv())),
algorithmIdentifier = X509AlgorithmIdentifier(EC_PUBLIC_KEY_OID, curveOid.encodeToTlv()),
subjectPublicKey = Asn1BitString(ansiX963Key)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ package at.asitplus.awesn1.crypto
import at.asitplus.awesn1.*
import at.asitplus.awesn1.encoding.Asn1
import at.asitplus.awesn1.encoding.WrappedElement
import at.asitplus.awesn1.serialization.DER
import at.asitplus.awesn1.serialization.decodeFromTlv
import kotlinx.serialization.Serializable
import kotlin.experimental.ExperimentalObjCRefinement
import kotlin.jvm.JvmInline
Expand All @@ -34,28 +32,27 @@ value class X509AlgorithmIdentifier(override val element: Asn1Sequence) : Identi
* Convenience constructor for creating an instance of `X509AlgorithmIdentifier`
* using an `ObjectIdentifier` and a list of `Asn1Element` parameters.
*
* The passed [parameters] are unrolled, making construction of the algorithm identifier object work as follows:
* ```
* Asn1.Sequence {
* +oid
* parameters.forEach { +it }
* }
* ```
* **Note that passing `null` as [parameters] is different from passing [Asn1Null] as [parameters].**
* Passing `null` omits the second member from the sequence entirely. (e.g., ECDSA)
* Passing [Asn1Null] encodes ASN.1 NULL as the second member of the sequence. (e.g., RSA/PKCS1)
*
* @param oid The object identifier representing the algorithm.
* @param parameters A list of ASN.1 elements representing the algorithm parameters.
* @param parameters The algorithm parameters element, if any.
*/
constructor(
oid: ObjectIdentifier,
parameters: List<Asn1Element>
parameters: Asn1Element?
) : this(Asn1.Sequence {
+oid
parameters.forEach { +it }
parameters?.let { +it }
})

@Deprecated(level = DeprecationLevel.WARNING, message = "parameters can only have 0 or 1 elements, use nullable ctor",
replaceWith = ReplaceWith("X509AlgorithmIdentifier(oid, parameters.singleOrNull())"))
constructor(oid: ObjectIdentifier, parameters: List<Asn1Element>) : this(oid, parameters.singleOrNull())

init {
require(element.children.isNotEmpty()) { "AlgorithmIdentifier must not be an empty SEQUENCE" }
oid //check that oid is present
val _ = oid //check that oid is present
}

//already throws during init, so no throws declaration here
Expand Down Expand Up @@ -84,36 +81,7 @@ value class X509AlgorithmIdentifier(override val element: Asn1Sequence) : Identi
get() = when (element.children.size) {
1 -> null
2 -> element.children[1]
else -> throw Asn1Exception("AlgorithmIdentifier has ${element.children.size} children")
}

/**
* Parses [parameters] as RSASSA-PSS parameters if this identifier uses the `id-RSASSA-PSS` OID.
*
* This helper models [RFC 4055, section 3.1](https://www.rfc-editor.org/rfc/rfc4055.html#section-3.1) without
* making [X509AlgorithmIdentifier] itself enforce algorithm-specific parameter schemas during generic DER parsing.
*
* @return `null` if this algorithm is nor RSA_SSA_PSS
*
* @throws Asn1Exception if this algorithm is RSA_SSA_PSS has no parameters, or the parameter element is
* not a valid `RSASSA-PSS-params` SEQUENCE.
*
* From Swift/Objective-C use the throwing `rsaSsaPssParams()` accessor (exported as a static
* `rsaSsaPssParams(_:)`, since value classes are not bridged as Objective-C types).
*/
@OptIn(ExperimentalObjCRefinement::class)
@Suppress("WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET")
@get:Throws(Asn1Exception::class)
@HiddenFromObjC
@get:HiddenFromObjC
val rsaSsaPssParams: RsaSsaPssParams?
get() = runWrappingAs(a = ::Asn1Exception) {
if (oid != RsaSsaPssParams.RSA_SSA_PSS_OID) {
return null
}
DER.decodeFromTlv<RsaSsaPssParams>(
parameters?.asSequence() ?: throw Asn1Exception("RSASSA-PSS AlgorithmIdentifier has no parameters")
)
else -> throw Asn1Exception("AlgorithmIdentifier has ${element.children.size} (> 2) children")
}

override fun toString(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,4 @@ value class X509SignatureValue(val rawBitString: Asn1BitString): WrappedEncodabl
constructor(rawBytes: ByteArray) : this(Asn1BitString(rawBytes))

val rawBytes: ByteArray get() = rawBitString.bitCarryingBytes

// runRethrowing: a malformed ECDSA-Sig-Value (fewer than two children, or non-positive integers) would
// otherwise leak NoSuchElementException/ClassCastException instead of a catchable Asn1Exception.
@Throws(Asn1Exception::class)
fun decodeRS(): Pair<Asn1Integer.Positive, Asn1Integer.Positive> = runRethrowing {
Asn1Element.parse(rawBytes).asSequence().decodeAs {
next().asPrimitive().decodeToAsn1Integer() as Asn1Integer.Positive to
next().asPrimitive().decodeToAsn1Integer() as Asn1Integer.Positive
}
}

companion object {
fun fromRS(r: Asn1Integer.Positive, s: Asn1Integer.Positive) =
X509SignatureValue(Asn1.Sequence { +r; +s }.derEncoded)
}
}

fun X509SignatureValue.decodeRsOrNull() = catchingUnwrapped { decodeRS() }.getOrNull()
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private fun randomBitStringSignatureValue(random: Random) =
X509SignatureValue(randomBytes(random))

private fun randomEcdsaSignatureValue(random: Random) =
X509SignatureValue.fromRS(positiveAsn1Integer(random), positiveAsn1Integer(random))
EcdsaSigValue(positiveAsn1Integer(random), positiveAsn1Integer(random)).toX509SignatureValue()

private fun randomEcPrivateKey(random: Random) = Sec1EcPrivateKeyInfo(
privateKey = randomBytes(random, 32),
Expand All @@ -95,7 +95,7 @@ private fun randomEcPrivateKey(random: Random) = Sec1EcPrivateKeyInfo(
private fun randomEncryptedPrivateKeyInfo(random: Random) = EncryptedPrivateKeyInfo(
encryptionAlgorithm = X509AlgorithmIdentifier(
oid = randomOid(random),
parameters = randomRawElement(random).takeIf { random.nextBoolean() }?.let { listOf(it) }?:listOf(),
parameters = randomRawElement(random).takeIf { random.nextBoolean() },
),
encryptedData = if (random.nextBoolean()) Asn1EncapsulatingOctetString(
listOf(Asn1OctetString(randomBytes(random, 32)))
Expand Down Expand Up @@ -128,7 +128,7 @@ private fun randomRsaPublicKey(random: Random) = Pkcs1RsaPublicKeyInfo(

private fun randomSignatureAlgorithmIdentifier(random: Random) = X509AlgorithmIdentifier(
oid = randomOid(random),
parameters = List(random.nextInt(0, 3)) { randomRawElement(random) },
parameters = randomRawElement(random),
)

private fun randomSubjectPublicKeyInfo(random: Random): SubjectPublicKeyInfo =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package at.asitplus.awesn1.crypto

import at.asitplus.awesn1.Asn1Integer
import at.asitplus.awesn1.crypto.EcdsaSigValue.Companion.toEcdsaSigValue
import at.asitplus.awesn1.serialization.DER
import at.asitplus.awesn1.serialization.decodeFromDer
import at.asitplus.testballoon.matrix.matrixSuite
import io.kotest.matchers.shouldBe
import kotlin.io.encoding.Base64

val EcdsaSigValueTest by matrixSuite {
"Test" {
val sigValue = Base64.UrlSafe.decode("A0kAMEYCIQCnXiAKLwJP0uXBKtTmJccBu" +
"yddhFFVTz-J0DNHBi21lgIhAI1SUoIYXXqdZMrKox4_HBTEmuxvG9sloAoDH5rfsyd4")
.let { DER.decodeFromDer<X509SignatureValue>(it) }
sigValue.toEcdsaSigValue().let {
it.r.toString() shouldBe "75702550467927847687504835259358957068594330833158195591194084637648375100822"
it.s.toString() shouldBe "63921562560111841846899158949481573510824775155107323853077162713843342976888"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import at.asitplus.awesn1.Asn1Time
import at.asitplus.awesn1.Asn1Element
import at.asitplus.awesn1.Asn1Integer
import at.asitplus.awesn1.Asn1Integer.Sign
import at.asitplus.awesn1.crypto.X509AlgorithmIdentifier
import at.asitplus.awesn1.encoding.parse
import at.asitplus.awesn1.crypto.legacy.EcPrivateKeyInfo as LegacyEcPrivateKeyInfo
import at.asitplus.awesn1.crypto.legacy.EncryptedPrivateKeyInfo as LegacyEncryptedPrivateKeyInfo
Expand Down Expand Up @@ -86,7 +87,7 @@ private fun LegacyEncryptedPrivateKeyInfo.toCurrent() =
private fun LegacyPkcs8PrivateKeyInfo.toCurrent() =
Pkcs8PrivateKeyInfo(
Pkcs8PrivateKeyInfo.Version.V1,
privateKeyAlgorithm = X509AlgorithmIdentifier(algorithmOid, algorithmParameters),
privateKeyAlgorithm = X509AlgorithmIdentifier(algorithmOid, algorithmParameters.singleOrNull()),
privateKey = privateKey,
attributes = attributes?.toSet(),
)
Expand Down Expand Up @@ -121,14 +122,14 @@ private fun LegacyRsaPublicKeyInfo.toCurrent() =
private fun LegacySignatureAlgorithmIdentifier.toCurrent() =
X509AlgorithmIdentifier(
oid = oid,
parameters = parameters,
parameters = parameters.singleOrNull(),
)

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

private fun LegacySubjectPublicKeyInfo.toCurrent() =
SubjectPublicKeyInfo(
algorithmIdentifier = X509AlgorithmIdentifier(algorithmOid, algorithmParameters),
algorithmIdentifier = X509AlgorithmIdentifier(algorithmOid, algorithmParameters.singleOrNull()),
subjectPublicKey = subjectPublicKey,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private fun minimalTbsCertificate(
subjectUniqueID: Asn1BitString? = null,
) = X509TbsCertificate(
serialNumber = Asn1Integer(1u),
signatureAlgorithm = X509AlgorithmIdentifier(ObjectIdentifier("1.2.840.113549.1.1.11"), emptyList()),
signatureAlgorithm = X509AlgorithmIdentifier(ObjectIdentifier("1.2.840.113549.1.1.11"), null),
issuerName = X500Name(X500RelativeDistinguishedName(setOf(X500AttributeTypeAndValue.CommonName("issuer")))),
validFrom = Asn1Time.SecondsCapped(Instant.fromEpochSeconds(1_700_000_000L)),
validUntil = Asn1Time.SecondsCapped(Instant.fromEpochSeconds(1_700_086_400L)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ val SerializationTreeBuilderTest by matrixSuite {
"X509AlgorithmIdentifier unary plus works with and without DER context" {
val algorithm = X509AlgorithmIdentifier(
ObjectIdentifier("1.2.840.113549.1.1.11"),
listOf(Asn1.Null()),
Asn1.Null(),
)
val expected = listOf(DER.encodeToTlv(algorithm))

Expand All @@ -44,7 +44,7 @@ val SerializationTreeBuilderTest by matrixSuite {
val publicKey = Pkcs1RsaPublicKeyInfo(Asn1Integer(3233), Asn1Integer(17))
val algorithm = X509AlgorithmIdentifier(
ObjectIdentifier("1.2.840.113549.1.1.11"),
listOf(Asn1.Null()),
Asn1.Null(),
)
val name = X500Name(X500RelativeDistinguishedName(X500AttributeTypeAndValue.CommonName("example")))
val certificate = X509Certificate(
Expand Down
Loading