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 Signed API #194

Merged
merged 14 commits into from
Nov 25, 2024
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### 3.11.0 NEXT

* Add type parameter to `CoseSigned` for its payload
* Add companion method `CoseSigned.fromObject` to create a `CoseSigned` with a typed payload (outside of the usual `ByteArray`)

### 3.10.0 (Supreme 0.5.0) More ~~cowbell~~ targets!
A new artifact, minor breaking changes and a lot more targets ahead!

Expand Down
25 changes: 12 additions & 13 deletions docs/docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,29 +69,28 @@ val protectedHeader = CoseHeader(
val payload = byteArrayOf(0xC, 0xA, 0xF, 0xE)
```

Both of these are signature inputs, so we'll construct a `CoseSignatureInput` to sign.
Both of these are signature inputs, so we can construct the signature input:

```kotlin
val signatureInput = CoseSignatureInput(
contextString = "Signature1",
protectedHeader = ByteStringWrapper(protectedHeader),
externalAad = byteArrayOf(),
val signatureInput = CoseSigned.prepareCoseSignatureInput(
protectedHeader = protectedHeader,
payload = payload,
).serialize()
externalAad = byteArrayOf()
)
```


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
CoseSigned(
protectedHeader = ByteStringWrapper(protectedHeader),
unprotectedHeader = unprotectedHeader,
payload = payload,
signature = signature
)
// 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,83 +1,86 @@
package at.asitplus.signum.indispensable.cosef

import at.asitplus.KmmResult
import at.asitplus.catching
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.CryptoSignature
import at.asitplus.signum.indispensable.SignatureAlgorithm
import at.asitplus.signum.indispensable.*
import at.asitplus.signum.indispensable.cosef.io.Base16Strict
import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapper
import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapperSerializer
import at.asitplus.signum.indispensable.cosef.io.coseCompliantSerializer
import at.asitplus.signum.indispensable.pki.X509Certificate
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ByteArraySerializer
import kotlinx.serialization.*
import kotlinx.serialization.cbor.ByteString
import kotlinx.serialization.cbor.CborArray
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* Representation of a signed COSE_Sign1 object, i.e. consisting of protected header, unprotected header and payload.
*
* See [RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html).
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@Serializable(with = CoseSignedSerializer::class)
@CborArray
data class CoseSigned(
@Serializable(with = ByteStringWrapperCoseHeaderSerializer::class)
data class CoseSigned<P : Any?>(
@ByteString
val protectedHeader: ByteStringWrapper<CoseHeader>,
val unprotectedHeader: CoseHeader?,
@ByteString
val payload: ByteArray?,
@ByteString
@SerialName("signature")
private val rawSignature: ByteArray
val rawSignature: ByteArray,
) {

constructor(
protectedHeader: ByteStringWrapper<CoseHeader>,
protectedHeader: CoseHeader,
unprotectedHeader: CoseHeader?,
payload: ByteArray?,
signature: CryptoSignature.RawByteEncodable
) : this(protectedHeader, unprotectedHeader, payload, signature.rawByteArray)
) : this(
protectedHeader = ByteStringWrapper(value = protectedHeader),
unprotectedHeader = unprotectedHeader,
payload = payload,
rawSignature = signature.rawByteArray
)

val signature: CryptoSignature by lazy {
if (protectedHeader.value.usesEC() ?: unprotectedHeader?.usesEC() ?: (rawSignature.size < 2048))
CryptoSignature.EC.fromRawBytes(rawSignature)
else CryptoSignature.RSAorHMAC(rawSignature)
}

fun serialize() = coseCompliantSerializer.encodeToByteArray(this)
fun serialize(): ByteArray = coseCompliantSerializer.encodeToByteArray(CoseSignedSerializer(), this)

/**
* Decodes the payload of this object into a [ByteStringWrapper] containing an object of type [P].
*
* Note that this does not work if the payload is directly a [ByteArray].
*/
fun getTypedPayload(deserializer: KSerializer<P>): KmmResult<ByteStringWrapper<P>?> = catching {
payload?.let {
coseCompliantSerializer.decodeFromByteArray(ByteStringWrapperSerializer(deserializer), it)
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as CoseSigned
other as CoseSigned<*>

if (protectedHeader != other.protectedHeader) return false
if (unprotectedHeader != other.unprotectedHeader) return false
if (payload != null) {
if (other.payload == null) return false
if (!payload.contentEquals(other.payload)) return false
if (!payload.contentEqualsIfArray(other.payload)) return false
} else if (other.payload != null) return false
return rawSignature.contentEquals(other.rawSignature)
}

override fun hashCode(): Int {
var result = protectedHeader.hashCode()
result = 31 * result + (unprotectedHeader?.hashCode() ?: 0)
result = 31 * result + (payload?.contentHashCode() ?: 0)
result = 31 * result + (payload?.contentHashCodeIfArray() ?: 0)
result = 31 * result + rawSignature.contentHashCode()
return result
}
Expand All @@ -90,9 +93,53 @@ data class CoseSigned(
}

companion object {
fun deserialize(it: ByteArray) = catching {
coseCompliantSerializer.decodeFromByteArray<CoseSigned>(it)
fun deserialize(it: ByteArray): KmmResult<CoseSigned<ByteArray>> = catching {
coseCompliantSerializer.decodeFromByteArray<CoseSigned<ByteArray>>(it)
}

/**
* Creates a [CoseSigned] object from the given parameters,
* encapsulating the [payload] into a [ByteStringWrapper].
*
* This has to be an inline function with a reified type parameter,
* so it can't be a constructor (leads to a runtime error).
*/
inline fun <reified P : Any> fromObject(
protectedHeader: CoseHeader,
unprotectedHeader: CoseHeader?,
payload: P,
signature: CryptoSignature.RawByteEncodable
) = CoseSigned<P>(
protectedHeader = ByteStringWrapper(value = protectedHeader),
unprotectedHeader = unprotectedHeader,
payload = when (payload) {
is ByteArray -> payload
is ByteStringWrapper<*> -> coseCompliantSerializer.encodeToByteArray(payload)
else -> coseCompliantSerializer.encodeToByteArray(ByteStringWrapper(payload))
},
rawSignature = signature.rawByteArray
)

/**
* Called by COSE signing implementations to get the bytes that will be
* used as the input for signature calculation of a `COSE_Sign1` object
*/
inline fun <reified P : Any> prepareCoseSignatureInput(
protectedHeader: CoseHeader,
payload: P?,
externalAad: ByteArray = byteArrayOf(),
): ByteArray = CoseSignatureInput(
contextString = "Signature1",
protectedHeader = ByteStringWrapper(protectedHeader),
externalAad = externalAad,
payload = when (payload) {
is ByteArray -> payload
is ByteStringWrapper<*> -> coseCompliantSerializer.encodeToByteArray(payload)
else -> coseCompliantSerializer.encodeToByteArray(ByteStringWrapper(payload))
},
).serialize()


}
}

Expand All @@ -105,7 +152,6 @@ fun CoseHeader.usesEC(): Boolean? = algorithm?.algorithm?.let { it is SignatureA
@CborArray
data class CoseSignatureInput(
val contextString: String,
@Serializable(with = ByteStringWrapperCoseHeaderSerializer::class)
@ByteString
val protectedHeader: ByteStringWrapper<CoseHeader>,
@ByteString
Expand Down Expand Up @@ -155,19 +201,3 @@ data class CoseSignatureInput(
}
}

object ByteStringWrapperCoseHeaderSerializer : KSerializer<ByteStringWrapper<CoseHeader>> {

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("ByteStringWrapperCoseHeaderSerializer", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: ByteStringWrapper<CoseHeader>) {
val bytes = coseCompliantSerializer.encodeToByteArray(value.value)
encoder.encodeSerializableValue(ByteArraySerializer(), bytes)
}

override fun deserialize(decoder: Decoder): ByteStringWrapper<CoseHeader> {
val bytes = decoder.decodeSerializableValue(ByteArraySerializer())
return ByteStringWrapper(coseCompliantSerializer.decodeFromByteArray(bytes), bytes)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package at.asitplus.signum.indispensable.cosef

import at.asitplus.signum.indispensable.cosef.io.ByteStringWrapperSerializer
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ByteArraySerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure

class CoseSignedSerializer<P : Any?> : KSerializer<CoseSigned<P>> {

@OptIn(InternalSerializationApi::class)
override val descriptor: SerialDescriptor = buildSerialDescriptor("CoseSigned", StructureKind.LIST) {
element("protectedHeader", ByteStringWrapperSerializer(CoseHeader.serializer()).descriptor)
element("unprotectedHeader", CoseHeader.serializer().descriptor)
element("payload", ByteArraySerializer().descriptor)
element("signature", ByteArraySerializer().descriptor)
}

override fun deserialize(decoder: Decoder): CoseSigned<P> {
return decoder.decodeStructure(descriptor) {
val protectedHeader = decodeSerializableElement(descriptor, 0, ByteStringWrapperSerializer(CoseHeader.serializer()))
val unprotectedHeader = decodeNullableSerializableElement(descriptor, 1, CoseHeader.serializer())
val payload = decodeNullableSerializableElement(descriptor, 2, ByteArraySerializer())
val signature = decodeSerializableElement(descriptor, 3, ByteArraySerializer())
CoseSigned(protectedHeader, unprotectedHeader, payload, signature)
}
}

override fun serialize(encoder: Encoder, value: CoseSigned<P>) {
encoder.encodeStructure(descriptor) {
encodeSerializableElement(descriptor, 0, ByteStringWrapperSerializer(CoseHeader.serializer()), value.protectedHeader)
encodeNullableSerializableElement(descriptor, 1, CoseHeader.serializer(), value.unprotectedHeader)
encodeNullableSerializableElement(descriptor, 2, ByteArraySerializer(), value.payload)
encodeSerializableElement(descriptor, 3, ByteArraySerializer(), value.rawSignature)
}
}

}
Loading
Loading