Skip to content

Commit

Permalink
COSE: Implement detached payload (i.e. transported dseparately)
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Jan 10, 2025
1 parent 86833d8 commit 2ba9ba9
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### NEXT

* Add COSE object creation with detached payload, i.e. setting a `null` payload in `CoseSigned`, and clients are responsible to transport the payload separately

### 3.12.0 (Supreme 0.6.2)
* Fix COSE signature verification (this is breaking change in `indispensable-cosef`):
* Introduce class `CoseSignedBytes` which holds the bytes as transmitted on the wire
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ data class CoseSigned<P : Any?> internal constructor(
val wireFormat: CoseSignedBytes,
) {

fun prepareCoseSignatureInput(externalAad: ByteArray = byteArrayOf()): ByteArray =
wireFormat.toCoseSignatureInput(externalAad)
fun prepareCoseSignatureInput(
externalAad: ByteArray = byteArrayOf(),
detachedPayload: ByteArray? = null,
): ByteArray = wireFormat.toCoseSignatureInput(externalAad, detachedPayload)

fun serialize(parameterSerializer: KSerializer<P>): ByteArray = coseCompliantSerializer
.encodeToByteArray(CoseSignedSerializer(parameterSerializer), this)
Expand Down Expand Up @@ -129,16 +131,15 @@ data class CoseSigned<P : Any?> internal constructor(
/**
* If [this] is a [ByteArray], use it as is, otherwise encode it as a [ByteStringWrapper], with CBOR tag 24
*/
private fun <P : Any> P?.toRawPayload(payloadSerializer: KSerializer<P>): ByteArray = when (this) {
private fun <P : Any> P?.toRawPayload(payloadSerializer: KSerializer<P>): ByteArray? = when (this) {
is ByteArray -> this
is Nothing -> byteArrayOf()
is Nothing -> null
is ByteStringWrapper<*> -> throw IllegalArgumentException("Payload must not be a ByteStringWrapper")
is P -> coseCompliantSerializer.encodeToByteArray<ByteStringWrapper<P>>(
ByteStringWrapperSerializer(payloadSerializer),
ByteStringWrapper(this)
).wrapInCborTag(24)

else -> byteArrayOf()
else -> null
}

private fun ByteArray.wrapInCborTag(tag: Byte) = byteArrayOf(0xd8.toByte()) + byteArrayOf(tag) + this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ data class CoseSignedBytes(
) {
fun toCoseSignatureInput(
externalAad: ByteArray = byteArrayOf(),
detachedPayload: ByteArray? = null,
): ByteArray = CoseSignatureInput(
contextString = "Signature1",
protectedHeader = protectedHeader.toZeroLengthByteString(),
externalAad = externalAad,
payload = payload,
payload = payload ?: detachedPayload,
).serialize()

fun serialize(): ByteArray = coseCompliantSerializer.encodeToByteArray(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class CoseSignedSerializer<P : Any?>(
} else {
runCatching { fromBytes() }
.getOrElse { fromByteStringWrapper() }
// if it still fails, the input not valid
// if it still fails, the input is not valid
}

private fun ByteArray.fromBytes(): P =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ class CoseSerializationTest : FreeSpec({
CoseSigned.deserialize(ByteArraySerializer(), serialized).getOrThrow() shouldBe cose
}

"Serialization is correct for null" {
val cose = CoseSigned.create(
protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256),
unprotectedHeader = null,
payload = null,
signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), //RSAorHMAC because EC expects tuple
payloadSerializer = ByteArraySerializer(),
)
val serialized = cose.serialize(ByteArraySerializer())

val serializedString = serialized.encodeToString(Base16Strict).uppercase()
serializedString shouldContain "A0F643" // Empty unprotected header; null; begin of signature
serializedString shouldContain "8445A101390100" // array of 5 bytes that is a map with -257 (the header for RS256)
cose.payload shouldBe null

CoseSigned.deserialize(ByteArraySerializer(), serialized).getOrThrow() shouldBe cose
}

"Deserialization is correct for byte array" {
val input = "8445A101390100A054546869732069732074686520636F6E74656E742E43626172"

Expand All @@ -89,6 +107,14 @@ class CoseSerializationTest : FreeSpec({
cose.wireFormat.payload shouldBe "546869732069732074686520636F6E74656E742E".decodeToByteArray(Base16())
}

"Deserialization is correct for null payload" {
val input = "8445A101390100A0F643626172"

val cose = CoseSigned.deserialize(ByteArraySerializer(), input.decodeToByteArray(Base16())).getOrThrow()
cose.payload shouldBe null
cose.wireFormat.payload shouldBe null
}

"Deserialization fails when trying to parse byte array as data class" {
val input = "8445A101390100A054546869732069732074686520636F6E74656E742E43626172"

Expand Down

0 comments on commit 2ba9ba9

Please sign in to comment.