From 87655e7087de8aa8f950be6f03fe1aaa83ed0c91 Mon Sep 17 00:00:00 2001 From: Mattias de Zalenski Date: Sat, 22 Mar 2025 11:57:40 +0100 Subject: [PATCH 1/6] Add missing ending parenthesis in some toString --- koap-core/src/commonMain/kotlin/Message.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/koap-core/src/commonMain/kotlin/Message.kt b/koap-core/src/commonMain/kotlin/Message.kt index 503c0a6d..c0100048 100644 --- a/koap-core/src/commonMain/kotlin/Message.kt +++ b/koap-core/src/commonMain/kotlin/Message.kt @@ -296,7 +296,7 @@ sealed class Message { override fun hashCode(): Int = etag.contentHashCode() - override fun toString(): String = "ETag(etag=${etag.toHexString()}" + override fun toString(): String = "ETag(etag=${etag.toHexString()})" } /** RFC 7252 5.10.7. Location-Path and Location-Query */ @@ -336,7 +336,7 @@ sealed class Message { override fun hashCode(): Int = etag.contentHashCode() - override fun toString(): String = "IfMatch(etag=${etag.toHexString()}" + override fun toString(): String = "IfMatch(etag=${etag.toHexString()})" } /** RFC 7252 5.10.8.2. If-None-Match */ From 2d23882193dfce19ab55730640be02d125d64750 Mon Sep 17 00:00:00 2001 From: Mattias de Zalenski Date: Sat, 22 Mar 2025 03:34:36 +0100 Subject: [PATCH 2/6] Add Oscore and Edhoc options --- koap-core/api/koap-core.api | 16 +++++++++ koap-core/api/koap-core.klib.api | 17 ++++++++++ koap-core/src/commonMain/kotlin/Decoder.kt | 4 +++ koap-core/src/commonMain/kotlin/Message.kt | 28 +++++++++++++-- koap-core/src/commonMain/kotlin/ToFormat.kt | 4 +++ .../src/commonTest/kotlin/DecoderTest.kt | 34 +++++++++++++++++++ .../src/commonTest/kotlin/EncoderTest.kt | 34 +++++++++++++++++++ 7 files changed, 135 insertions(+), 2 deletions(-) diff --git a/koap-core/api/koap-core.api b/koap-core/api/koap-core.api index 5e4d5238..19a8bd5f 100644 --- a/koap-core/api/koap-core.api +++ b/koap-core/api/koap-core.api @@ -309,6 +309,11 @@ public final class com/juul/koap/Message$Option$Echo : com/juul/koap/Message$Opt public fun toString ()Ljava/lang/String; } +public final class com/juul/koap/Message$Option$Edhoc : com/juul/koap/Message$Option { + public static final field INSTANCE Lcom/juul/koap/Message$Option$Edhoc; + public fun toString ()Ljava/lang/String; +} + public abstract class com/juul/koap/Message$Option$Format : com/juul/koap/Message$Option { public abstract fun getNumber ()I } @@ -475,6 +480,17 @@ public final class com/juul/koap/Message$Option$Observe$Registration$Register : public fun toString ()Ljava/lang/String; } +public final class com/juul/koap/Message$Option$Oscore : com/juul/koap/Message$Option { + public fun ([B)V + public final fun component1 ()[B + public final fun copy ([B)Lcom/juul/koap/Message$Option$Oscore; + public static synthetic fun copy$default (Lcom/juul/koap/Message$Option$Oscore;[BILjava/lang/Object;)Lcom/juul/koap/Message$Option$Oscore; + public fun equals (Ljava/lang/Object;)Z + public final fun getCoseObject ()[B + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/juul/koap/Message$Option$ProxyScheme : com/juul/koap/Message$Option { public fun (Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; diff --git a/koap-core/api/koap-core.klib.api b/koap-core/api/koap-core.klib.api index 617ba6fe..8e846750 100644 --- a/koap-core/api/koap-core.klib.api +++ b/koap-core/api/koap-core.klib.api @@ -483,6 +483,19 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0] } } + final class Oscore : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.Oscore|null[0] + constructor (kotlin/ByteArray) // com.juul.koap/Message.Option.Oscore.|(kotlin.ByteArray){}[0] + + final val coseObject // com.juul.koap/Message.Option.Oscore.coseObject|{}coseObject[0] + final fun (): kotlin/ByteArray // com.juul.koap/Message.Option.Oscore.coseObject.|(){}[0] + + final fun component1(): kotlin/ByteArray // com.juul.koap/Message.Option.Oscore.component1|component1(){}[0] + final fun copy(kotlin/ByteArray = ...): com.juul.koap/Message.Option.Oscore // com.juul.koap/Message.Option.Oscore.copy|copy(kotlin.ByteArray){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.juul.koap/Message.Option.Oscore.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.juul.koap/Message.Option.Oscore.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.juul.koap/Message.Option.Oscore.toString|toString(){}[0] + } + final class ProxyScheme : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.ProxyScheme|null[0] constructor (kotlin/String) // com.juul.koap/Message.Option.ProxyScheme.|(kotlin.String){}[0] @@ -704,6 +717,10 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0] } } + final object Edhoc : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.Edhoc|null[0] + final fun toString(): kotlin/String // com.juul.koap/Message.Option.Edhoc.toString|toString(){}[0] + } + final object IfNoneMatch : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.IfNoneMatch|null[0] final fun toString(): kotlin/String // com.juul.koap/Message.Option.IfNoneMatch.toString|toString(){}[0] } diff --git a/koap-core/src/commonMain/kotlin/Decoder.kt b/koap-core/src/commonMain/kotlin/Decoder.kt index 28d84c86..8aa21f10 100644 --- a/koap-core/src/commonMain/kotlin/Decoder.kt +++ b/koap-core/src/commonMain/kotlin/Decoder.kt @@ -32,6 +32,7 @@ import com.juul.koap.Message.Option.Block2 import com.juul.koap.Message.Option.ContentFormat import com.juul.koap.Message.Option.ETag import com.juul.koap.Message.Option.Echo +import com.juul.koap.Message.Option.Edhoc import com.juul.koap.Message.Option.Format import com.juul.koap.Message.Option.HopLimit import com.juul.koap.Message.Option.IfMatch @@ -41,6 +42,7 @@ import com.juul.koap.Message.Option.LocationQuery import com.juul.koap.Message.Option.MaxAge import com.juul.koap.Message.Option.NoResponse import com.juul.koap.Message.Option.Observe +import com.juul.koap.Message.Option.Oscore import com.juul.koap.Message.Option.ProxyScheme import com.juul.koap.Message.Option.ProxyUri import com.juul.koap.Message.Option.QBlock1 @@ -406,6 +408,7 @@ internal fun ByteArrayReader.readOption(preceding: Format?): Option? { 6 -> Observe(readNumberOfLength(length)) 7 -> UriPort(readNumberOfLength(length)) 8 -> LocationPath(readUtf8(length)) + 9 -> Oscore(readByteArray(length)) 11 -> UriPath(readUtf8(length)) 12 -> ContentFormat(readNumberOfLength(length)) 14 -> MaxAge(readNumberOfLength(length)) @@ -414,6 +417,7 @@ internal fun ByteArrayReader.readOption(preceding: Format?): Option? { 17 -> Accept(readNumberOfLength(length)) 19 -> blockOf(readNumberOfLength(length)) 20 -> LocationQuery(readUtf8(length)) + 21 -> Edhoc 23 -> blockOf(readNumberOfLength(length)) 27 -> blockOf(readNumberOfLength(length)) 28 -> Size2(readNumberOfLength(length)) diff --git a/koap-core/src/commonMain/kotlin/Message.kt b/koap-core/src/commonMain/kotlin/Message.kt index c0100048..d9c56c51 100644 --- a/koap-core/src/commonMain/kotlin/Message.kt +++ b/koap-core/src/commonMain/kotlin/Message.kt @@ -44,6 +44,7 @@ private val PROXY_SCHEME_LENGTH_RANGE = 1..255 private val SIZE_RANGE = UINT_RANGE // Size1, Size2 private val OBSERVE_RANGE = 0..16_777_215 // 3-byte unsigned int internal val BLOCK_NUMBER_RANGE = 0..0xF_FF_FF +private val OSCORE_LENGTH_RANGE = 0..255 private val HOP_LIMIT_RANGE = 1..255 private val ECHO_SIZE_RANGE = 1..40 private val REQUEST_TAG_SIZE_RANGE = 0..8 @@ -88,7 +89,7 @@ sealed class Message { * | 6 | [Observe][Observe] | uint | 0-3 | [RFC 7641](https://tools.ietf.org/html/rfc7641#section-2) Observing Resources 2 | * | 7 | [Uri-Port][UriPort] | uint | 0-2 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.1) CoAP 5.10.1 | * | 8 | [Location-Path][LocationPath] | string | 0-255 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.7) CoAP 5.10.7 | - * | 9 | OSCORE | opaque | 0-255 | [RFC 8613](https://tools.ietf.org/html/rfc8613#section-2) OSCORE 2 | + * | 9 | [OSCORE][Oscore] | opaque | 0-255 | [RFC 8613](https://tools.ietf.org/html/rfc8613#section-2) OSCORE 2 | * | 11 | [Uri-Path][UriPath] | string | 0-255 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.1) CoAP 5.10.1 | * | 12 | [Content-Format][ContentFormat] | uint | 0-2 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.3) CoAP 5.10.3 | * | 14 | [Max-Age][MaxAge] | uint | 0-4 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.5) CoAP 5.10.5 | @@ -97,7 +98,7 @@ sealed class Message { * | 17 | [Accept][Accept] | uint | 0-2 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.4) CoAP 5.10.4 | * | 19 | [Q-Block1][QBlock1] | uint | 0-3 | [RFC 9177](https://tools.ietf.org/html/rfc9177#section-4) Block-Wise Robust 4 | * | 20 | [Location-Query][LocationQuery] | string | 0-255 | [RFC 7252](https://tools.ietf.org/html/rfc7252#section-5.10.7) CoAP 5.10.7 | - * | 21 | EDHOC | empty | 0 | [RFC 9668](https://tools.ietf.org/html/rfc9668#section-3.1) EDHOC 3.1 | + * | 21 | [EDHOC][Edhoc] | empty | 0 | [RFC 9668](https://tools.ietf.org/html/rfc9668#section-3.1) EDHOC 3.1 | * | 23 | [Block2] | uint | 0-3 | [RFC 7959](https://tools.ietf.org/html/rfc7959#section-2.1) Block-Wise 2.1 | * | 27 | [Block1] | uint | 0-3 | [RFC 7959](https://tools.ietf.org/html/rfc7959#section-2.1) Block-Wise 2.1 | * | 28 | [Size2] | uint | 0-4 | [RFC 7959](https://tools.ietf.org/html/rfc7959#section-4) Block-Wise 4 | @@ -514,6 +515,29 @@ sealed class Message { } } + /** RFC 8613 2. OSCORE */ + data class Oscore( + val coseObject: ByteArray, + ) : Option() { + init { + require(coseObject.size in OSCORE_LENGTH_RANGE) { + "Oscore length of ${coseObject.size} is outside allowable range of $OSCORE_LENGTH_RANGE" + } + } + + override fun equals(other: Any?): Boolean = + this === other || (other is Oscore && coseObject.contentEquals(other.coseObject)) + + override fun hashCode(): Int = coseObject.contentHashCode() + + override fun toString(): String = "Oscore(coseObject=${coseObject.toHexString()})" + } + + /** RFC 9668 3.1. EDHOC */ + object Edhoc : Option() { + override fun toString(): String = "Edhoc" + } + /** RFC 8768 3. Hop-Limit */ data class HopLimit( val hops: Long, diff --git a/koap-core/src/commonMain/kotlin/ToFormat.kt b/koap-core/src/commonMain/kotlin/ToFormat.kt index a2a59166..6d6b39de 100644 --- a/koap-core/src/commonMain/kotlin/ToFormat.kt +++ b/koap-core/src/commonMain/kotlin/ToFormat.kt @@ -7,6 +7,7 @@ import com.juul.koap.Message.Option.Block2 import com.juul.koap.Message.Option.ContentFormat import com.juul.koap.Message.Option.ETag import com.juul.koap.Message.Option.Echo +import com.juul.koap.Message.Option.Edhoc import com.juul.koap.Message.Option.Format import com.juul.koap.Message.Option.Format.empty import com.juul.koap.Message.Option.Format.opaque @@ -20,6 +21,7 @@ import com.juul.koap.Message.Option.LocationQuery import com.juul.koap.Message.Option.MaxAge import com.juul.koap.Message.Option.NoResponse import com.juul.koap.Message.Option.Observe +import com.juul.koap.Message.Option.Oscore import com.juul.koap.Message.Option.ProxyScheme import com.juul.koap.Message.Option.ProxyUri import com.juul.koap.Message.Option.QBlock1 @@ -43,6 +45,7 @@ internal fun Option.toFormat(): Format = is Observe -> uint(6, option.value) is UriPort -> uint(7, option.port) is LocationPath -> string(8, option.uri) + is Oscore -> opaque(9, option.coseObject) is UriPath -> string(11, option.uri) is ContentFormat -> uint(12, option.format) is MaxAge -> uint(14, option.seconds) @@ -51,6 +54,7 @@ internal fun Option.toFormat(): Format = is Accept -> uint(17, option.format) is QBlock1 -> uint(19, option.intValue.toLong()) is LocationQuery -> string(20, option.uri) + is Edhoc -> empty(21) is Block2 -> uint(23, option.intValue.toLong()) is Block1 -> uint(27, option.intValue.toLong()) is Size2 -> uint(28, option.bytes) diff --git a/koap-core/src/commonTest/kotlin/DecoderTest.kt b/koap-core/src/commonTest/kotlin/DecoderTest.kt index 02e976cc..54c77e80 100644 --- a/koap-core/src/commonTest/kotlin/DecoderTest.kt +++ b/koap-core/src/commonTest/kotlin/DecoderTest.kt @@ -6,6 +6,7 @@ import com.juul.koap.Message.Option.Block.Size.Bert import com.juul.koap.Message.Option.Block1 import com.juul.koap.Message.Option.Block2 import com.juul.koap.Message.Option.Echo +import com.juul.koap.Message.Option.Edhoc import com.juul.koap.Message.Option.HopLimit import com.juul.koap.Message.Option.NoResponse import com.juul.koap.Message.Option.NoResponse.NotInterestedIn.Response4xx @@ -13,6 +14,7 @@ import com.juul.koap.Message.Option.NoResponse.NotInterestedIn.Response5xx import com.juul.koap.Message.Option.Observe import com.juul.koap.Message.Option.Observe.Registration.Deregister import com.juul.koap.Message.Option.Observe.Registration.Register +import com.juul.koap.Message.Option.Oscore import com.juul.koap.Message.Option.QBlock1 import com.juul.koap.Message.Option.QBlock2 import com.juul.koap.Message.Option.RequestTag @@ -253,6 +255,38 @@ class DecoderTest { ) } + @Test + fun decodeOscoreOptionWithEmptyCoseObject() { + testReadOption( + encoded = """ + 90 # Option Delta: 9, Option Length: 0 + """, + expected = Oscore(byteArrayOf()), + ) + } + + @Test + fun decodeOscoreOptionWithSampleCoseObject() { + // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.6 Test Vector 6 + testReadOption( + encoded = """ + 9b # Option Delta: 9, Option Length: 11 + 19 14 08 37 CB F3 21 00 17 A2 D3 # Option Value: (COSE object) + """, + expected = Oscore("19140837CBF3210017A2D3".decodeHex().toByteArray()), + ) + } + + @Test + fun decodeEdhocOption() { + testReadOption( + encoded = """ + D0 08 # Option Delta: 21, Option Length: 0 + """, + expected = Edhoc, + ) + } + @Test fun decodeHopLimit() { testReadOption( diff --git a/koap-core/src/commonTest/kotlin/EncoderTest.kt b/koap-core/src/commonTest/kotlin/EncoderTest.kt index d19ded62..ae21fe65 100644 --- a/koap-core/src/commonTest/kotlin/EncoderTest.kt +++ b/koap-core/src/commonTest/kotlin/EncoderTest.kt @@ -7,6 +7,7 @@ import com.juul.koap.Message.Option.Block.Size.Bert import com.juul.koap.Message.Option.Block1 import com.juul.koap.Message.Option.Block2 import com.juul.koap.Message.Option.Echo +import com.juul.koap.Message.Option.Edhoc import com.juul.koap.Message.Option.HopLimit import com.juul.koap.Message.Option.NoResponse import com.juul.koap.Message.Option.NoResponse.NotInterestedIn.Response2xx @@ -15,6 +16,7 @@ import com.juul.koap.Message.Option.NoResponse.NotInterestedIn.Response5xx import com.juul.koap.Message.Option.Observe import com.juul.koap.Message.Option.Observe.Registration.Deregister import com.juul.koap.Message.Option.Observe.Registration.Register +import com.juul.koap.Message.Option.Oscore import com.juul.koap.Message.Option.QBlock1 import com.juul.koap.Message.Option.QBlock2 import com.juul.koap.Message.Option.RequestTag @@ -366,6 +368,38 @@ class EncoderTest { ) } + @Test + fun writeOscoreOptionWithEmptyCoseObject() { + testWriteOption( + option = Oscore(byteArrayOf()), + expected = """ + 90 # Option Delta: 9, Option Length: 0 + """, + ) + } + + @Test + fun writeOscoreOptionWithCoseObject() { + // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.4 Test Vector 4 + testWriteOption( + option = Oscore(byteArrayOf(0x09, 0x14)), + expected = """ + 92 # Option Delta: 9, Option Length: 2 + 09 14 # Option Value: 09 14 + """, + ) + } + + @Test + fun writeEdhocOption() { + testWriteOption( + option = Edhoc, + expected = """ + D0 08 # Option Delta: 21, Option Length: 0 + """, + ) + } + @Test fun observeOptionWithValueOutsideOfAllowableRangeThrowsIllegalArgumentException() { assertFailsWith { From df342bd83fa875c8fa3566e86f196223eba9d6d4 Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Tue, 25 Mar 2025 00:19:03 -0700 Subject: [PATCH 3/6] Add unit test for test vector 8 --- .../src/commonTest/kotlin/DecoderTest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/koap-core/src/commonTest/kotlin/DecoderTest.kt b/koap-core/src/commonTest/kotlin/DecoderTest.kt index 54c77e80..2df6cd8d 100644 --- a/koap-core/src/commonTest/kotlin/DecoderTest.kt +++ b/koap-core/src/commonTest/kotlin/DecoderTest.kt @@ -2,6 +2,7 @@ package com.juul.koap.test import com.juul.koap.Message import com.juul.koap.Message.Code.Method.GET +import com.juul.koap.Message.Code.Response.Changed import com.juul.koap.Message.Option.Block.Size.Bert import com.juul.koap.Message.Option.Block1 import com.juul.koap.Message.Option.Block2 @@ -277,6 +278,35 @@ class DecoderTest { ) } + // Test Vector 8: OSCORE Response with Partial IV, Server + // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.8 + @Test + fun decodeOscoreOptionTestVector8() { + val unprotectedCoapResponse = "64455d1f00003974ff48656c6c6f20576f726c6421".decodeHex().toByteArray().decode() + + val oscoreOptionValue = "0100".decodeHex().toByteArray() + val ciphertext = "4d4c13669384b67354b2b6175ff4b8658c666a6cf88e".decodeHex().toByteArray() + val protectedCoapResponse = "64445d1f00003974920100ff4d4c13669384b67354b2b6175ff4b8658c666a6cf88e".decodeHex().toByteArray() + + assertEquals( + expected = Message.Udp( + type = unprotectedCoapResponse.type, + + // the Outer Code SHALL be set to [..] 2.04 (Changed) for responses + // https://datatracker.ietf.org/doc/html/rfc8613#section-4.2 + code = Changed, + + id = unprotectedCoapResponse.id, + token = unprotectedCoapResponse.token, + options = listOf( + Oscore(oscoreOptionValue), + ), + payload = ciphertext, + ), + actual = protectedCoapResponse.decode(), + ) + } + @Test fun decodeEdhocOption() { testReadOption( From 667ec4eca602950d05d78ba92660208d280de7ed Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Tue, 25 Mar 2025 00:21:00 -0700 Subject: [PATCH 4/6] Quote text from RFC --- koap-core/src/commonTest/kotlin/DecoderTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koap-core/src/commonTest/kotlin/DecoderTest.kt b/koap-core/src/commonTest/kotlin/DecoderTest.kt index 2df6cd8d..3e03cc15 100644 --- a/koap-core/src/commonTest/kotlin/DecoderTest.kt +++ b/koap-core/src/commonTest/kotlin/DecoderTest.kt @@ -292,7 +292,7 @@ class DecoderTest { expected = Message.Udp( type = unprotectedCoapResponse.type, - // the Outer Code SHALL be set to [..] 2.04 (Changed) for responses + // "the Outer Code SHALL be set to [..] 2.04 (Changed) for responses" // https://datatracker.ietf.org/doc/html/rfc8613#section-4.2 code = Changed, From 73905df094c110e613df82eaab26287367ee7c67 Mon Sep 17 00:00:00 2001 From: Mattias de Zalenski Date: Tue, 25 Mar 2025 10:43:42 +0100 Subject: [PATCH 5/6] Rename incorrectly named coseObject to value --- koap-core/api/koap-core.api | 2 +- koap-core/api/koap-core.klib.api | 4 ++-- koap-core/src/commonMain/kotlin/Message.kt | 12 ++++++------ koap-core/src/commonMain/kotlin/ToFormat.kt | 2 +- koap-core/src/commonTest/kotlin/DecoderTest.kt | 4 ++-- koap-core/src/commonTest/kotlin/EncoderTest.kt | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/koap-core/api/koap-core.api b/koap-core/api/koap-core.api index 19a8bd5f..c44cec48 100644 --- a/koap-core/api/koap-core.api +++ b/koap-core/api/koap-core.api @@ -486,7 +486,7 @@ public final class com/juul/koap/Message$Option$Oscore : com/juul/koap/Message$O public final fun copy ([B)Lcom/juul/koap/Message$Option$Oscore; public static synthetic fun copy$default (Lcom/juul/koap/Message$Option$Oscore;[BILjava/lang/Object;)Lcom/juul/koap/Message$Option$Oscore; public fun equals (Ljava/lang/Object;)Z - public final fun getCoseObject ()[B + public final fun getValue ()[B public fun hashCode ()I public fun toString ()Ljava/lang/String; } diff --git a/koap-core/api/koap-core.klib.api b/koap-core/api/koap-core.klib.api index 8e846750..c2764019 100644 --- a/koap-core/api/koap-core.klib.api +++ b/koap-core/api/koap-core.klib.api @@ -486,8 +486,8 @@ sealed class com.juul.koap/Message { // com.juul.koap/Message|null[0] final class Oscore : com.juul.koap/Message.Option { // com.juul.koap/Message.Option.Oscore|null[0] constructor (kotlin/ByteArray) // com.juul.koap/Message.Option.Oscore.|(kotlin.ByteArray){}[0] - final val coseObject // com.juul.koap/Message.Option.Oscore.coseObject|{}coseObject[0] - final fun (): kotlin/ByteArray // com.juul.koap/Message.Option.Oscore.coseObject.|(){}[0] + final val value // com.juul.koap/Message.Option.Oscore.value|{}value[0] + final fun (): kotlin/ByteArray // com.juul.koap/Message.Option.Oscore.value.|(){}[0] final fun component1(): kotlin/ByteArray // com.juul.koap/Message.Option.Oscore.component1|component1(){}[0] final fun copy(kotlin/ByteArray = ...): com.juul.koap/Message.Option.Oscore // com.juul.koap/Message.Option.Oscore.copy|copy(kotlin.ByteArray){}[0] diff --git a/koap-core/src/commonMain/kotlin/Message.kt b/koap-core/src/commonMain/kotlin/Message.kt index d9c56c51..4b3595a7 100644 --- a/koap-core/src/commonMain/kotlin/Message.kt +++ b/koap-core/src/commonMain/kotlin/Message.kt @@ -517,20 +517,20 @@ sealed class Message { /** RFC 8613 2. OSCORE */ data class Oscore( - val coseObject: ByteArray, + val value: ByteArray, ) : Option() { init { - require(coseObject.size in OSCORE_LENGTH_RANGE) { - "Oscore length of ${coseObject.size} is outside allowable range of $OSCORE_LENGTH_RANGE" + require(value.size in OSCORE_LENGTH_RANGE) { + "Oscore length of ${value.size} is outside allowable range of $OSCORE_LENGTH_RANGE" } } override fun equals(other: Any?): Boolean = - this === other || (other is Oscore && coseObject.contentEquals(other.coseObject)) + this === other || (other is Oscore && value.contentEquals(other.value)) - override fun hashCode(): Int = coseObject.contentHashCode() + override fun hashCode(): Int = value.contentHashCode() - override fun toString(): String = "Oscore(coseObject=${coseObject.toHexString()})" + override fun toString(): String = "Oscore(value=${value.toHexString()})" } /** RFC 9668 3.1. EDHOC */ diff --git a/koap-core/src/commonMain/kotlin/ToFormat.kt b/koap-core/src/commonMain/kotlin/ToFormat.kt index 6d6b39de..2b863d82 100644 --- a/koap-core/src/commonMain/kotlin/ToFormat.kt +++ b/koap-core/src/commonMain/kotlin/ToFormat.kt @@ -45,7 +45,7 @@ internal fun Option.toFormat(): Format = is Observe -> uint(6, option.value) is UriPort -> uint(7, option.port) is LocationPath -> string(8, option.uri) - is Oscore -> opaque(9, option.coseObject) + is Oscore -> opaque(9, option.value) is UriPath -> string(11, option.uri) is ContentFormat -> uint(12, option.format) is MaxAge -> uint(14, option.seconds) diff --git a/koap-core/src/commonTest/kotlin/DecoderTest.kt b/koap-core/src/commonTest/kotlin/DecoderTest.kt index 3e03cc15..641b62c0 100644 --- a/koap-core/src/commonTest/kotlin/DecoderTest.kt +++ b/koap-core/src/commonTest/kotlin/DecoderTest.kt @@ -257,7 +257,7 @@ class DecoderTest { } @Test - fun decodeOscoreOptionWithEmptyCoseObject() { + fun decodeOscoreOptionWithEmptyValue() { testReadOption( encoded = """ 90 # Option Delta: 9, Option Length: 0 @@ -267,7 +267,7 @@ class DecoderTest { } @Test - fun decodeOscoreOptionWithSampleCoseObject() { + fun decodeOscoreOptionTestVector6() { // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.6 Test Vector 6 testReadOption( encoded = """ diff --git a/koap-core/src/commonTest/kotlin/EncoderTest.kt b/koap-core/src/commonTest/kotlin/EncoderTest.kt index ae21fe65..5be1f907 100644 --- a/koap-core/src/commonTest/kotlin/EncoderTest.kt +++ b/koap-core/src/commonTest/kotlin/EncoderTest.kt @@ -369,7 +369,7 @@ class EncoderTest { } @Test - fun writeOscoreOptionWithEmptyCoseObject() { + fun writeOscoreOptionWithEmptyValue() { testWriteOption( option = Oscore(byteArrayOf()), expected = """ @@ -379,7 +379,7 @@ class EncoderTest { } @Test - fun writeOscoreOptionWithCoseObject() { + fun writeOscoreOptionTestVector4() { // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.4 Test Vector 4 testWriteOption( option = Oscore(byteArrayOf(0x09, 0x14)), From 292722d1ed7ed9ec143847cddf671caa4e824f97 Mon Sep 17 00:00:00 2001 From: Mattias de Zalenski Date: Tue, 25 Mar 2025 21:24:34 +0100 Subject: [PATCH 6/6] Add unit test for test vector 4-7 --- .../src/commonTest/kotlin/DecoderTest.kt | 123 +++++++++++++++++- 1 file changed, 116 insertions(+), 7 deletions(-) diff --git a/koap-core/src/commonTest/kotlin/DecoderTest.kt b/koap-core/src/commonTest/kotlin/DecoderTest.kt index 641b62c0..a8c6f5a5 100644 --- a/koap-core/src/commonTest/kotlin/DecoderTest.kt +++ b/koap-core/src/commonTest/kotlin/DecoderTest.kt @@ -2,6 +2,7 @@ package com.juul.koap.test import com.juul.koap.Message import com.juul.koap.Message.Code.Method.GET +import com.juul.koap.Message.Code.Method.POST import com.juul.koap.Message.Code.Response.Changed import com.juul.koap.Message.Option.Block.Size.Bert import com.juul.koap.Message.Option.Block1 @@ -266,15 +267,123 @@ class DecoderTest { ) } + // Test Vector 4: OSCORE Request, Client + // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.4 + @Test + fun decodeOscoreOptionTestVector4() { + val unprotectedCoapRequest = "44015d1f00003974396c6f63616c686f737483747631".decodeHex().toByteArray().decode() + + val oscoreOptionValue = "0914".decodeHex().toByteArray() + val ciphertext = "612f1092f1776f1c1668b3825e".decodeHex().toByteArray() + val protectedCoapRequest = "44025d1f00003974396c6f63616c686f7374620914ff612f1092f1776f1c1668b3825e".decodeHex().toByteArray() + + assertEquals( + expected = Message.Udp( + type = unprotectedCoapRequest.type, + + // "the Outer Code SHALL be set to 0.02 (POST) for requests [..]" + // https://datatracker.ietf.org/doc/html/rfc8613#section-4.2 + code = POST, + + id = unprotectedCoapRequest.id, + token = unprotectedCoapRequest.token, + options = listOf( + UriHost("localhost"), + Oscore(oscoreOptionValue), + ), + payload = ciphertext, + ), + actual = protectedCoapRequest.decode(), + ) + } + + // Test Vector 5: OSCORE Request, Client + // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.5 + @Test + fun decodeOscoreOptionTestVector5() { + val unprotectedCoapRequest = "440171c30000b932396c6f63616c686f737483747631".decodeHex().toByteArray().decode() + + val oscoreOptionValue = "091400".decodeHex().toByteArray() + val ciphertext = "4ed339a5a379b0b8bc731fffb0".decodeHex().toByteArray() + val protectedCoapRequest = "440271c30000b932396c6f63616c686f737463091400ff4ed339a5a379b0b8bc731fffb0".decodeHex().toByteArray() + + assertEquals( + expected = Message.Udp( + type = unprotectedCoapRequest.type, + + // "the Outer Code SHALL be set to 0.02 (POST) for requests [..]" + // https://datatracker.ietf.org/doc/html/rfc8613#section-4.2 + code = POST, + + id = unprotectedCoapRequest.id, + token = unprotectedCoapRequest.token, + options = listOf( + UriHost("localhost"), + Oscore(oscoreOptionValue), + ), + payload = ciphertext, + ), + actual = protectedCoapRequest.decode(), + ) + } + + // Test Vector 6: OSCORE Request, Client + // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.6 @Test fun decodeOscoreOptionTestVector6() { - // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.6 Test Vector 6 - testReadOption( - encoded = """ - 9b # Option Delta: 9, Option Length: 11 - 19 14 08 37 CB F3 21 00 17 A2 D3 # Option Value: (COSE object) - """, - expected = Oscore("19140837CBF3210017A2D3".decodeHex().toByteArray()), + val unprotectedCoapRequest = "44012f8eef9bbf7a396c6f63616c686f737483747631".decodeHex().toByteArray().decode() + + val oscoreOptionValue = "19140837cbf3210017a2d3".decodeHex().toByteArray() + val ciphertext = "72cd7273fd331ac45cffbe55c3".decodeHex().toByteArray() + val protectedCoapRequest = + "44022f8eef9bbf7a396c6f63616c686f73746b19140837cbf3210017a2d3ff72cd7273fd331ac45cffbe55c3".decodeHex().toByteArray() + + assertEquals( + expected = Message.Udp( + type = unprotectedCoapRequest.type, + + // "the Outer Code SHALL be set to 0.02 (POST) for requests [..]" + // https://datatracker.ietf.org/doc/html/rfc8613#section-4.2 + code = POST, + + id = unprotectedCoapRequest.id, + token = unprotectedCoapRequest.token, + options = listOf( + UriHost("localhost"), + Oscore(oscoreOptionValue), + ), + payload = ciphertext, + ), + actual = protectedCoapRequest.decode(), + ) + } + + // Test Vector 7: OSCORE Response, Server + // https://datatracker.ietf.org/doc/html/rfc8613#appendix-C.7 + @Test + fun decodeOscoreOptionTestVector7() { + val unprotectedCoapResponse = "64455d1f00003974ff48656c6c6f20576f726c6421".decodeHex().toByteArray().decode() + + val oscoreOptionValue = "".decodeHex().toByteArray() + val ciphertext = "dbaad1e9a7e7b2a813d3c31524378303cdafae119106".decodeHex().toByteArray() + val protectedCoapResponse = "64445d1f0000397490ffdbaad1e9a7e7b2a813d3c31524378303cdafae119106".decodeHex().toByteArray() + + assertEquals( + expected = Message.Udp( + type = unprotectedCoapResponse.type, + + // "the Outer Code SHALL be set to [..] 2.04 (Changed) for responses" + // https://datatracker.ietf.org/doc/html/rfc8613#section-4.2 + code = Changed, + + id = unprotectedCoapResponse.id, + token = unprotectedCoapResponse.token, + options = listOf( + Oscore(oscoreOptionValue), + ), + payload = ciphertext, + ), + actual = protectedCoapResponse.decode(), ) }