Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

COSE Header Certificates #245

Merged
merged 3 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## 3.0

### 3.12.2 (Supreme 0.6.4)

* Change `CoseHeader.certificateChain` (CBOR element 33 `x5chain`) from a single byte array to a list of byte arrays, acc. to specification
* Remove `CoseHeader.coseKey`, which has been an unofficial addition from OID4VCI, but has been removed since

### 3.12.1 (Supreme 0.6.3)

* Add COSE object creation with detached payload, i.e. setting a `null` payload in `CoseSigned`, and clients are responsible to transport the payload separately
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@ import at.asitplus.catching
import at.asitplus.signum.indispensable.cosef.io.Base16Strict
import at.asitplus.signum.indispensable.cosef.io.coseCompliantSerializer
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.*
import kotlinx.serialization.cbor.ByteString
import kotlinx.serialization.cbor.CborLabel
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray

/**
* Protected header of a [CoseSigned].
*
* See [RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html).
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@Serializable(with = CoseHeaderSerializer::class)
data class CoseHeader(
/**
* This header parameter is used to indicate the algorithm used for the security processing. This header parameter
Expand Down Expand Up @@ -92,13 +88,6 @@ data class CoseHeader(
@ByteString
val partialIv: ByteArray? = null,

/**
* OID4VCI: COSE key material the new Credential shall be bound to.
*/
@SerialName("COSE_Key")
@ByteString
val coseKey: ByteArray? = null,

/**
* This header parameter contains an ordered array of X.509 certificates. The certificates are to be ordered
* starting with the certificate containing the end-entity key followed by the certificate that signed it, and so
Expand All @@ -114,8 +103,7 @@ data class CoseHeader(
@CborLabel(33)
@SerialName("x5chain")
@ByteString
// TODO Might also be an array, if there is a real chain, not only one cert
val certificateChain: ByteArray? = null,
val certificateChain: List<ByteArray>? = null,

/**
* https://www.rfc-editor.org/rfc/rfc9596
Expand All @@ -133,7 +121,6 @@ data class CoseHeader(
) {

fun serialize() = coseCompliantSerializer.encodeToByteArray(this)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
Expand All @@ -143,26 +130,15 @@ data class CoseHeader(
if (algorithm != other.algorithm) return false
if (criticalHeaders != other.criticalHeaders) return false
if (contentType != other.contentType) return false
if (kid != null) {
if (other.kid == null) return false
if (!kid.contentEquals(other.kid)) return false
} else if (other.kid != null) return false
if (iv != null) {
if (other.iv == null) return false
if (!iv.contentEquals(other.iv)) return false
} else if (other.iv != null) return false
if (partialIv != null) {
if (other.partialIv == null) return false
if (!partialIv.contentEquals(other.partialIv)) return false
} else if (other.partialIv != null) return false
if (coseKey != null) {
if (other.coseKey == null) return false
if (!coseKey.contentEquals(other.coseKey)) return false
} else if (other.coseKey != null) return false
if (!kid.contentEquals(other.kid)) return false
if (!iv.contentEquals(other.iv)) return false
if (!partialIv.contentEquals(other.partialIv)) return false
if (certificateChain != null) {
if (other.certificateChain == null) return false
if (!certificateChain.contentEquals(other.certificateChain)) return false
if (!certificateChain.all { t -> other.certificateChain.any { it.contentEquals(t) } }) return false
if (!other.certificateChain.all { o -> certificateChain.any { it.contentEquals(o) } }) return false
} else if (other.certificateChain != null) return false
if (type != other.type) return false

return true
}
Expand All @@ -174,23 +150,24 @@ data class CoseHeader(
result = 31 * result + (kid?.contentHashCode() ?: 0)
result = 31 * result + (iv?.contentHashCode() ?: 0)
result = 31 * result + (partialIv?.contentHashCode() ?: 0)
result = 31 * result + (coseKey?.contentHashCode() ?: 0)
result = 31 * result + (certificateChain?.contentHashCode() ?: 0)
result = 31 * result + (certificateChain?.hashCode() ?: 0)
result = 31 * result + (type?.hashCode() ?: 0)
return result
}

override fun toString(): String {
return "CoseHeader(algorithm=$algorithm," +
" criticalHeaders=$criticalHeaders," +
" contentType=$contentType," +
" kid=${kid?.encodeToString(Base16Strict)}," +
" iv=${iv?.encodeToString(Base16Strict)}," +
" partialIv=${partialIv?.encodeToString(Base16Strict)}," +
" coseKey=${coseKey?.encodeToString(Base16Strict)}," +
" certificateChain=${certificateChain?.encodeToString(Base16Strict)})"
return "CoseHeader(" +
"algorithm=$algorithm, " +
"criticalHeaders=$criticalHeaders, " +
"contentType=$contentType, " +
"kid=${kid?.encodeToString(Base16Strict)}, " +
"iv=${iv?.encodeToString(Base16Strict)}, " +
"partialIv=${partialIv?.encodeToString(Base16Strict)}, " +
"certificateChain=${certificateChain?.joinToString { it.encodeToString(Base16Strict) }}, " +
"type=$type" +
")"
}


companion object {
fun deserialize(it: ByteArray) = catching {
coseCompliantSerializer.decodeFromByteArray<CoseHeader>(it)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package at.asitplus.signum.indispensable.cosef

import at.asitplus.catching
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ByteArraySerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.cbor.ByteString
import kotlinx.serialization.cbor.CborLabel
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure

/**
* Handles serialization of [CoseHeader], accounting for [CoseHeader.certificateChain], which may be an array OR a
* byte string.
*/
@OptIn(ExperimentalSerializationApi::class)
object CoseHeaderSerializer : KSerializer<CoseHeader> {

override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CoseHeader") {
element("alg", CoseAlgorithm.serializer().descriptor, listOf(CborLabel(1)))
element("crit", String.serializer().descriptor, listOf(CborLabel(2)))
element("content type", String.serializer().descriptor, listOf(CborLabel(3)))
element("kid", ByteArraySerializer().descriptor, listOf(CborLabel(4), ByteString()))
element("IV", ByteArraySerializer().descriptor, listOf(CborLabel(5), ByteString()))
element("Partial IV", ByteArraySerializer().descriptor, listOf(CborLabel(6), ByteString()))
element("x5chain", ListSerializer(ByteArraySerializer()).descriptor, listOf(CborLabel(33), ByteString()))
element("typ", String.serializer().descriptor, listOf(CborLabel(16)))
}

override fun deserialize(decoder: Decoder): CoseHeader {
val labels = mapOf<String, Long>(
"alg" to 1,
"crit" to 2,
"content type" to 3,
"kid" to 4,
"IV" to 5,
"Partial IV" to 6,
"x5chain" to 33,
"typ" to 16
)

var alg: CoseAlgorithm? = null
var crit: String? = null
var contentType: String? = null
var kid: ByteArray? = null
var iv: ByteArray? = null
var partialIv: ByteArray? = null
var x5chain: List<ByteArray>? = null
var typ: String? = null

decoder.decodeStructure(descriptor) {
while (true) {
val index = decodeElementIndex(descriptor)
if (index == -1) break
val label = descriptor.getElementAnnotations(index)
.filterIsInstance<CborLabel>().first().label
when (label) {
labels["alg"] -> alg = decodeNullableSerializableElement(
CoseAlgorithmSerializer.descriptor,
index,
CoseAlgorithm.serializer()
)

labels["crit"] -> crit = decodeStringElement(String.serializer().descriptor, index)
labels["content type"] -> contentType = decodeStringElement(String.serializer().descriptor, index)
labels["kid"] -> kid = decodeNullableSerializableElement(
ByteArraySerializer().descriptor,
index,
ByteArraySerializer()
)

labels["IV"] -> iv = decodeNullableSerializableElement(
ByteArraySerializer().descriptor,
index,
ByteArraySerializer()
)

labels["Partial IV"] -> partialIv = decodeNullableSerializableElement(
ByteArraySerializer().descriptor,
index,
ByteArraySerializer()
)
// may be a list of byte array or a single byte array
labels["x5chain"] -> x5chain = catching {
decodeNullableSerializableElement(
ListSerializer(ByteArraySerializer()).descriptor,
index,
ListSerializer(ByteArraySerializer())
)
}.getOrElse {
listOf(
decodeSerializableElement(
ByteArraySerializer().descriptor,
index,
ByteArraySerializer()
)
)
}

labels["typ"] -> typ = decodeStringElement(String.serializer().descriptor, index)

else -> break
}
}
}
return CoseHeader(
algorithm = alg,
criticalHeaders = crit,
contentType = contentType,
kid = kid,
iv = iv,
partialIv = partialIv,
certificateChain = x5chain,
type = typ
)
}

override fun serialize(encoder: Encoder, value: CoseHeader) {
with(value) {
encoder.encodeStructure(descriptor) {
algorithm?.let {
encodeSerializableElement(
descriptor,
0,
CoseAlgorithmSerializer,
algorithm
)
}
criticalHeaders?.let {
encodeStringElement(
descriptor,
1,
criticalHeaders
)
}
contentType?.let {
encodeStringElement(
descriptor,
2,
contentType
)
}
kid?.let {
encodeSerializableElement(
descriptor,
3,
ByteArraySerializer(),
kid
)
}
iv?.let {
encodeSerializableElement(
descriptor,
4,
ByteArraySerializer(),
iv
)
}
partialIv?.let {
encodeSerializableElement(
descriptor,
5,
ByteArraySerializer(),
partialIv
)
}
certificateChain?.let {
if (it.size == 1) {
encodeSerializableElement(
descriptor,
6,
ByteArraySerializer(),
certificateChain.first()
)
} else {
encodeSerializableElement(
descriptor,
6,
ListSerializer(ByteArraySerializer()),
certificateChain
)
}
}
type?.let {
encodeStringElement(
descriptor,
7,
type
)
}
}
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,4 @@ class CoseSignedSerializer<P : Any?>(
}

private fun CoseHeader.usesEC(): Boolean? = algorithm?.algorithm?.let { it is SignatureAlgorithm.ECDSA }
?: certificateChain?.let { X509Certificate.decodeFromDerOrNull(it)?.publicKey is CryptoPublicKey.EC }
?: certificateChain?.firstOrNull()?.let { X509Certificate.decodeFromDerOrNull(it)?.publicKey is CryptoPublicKey.EC }
Loading