From a34db53e93943c54644738c24c86150b82c8d771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 30 Oct 2023 11:22:37 +0100 Subject: [PATCH] address PR #23 comments --- .../asitplus/crypto/datatypes/cose/CoseKey.kt | 281 +++++------------- .../crypto/datatypes/cose/CoseKeyParams.kt | 152 ++++++++++ .../asitplus/crypto/datatypes/JwsAlgorithm.kt | 10 +- 3 files changed, 238 insertions(+), 205 deletions(-) create mode 100644 datatypes-cose/src/commonMain/kotlin/at/asitplus/crypto/datatypes/cose/CoseKeyParams.kt diff --git a/datatypes-cose/src/commonMain/kotlin/at/asitplus/crypto/datatypes/cose/CoseKey.kt b/datatypes-cose/src/commonMain/kotlin/at/asitplus/crypto/datatypes/cose/CoseKey.kt index 8c9d53ff..0a6bf1d2 100644 --- a/datatypes-cose/src/commonMain/kotlin/at/asitplus/crypto/datatypes/cose/CoseKey.kt +++ b/datatypes-cose/src/commonMain/kotlin/at/asitplus/crypto/datatypes/cose/CoseKey.kt @@ -2,7 +2,6 @@ package at.asitplus.crypto.datatypes.cose import at.asitplus.crypto.datatypes.CryptoPublicKey import at.asitplus.crypto.datatypes.EcCurve -import at.asitplus.crypto.datatypes.asn1.decodeFromDer import at.asitplus.crypto.datatypes.asn1.encodeToByteArray import at.asitplus.crypto.datatypes.cose.io.cborSerializer import io.github.aakira.napier.Napier @@ -18,148 +17,9 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.decodeStructure -// Class needed to handle overlapping serial labels in COSE standard -sealed class CoseKeyParams() { - - abstract fun toCryptoPublicKey(): CryptoPublicKey? - - // Implements elliptic curve public key parameters in case of y being a Bytearray - data class EcYByteArrayParams( - val curve: CoseEllipticCurve? = null, - val x: ByteArray? = null, - val y: ByteArray? = null, - val d: ByteArray? = null - ) : CoseKeyParams() { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as EcYByteArrayParams - - if (curve != other.curve) return false - if (x != null) { - if (other.x == null) return false - if (!x.contentEquals(other.x)) return false - } else if (other.x != null) return false - if (y != null) { - if (other.y == null) return false - if (!y.contentEquals(other.y)) return false - } else if (other.y != null) return false - if (d != null) { - if (other.d == null) return false - if (!d.contentEquals(other.d)) return false - } else if (other.d != null) return false - - return true - } - - override fun hashCode(): Int { - var result = curve?.hashCode() ?: 0 - result = 31 * result + (x?.contentHashCode() ?: 0) - result = 31 * result + (y?.contentHashCode() ?: 0) - result = 31 * result + (d?.contentHashCode() ?: 0) - return result - } - - override fun toCryptoPublicKey(): CryptoPublicKey? { - return let { - CryptoPublicKey.Ec.fromCoordinates( - curve = curve?.toJwkCurve() ?: return null, - x = x ?: return null, - y = y ?: return null - ) - } - } - } - - // Implements elliptic curve public key parameters in case of y being a bool value - data class EcYBoolParams( - val curve: CoseEllipticCurve? = null, - val x: ByteArray? = null, - val y: Boolean? = null, - val d: ByteArray? = null - ) : CoseKeyParams() { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as EcYBoolParams - - if (curve != other.curve) return false - if (x != null) { - if (other.x == null) return false - if (!x.contentEquals(other.x)) return false - } else if (other.x != null) return false - if (y != other.y) return false - if (d != null) { - if (other.d == null) return false - if (!d.contentEquals(other.d)) return false - } else if (other.d != null) return false - - return true - } - - override fun hashCode(): Int { - var result = curve?.hashCode() ?: 0 - result = 31 * result + (x?.contentHashCode() ?: 0) - result = 31 * result + (y?.hashCode() ?: 0) - result = 31 * result + (d?.contentHashCode() ?: 0) - return result - } - - override fun toCryptoPublicKey(): CryptoPublicKey? = TODO() - -// TODO conversion to cryptoPublicKey (needs de-/compression of Y coordinate) - } - - // Implements RSA public key parameters - data class RsaParams( - val n: ByteArray? = null, - val e: ByteArray? = null, - val d: ByteArray? = null - ) : CoseKeyParams() { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as RsaParams - - if (n != null) { - if (other.n == null) return false - if (!n.contentEquals(other.n)) return false - } else if (other.n != null) return false - if (e != null) { - if (other.e == null) return false - if (!e.contentEquals(other.e)) return false - } else if (other.e != null) return false - if (d != null) { - if (other.d == null) return false - if (!d.contentEquals(other.d)) return false - } else if (other.d != null) return false - - return true - } - - override fun hashCode(): Int { - var result = n?.contentHashCode() ?: 0 - result = 31 * result + (e?.contentHashCode() ?: 0) - result = 31 * result + (d?.contentHashCode() ?: 0) - return result - } - - override fun toCryptoPublicKey(): CryptoPublicKey? { - return let { - CryptoPublicKey.Rsa( - n = n ?: return null, - e = e?.let { bytes -> Int.decodeFromDer(bytes) } ?: return null - ) - } - } - } -} - - +/** + * COSE public key as per [RFC 8152](https://www.rfc-editor.org/rfc/rfc8152.html#page-33). Since this is used as part of a COSE-specific DTO, every property is nullable + */ @OptIn(ExperimentalSerializationApi::class) @Serializable(with = CoseKeySerializer::class) data class CoseKey( @@ -214,55 +74,42 @@ data class CoseKey( return result } + /** + * @return a [CryptoPublicKey] equivalent if conversion is possibl (i.e. if all key params are set)
or `null` in case the required key params are not contained in this COSE key (i.e. if only a `kid` is used)) + */ fun toCryptoPublicKey() = keyParams.toCryptoPublicKey() fun serialize() = cborSerializer.encodeToByteArray(this) companion object { - fun deserialize(it: ByteArray) = kotlin.runCatching { - cborSerializer.decodeFromByteArray(it) + cborSerializer.decodeFromByteArray(it) }.getOrElse { Napier.w("deserialize failed", it) null } - fun fromAnsiX963Bytes(input: ByteArray, algorithm: CoseAlgorithm? = null): CoseKey? = - CryptoPublicKey.Ec.fromAnsiX963Bytes(input).toCoseKey(algorithm) - - fun fromCoordinates( - curve: CoseEllipticCurve, - x: ByteArray, - y: ByteArray, - algorithm: CoseAlgorithm? = null - ): CoseKey? = CryptoPublicKey.Ec.fromCoordinates(curve.toJwkCurve(), x, y).toCoseKey(algorithm) - - fun fromPKCS1encoded(input: ByteArray, algorithm: CoseAlgorithm? = null): CoseKey? = - CryptoPublicKey.Rsa.fromPKCS1encoded(input).toCoseKey(algorithm) - - @Deprecated("Use function [fromAnsiX963Bytes] above instead!") + @Deprecated("Use [CryptoPublicKey.fromAnsiX963Bytes] and [toCoseKey] instead!") fun fromAnsiX963Bytes(type: CoseKeyType, curve: CoseEllipticCurve, it: ByteArray) = if (type == CoseKeyType.EC2 && curve == CoseEllipticCurve.P256) { val pubKey = CryptoPublicKey.Ec.fromAnsiX963Bytes(it) pubKey.toCoseKey() } else null - @Deprecated("Use function [fromCoordinates] above instead") + @Deprecated("Use function [CryptoPublicKey.fromCoordinates] and [toCoseKey] above instead") fun fromCoordinates( type: CoseKeyType, curve: CoseEllipticCurve, x: ByteArray, y: ByteArray - ): CoseKey? { - return fromCoordinates(curve, x, y) - } + ): CoseKey? = CryptoPublicKey.Ec.fromCoordinates(curve.toJwkCurve(), x, y).toCoseKey() } } /** * Converts CryptoPublicKey into CoseKey - * If algorithm is not set then key can be used for any algorithm with same kty (RFC 8152), returns null for invalid kty/algorithm pairs + * If [algorithm] is not set then key can be used for any algorithm with same kty (RFC 8152), returns null for invalid kty/algorithm pairs */ fun CryptoPublicKey.toCoseKey(algorithm: CoseAlgorithm? = null): CoseKey? = when (this) { @@ -374,7 +221,7 @@ object CoseKeySerializer : KSerializer { }, when (val params = src.keyParams) { is CoseKeyParams.RsaParams -> params.d - is CoseKeyParams.EcYByteArrayParams -> params.d; else -> TODO() + is CoseKeyParams.EcYByteArrayParams -> params.d }, ) @@ -470,7 +317,7 @@ object CoseKeySerializer : KSerializer { get() = CoseKeySerialContainer.serializer().descriptor override fun deserialize(decoder: Decoder): CoseKey { - val labels = mapOf( + val labels = mapOf( "kty" to 1, "kid" to 2, "alg" to 3, @@ -494,50 +341,78 @@ object CoseKeySerializer : KSerializer { var d: ByteArray? = null decoder.decodeStructure(descriptor) { - val e=this while (true) { - val index= decodeElementIndex(descriptor) - if(index==-1) break - val label = descriptor.getElementAnnotations(index).filterIsInstance().first().label - if (label == labels["kty"]) type = - decodeSerializableElement(CoseKeyTypeSerializer.descriptor, index, CoseKeyTypeSerializer) - else if (label == labels["kid"]) keyId = - decodeNullableSerializableElement(ByteArraySerializer().descriptor, index, ByteArraySerializer()) - else if (label == labels["alg"]) alg = - decodeNullableSerializableElement(CoseAlgorithmSerializer.descriptor, index, CoseAlgorithmSerializer) - else if (label == labels["key_ops"]) keyOps = - decodeNullableSerializableElement( - ArraySerializer(CoseKeyOperationSerializer).descriptor, - index, - ArraySerializer(CoseKeyOperationSerializer) - ) - else if (label == labels["n/crv"]) { - when (type) { - CoseKeyType.EC2 -> { - val deser = CoseEllipticCurveSerializer - crv = decodeNullableSerializableElement(deser.descriptor, index, deser) - } - - CoseKeyType.RSA -> { - val deser = ByteArraySerializer() - n = decodeNullableSerializableElement(deser.descriptor, index, deser) + val index = decodeElementIndex(descriptor) + if (index == -1) break + val label = descriptor.getElementAnnotations(index).filterIsInstance().first().label + when (label) { + labels["kty"] -> type = + decodeSerializableElement(CoseKeyTypeSerializer.descriptor, index, CoseKeyTypeSerializer) + + labels["kid"] -> keyId = + decodeNullableSerializableElement( + ByteArraySerializer().descriptor, + index, + ByteArraySerializer() + ) + + labels["alg"] -> alg = + decodeNullableSerializableElement( + CoseAlgorithmSerializer.descriptor, + index, + CoseAlgorithmSerializer + ) + + labels["key_ops"] -> keyOps = + decodeNullableSerializableElement( + ArraySerializer(CoseKeyOperationSerializer).descriptor, + index, + ArraySerializer(CoseKeyOperationSerializer) + ) + + labels["n/crv"] -> { + when (type) { + CoseKeyType.EC2 -> { + val deser = CoseEllipticCurveSerializer + crv = decodeNullableSerializableElement(deser.descriptor, index, deser) + } + + CoseKeyType.RSA -> { + val deser = ByteArraySerializer() + n = decodeNullableSerializableElement(deser.descriptor, index, deser) + } + + CoseKeyType.SYMMETRIC -> TODO() } - CoseKeyType.SYMMETRIC -> TODO() - null -> TODO() } - } else if (label == labels["x/e"]) xOrE = - decodeNullableSerializableElement(ByteArraySerializer().descriptor, index, ByteArraySerializer()) - else if (label == labels["y"]) y = - decodeNullableSerializableElement(ByteArraySerializer().descriptor, index, ByteArraySerializer()) - else if (label == labels["d"]) d = - decodeNullableSerializableElement(ByteArraySerializer().descriptor, index, ByteArraySerializer()) - else { - break + labels["x/e"] -> xOrE = + decodeNullableSerializableElement( + ByteArraySerializer().descriptor, + index, + ByteArraySerializer() + ) + + labels["y"] -> y = + decodeNullableSerializableElement( + ByteArraySerializer().descriptor, + index, + ByteArraySerializer() + ) + + labels["d"] -> d = + decodeNullableSerializableElement( + ByteArraySerializer().descriptor, + index, + ByteArraySerializer() + ) + + else -> { + break + } } } - } return when (type) { CoseKeyType.EC2 -> { diff --git a/datatypes-cose/src/commonMain/kotlin/at/asitplus/crypto/datatypes/cose/CoseKeyParams.kt b/datatypes-cose/src/commonMain/kotlin/at/asitplus/crypto/datatypes/cose/CoseKeyParams.kt new file mode 100644 index 00000000..382c4d57 --- /dev/null +++ b/datatypes-cose/src/commonMain/kotlin/at/asitplus/crypto/datatypes/cose/CoseKeyParams.kt @@ -0,0 +1,152 @@ +package at.asitplus.crypto.datatypes.cose + +import at.asitplus.crypto.datatypes.CryptoPublicKey +import at.asitplus.crypto.datatypes.asn1.decodeFromDer + +/** + * Wrapper to handle parameters for different COSE public key types. + */ +sealed class CoseKeyParams() { + + abstract fun toCryptoPublicKey(): CryptoPublicKey? + + /** + * COSE EC public key parameters **without point compression**, i.e. the y coordinate being a ByteArray. + * Since this is used as part of a COSE-specific DTO, every property is nullable + */ + data class EcYByteArrayParams( + val curve: CoseEllipticCurve? = null, + val x: ByteArray? = null, + val y: ByteArray? = null, + val d: ByteArray? = null + ) : CoseKeyParams() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as EcYByteArrayParams + + if (curve != other.curve) return false + if (x != null) { + if (other.x == null) return false + if (!x.contentEquals(other.x)) return false + } else if (other.x != null) return false + if (y != null) { + if (other.y == null) return false + if (!y.contentEquals(other.y)) return false + } else if (other.y != null) return false + if (d != null) { + if (other.d == null) return false + if (!d.contentEquals(other.d)) return false + } else if (other.d != null) return false + + return true + } + + override fun hashCode(): Int { + var result = curve?.hashCode() ?: 0 + result = 31 * result + (x?.contentHashCode() ?: 0) + result = 31 * result + (y?.contentHashCode() ?: 0) + result = 31 * result + (d?.contentHashCode() ?: 0) + return result + } + + override fun toCryptoPublicKey(): CryptoPublicKey? { + return CryptoPublicKey.Ec.fromCoordinates( + curve = curve?.toJwkCurve() ?: return null, + x = x ?: return null, + y = y ?: return null + ) + } + } + + + /* + //TODO Implements elliptic curve public key parameters in case of y being a bool value + data class EcYBoolParams( + val curve: CoseEllipticCurve? = null, + val x: ByteArray? = null, + val y: Boolean? = null, + val d: ByteArray? = null + ) : CoseKeyParams() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as EcYBoolParams + + if (curve != other.curve) return false + if (x != null) { + if (other.x == null) return false + if (!x.contentEquals(other.x)) return false + } else if (other.x != null) return false + if (y != other.y) return false + if (d != null) { + if (other.d == null) return false + if (!d.contentEquals(other.d)) return false + } else if (other.d != null) return false + + return true + } + + override fun hashCode(): Int { + var result = curve?.hashCode() ?: 0 + result = 31 * result + (x?.contentHashCode() ?: 0) + result = 31 * result + (y?.hashCode() ?: 0) + result = 31 * result + (d?.contentHashCode() ?: 0) + return result + } + + override fun toCryptoPublicKey(): CryptoPublicKey? = TODO() + + // TODO conversion to cryptoPublicKey (needs de-/compression of Y coordinate) + */ + + /** + * COSE RSA public key params. Since this is used as part of a COSE-specific DTO, every property is nullable + */ + data class RsaParams( + val n: ByteArray? = null, + val e: ByteArray? = null, + val d: ByteArray? = null + ) : CoseKeyParams() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as RsaParams + + if (n != null) { + if (other.n == null) return false + if (!n.contentEquals(other.n)) return false + } else if (other.n != null) return false + if (e != null) { + if (other.e == null) return false + if (!e.contentEquals(other.e)) return false + } else if (other.e != null) return false + if (d != null) { + if (other.d == null) return false + if (!d.contentEquals(other.d)) return false + } else if (other.d != null) return false + + return true + } + + override fun hashCode(): Int { + var result = n?.contentHashCode() ?: 0 + result = 31 * result + (e?.contentHashCode() ?: 0) + result = 31 * result + (d?.contentHashCode() ?: 0) + return result + } + + override fun toCryptoPublicKey(): CryptoPublicKey? { + return let { + CryptoPublicKey.Rsa( + n = n ?: return null, + e = e?.let { bytes -> Int.decodeFromDer(bytes) } ?: return null + ) + } + } + } +} \ No newline at end of file diff --git a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/JwsAlgorithm.kt b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/JwsAlgorithm.kt index 189e07a7..33d4a734 100644 --- a/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/JwsAlgorithm.kt +++ b/datatypes/src/commonMain/kotlin/at/asitplus/crypto/datatypes/JwsAlgorithm.kt @@ -38,7 +38,13 @@ enum class JwsAlgorithm(val identifier: String, override val oid: ObjectIdentifi */ NON_JWS_SHA1_WITH_RSA("RS1", KnownOIDs.sha1WithRSAEncryption); - val signatureValueLength + /** + * For `ESXXX` and `HSXXX` this is the length (in bytes) of the signature value obtained when using a certain signature algorithm. + * + * `null` for RSA-based signatures with length depending on the key size (i.e. `PSXXX`, `RSXXX`, and [NON_JWS_SHA1_WITH_RSA]) + * + */ + val signatureValueLength: Int? get() = when (this) { ES256 -> 256 / 8 * 2 ES384 -> 384 / 8 * 2 @@ -46,7 +52,7 @@ enum class JwsAlgorithm(val identifier: String, override val oid: ObjectIdentifi HS256 -> 256 / 8 HS384 -> 384 / 8 HS512 -> 512 / 8 - else -> -1 // RSA signatures do not have a fixed size + else -> null } private fun encodePSSParams(bits: Int): Asn1Sequence {