From 59b290fcc7da5c2eda7c4fa79def0acc88c796fa Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Wed, 12 Aug 2020 01:58:51 +0300 Subject: [PATCH] Refactor minasn1 and fix security issues. (#323) * Refactor minasn1 and fix security issues. * Fix for RSA test vectors. --- libp2p/crypto/minasn1.nim | 526 ++++++++++++++++++++++++-------------- tests/testminasn1.nim | 214 ++++++++++++++++ tests/testnative.nim | 3 +- 3 files changed, 545 insertions(+), 198 deletions(-) create mode 100644 tests/testminasn1.nim diff --git a/libp2p/crypto/minasn1.nim b/libp2p/crypto/minasn1.nim index 32c71fe51..fbfd33f73 100644 --- a/libp2p/crypto/minasn1.nim +++ b/libp2p/crypto/minasn1.nim @@ -11,7 +11,7 @@ {.push raises: [Defect].} -import stew/[endians2, results] +import stew/[endians2, results, ctops] export results # We use `ncrutils` for constant-time hexadecimal encoding/decoding procedures. import nimcrypto/utils as ncrutils @@ -123,7 +123,7 @@ proc len*[T: Asn1Buffer|Asn1Composite](abc: T): int {.inline.} = len(abc.buffer) - abc.offset proc len*(field: Asn1Field): int {.inline.} = - result = field.length + field.length template getPtr*(field: untyped): pointer = cast[pointer](unsafeAddr field.buffer[field.offset]) @@ -154,30 +154,32 @@ proc code*(tag: Asn1Tag): byte {.inline.} = of Asn1Tag.Context: 0xA0'u8 -proc asn1EncodeLength*(dest: var openarray[byte], length: int64): int = +proc asn1EncodeLength*(dest: var openarray[byte], length: uint64): int = ## Encode ASN.1 DER length part of TLV triple and return number of bytes ## (octets) used. ## ## If length of ``dest`` is less then number of required bytes to encode - ## ``length`` value, then result of encoding will not be stored in ``dest`` + ## ``length`` value, then result of encoding WILL NOT BE stored in ``dest`` ## but number of bytes (octets) required will be returned. - if length < 0x80: + if length < 0x80'u64: if len(dest) >= 1: - dest[0] = cast[byte](length) - result = 1 + dest[0] = byte(length and 0x7F'u64) + 1 else: - result = 0 + var res = 1'u64 var z = length while z != 0: - inc(result) + inc(res) z = z shr 8 - if len(dest) >= result + 1: - dest[0] = cast[byte](0x80 + result) + if uint64(len(dest)) >= res: + dest[0] = byte((0x80'u64 + (res - 1'u64)) and 0xFF) var o = 1 - for j in countdown(result - 1, 0): - dest[o] = cast[byte](length shr (j shl 3)) + for j in countdown(res - 2, 0): + dest[o] = byte((length shr (j shl 3)) and 0xFF'u64) inc(o) - inc(result) + # Because our `length` argument is `uint64`, `res` could not be bigger + # then 9, so it is safe to convert it to `int`. + int(res) proc asn1EncodeInteger*(dest: var openarray[byte], value: openarray[byte]): int = @@ -185,35 +187,46 @@ proc asn1EncodeInteger*(dest: var openarray[byte], ## and return number of bytes (octets) used. ## ## If length of ``dest`` is less then number of required bytes to encode - ## ``value``, then result of encoding will not be stored in ``dest`` + ## ``value``, then result of encoding WILL NOT BE stored in ``dest`` ## but number of bytes (octets) required will be returned. var buffer: array[16, byte] - var o = 0 var lenlen = 0 - for i in 0.. 0: - if o == len(value): - dec(o) - if value[o] >= 0x80'u8: - lenlen = asn1EncodeLength(buffer, len(value) - o + 1) - result = 1 + lenlen + 1 + (len(value) - o) + + let offset = + block: + var o = 0 + for i in 0 ..< len(value): + if value[o] != 0x00: + break + inc(o) + if o < len(value): + o + else: + o - 1 + + let destlen = + if len(value) > 0: + if value[offset] >= 0x80'u8: + lenlen = asn1EncodeLength(buffer, uint64(len(value) - offset + 1)) + 1 + lenlen + 1 + (len(value) - offset) + else: + lenlen = asn1EncodeLength(buffer, uint64(len(value) - offset)) + 1 + lenlen + (len(value) - offset) else: - lenlen = asn1EncodeLength(buffer, len(value) - o) - result = 1 + lenlen + (len(value) - o) - else: - result = 2 - if len(dest) >= result: - var s = 1 + 2 + + if len(dest) >= destlen: + var shift = 1 dest[0] = Asn1Tag.Integer.code() copyMem(addr dest[1], addr buffer[0], lenlen) - if value[o] >= 0x80'u8: - dest[1 + lenlen] = 0x00'u8 - s = 2 - if len(value) > 0: - copyMem(addr dest[s + lenlen], unsafeAddr value[o], len(value) - o) + # If ``destlen > 2`` it means that ``len(value) > 0`` too. + if destlen > 2: + if value[offset] >= 0x80'u8: + dest[1 + lenlen] = 0x00'u8 + shift = 2 + copyMem(addr dest[shift + lenlen], unsafeAddr value[offset], + len(value) - offset) + destlen proc asn1EncodeInteger*[T: SomeUnsignedInt](dest: var openarray[byte], value: T): int = @@ -232,11 +245,12 @@ proc asn1EncodeBoolean*(dest: var openarray[byte], value: bool): int = ## If length of ``dest`` is less then number of required bytes to encode ## ``value``, then result of encoding will not be stored in ``dest`` ## but number of bytes (octets) required will be returned. - result = 3 - if len(dest) >= result: + let res = 3 + if len(dest) >= res: dest[0] = Asn1Tag.Boolean.code() dest[1] = 0x01'u8 dest[2] = if value: 0xFF'u8 else: 0x00'u8 + res proc asn1EncodeNull*(dest: var openarray[byte]): int = ## Encode ASN.1 DER `NULL` and return number of bytes (octets) used. @@ -244,13 +258,14 @@ proc asn1EncodeNull*(dest: var openarray[byte]): int = ## If length of ``dest`` is less then number of required bytes to encode ## ``value``, then result of encoding will not be stored in ``dest`` ## but number of bytes (octets) required will be returned. - result = 2 - if len(dest) >= result: + let res = 2 + if len(dest) >= res: dest[0] = Asn1Tag.Null.code() dest[1] = 0x00'u8 + res proc asn1EncodeOctetString*(dest: var openarray[byte], - value: openarray[byte]): int = + value: openarray[byte]): int = ## Encode array of bytes as ASN.1 DER `OCTET STRING` and return number of ## bytes (octets) used. ## @@ -258,38 +273,50 @@ proc asn1EncodeOctetString*(dest: var openarray[byte], ## ``value``, then result of encoding will not be stored in ``dest`` ## but number of bytes (octets) required will be returned. var buffer: array[16, byte] - var lenlen = asn1EncodeLength(buffer, len(value)) - result = 1 + lenlen + len(value) - if len(dest) >= result: + let lenlen = asn1EncodeLength(buffer, uint64(len(value))) + let res = 1 + lenlen + len(value) + if len(dest) >= res: dest[0] = Asn1Tag.OctetString.code() copyMem(addr dest[1], addr buffer[0], lenlen) if len(value) > 0: copyMem(addr dest[1 + lenlen], unsafeAddr value[0], len(value)) + res proc asn1EncodeBitString*(dest: var openarray[byte], value: openarray[byte], bits = 0): int = ## Encode array of bytes as ASN.1 DER `BIT STRING` and return number of bytes ## (octets) used. ## - ## ``bits`` number of used bits in ``value``. If ``bits == 0``, all the bits - ## from ``value`` are used, if ``bits != 0`` only number of ``bits`` will be - ## used. + ## ``bits`` number of unused bits in ``value``. If ``bits == 0``, all the bits + ## from ``value`` will be used. ## ## If length of ``dest`` is less then number of required bytes to encode ## ``value``, then result of encoding will not be stored in ``dest`` ## but number of bytes (octets) required will be returned. var buffer: array[16, byte] - var lenlen = asn1EncodeLength(buffer, len(value) + 1) - var lbits = 0 - if bits != 0: - lbits = len(value) shl 3 - bits - result = 1 + lenlen + 1 + len(value) - if len(dest) >= result: + let bitlen = + if bits != 0: + (len(value) shl 3) - bits + else: + (len(value) shl 3) + + # Number of bytes used + let bytelen = (bitlen + 7) shr 3 + # Number of unused bits + let unused = (8 - (bitlen and 7)) and 7 + let mask = not((1'u8 shl unused) - 1'u8) + var lenlen = asn1EncodeLength(buffer, uint64(bytelen + 1)) + let res = 1 + lenlen + 1 + len(value) + if len(dest) >= res: dest[0] = Asn1Tag.BitString.code() copyMem(addr dest[1], addr buffer[0], lenlen) - dest[1 + lenlen] = cast[byte](lbits) - if len(value) > 0: - copyMem(addr dest[2 + lenlen], unsafeAddr value[0], len(value)) + dest[1 + lenlen] = byte(unused) + if bytelen > 0: + let lastbyte = value[bytelen - 1] + copyMem(addr dest[2 + lenlen], unsafeAddr value[0], bytelen) + # Set unused bits to zero + dest[2 + lenlen + bytelen - 1] = lastbyte and mask + res proc asn1EncodeTag[T: SomeUnsignedInt](dest: var openarray[byte], value: T): int = @@ -297,53 +324,48 @@ proc asn1EncodeTag[T: SomeUnsignedInt](dest: var openarray[byte], if value <= cast[T](0x7F): if len(dest) >= 1: dest[0] = cast[byte](value) - result = 1 + 1 else: var s = 0 + var res = 0 while v != 0: v = v shr 7 s += 7 - inc(result) - if len(dest) >= result: + inc(res) + if len(dest) >= res: var k = 0 while s != 0: s -= 7 dest[k] = cast[byte](((value shr s) and cast[T](0x7F)) or cast[T](0x80)) inc(k) dest[k - 1] = dest[k - 1] and 0x7F'u8 + res proc asn1EncodeOid*(dest: var openarray[byte], value: openarray[int]): int = ## Encode array of integers ``value`` as ASN.1 DER `OBJECT IDENTIFIER` and ## return number of bytes (octets) used. ## - ## OBJECT IDENTIFIER requirements for ``value`` elements: - ## * len(value) >= 2 - ## * value[0] >= 1 and value[0] < 2 - ## * value[1] >= 1 and value[1] < 39 - ## ## If length of ``dest`` is less then number of required bytes to encode ## ``value``, then result of encoding will not be stored in ``dest`` ## but number of bytes (octets) required will be returned. var buffer: array[16, byte] - result = 1 - doAssert(len(value) >= 2) - doAssert(value[0] >= 1 and value[0] < 2) - doAssert(value[1] >= 1 and value[1] <= 39) + var res = 1 var oidlen = 1 for i in 2..= result: + res += asn1EncodeLength(buffer, uint64(oidlen)) + res += oidlen + if len(dest) >= res: let last = dest.high var offset = 1 dest[0] = Asn1Tag.Oid.code() - offset += asn1EncodeLength(dest.toOpenArray(offset, last), oidlen) + offset += asn1EncodeLength(dest.toOpenArray(offset, last), uint64(oidlen)) dest[offset] = cast[byte](value[0] * 40 + value[1]) offset += 1 for i in 2..= result: + let lenlen = asn1EncodeLength(buffer, uint64(len(value))) + let res = 1 + lenlen + len(value) + if len(dest) >= res: dest[0] = Asn1Tag.Oid.code() copyMem(addr dest[1], addr buffer[0], lenlen) copyMem(addr dest[1 + lenlen], unsafeAddr value[0], len(value)) + res proc asn1EncodeSequence*(dest: var openarray[byte], value: openarray[byte]): int = @@ -372,12 +395,13 @@ proc asn1EncodeSequence*(dest: var openarray[byte], ## ``value``, then result of encoding will not be stored in ``dest`` ## but number of bytes (octets) required will be returned. var buffer: array[16, byte] - var lenlen = asn1EncodeLength(buffer, len(value)) - result = 1 + lenlen + len(value) - if len(dest) >= result: + let lenlen = asn1EncodeLength(buffer, uint64(len(value))) + let res = 1 + lenlen + len(value) + if len(dest) >= res: dest[0] = Asn1Tag.Sequence.code() copyMem(addr dest[1], addr buffer[0], lenlen) copyMem(addr dest[1 + lenlen], unsafeAddr value[0], len(value)) + res proc asn1EncodeComposite*(dest: var openarray[byte], value: Asn1Composite): int = @@ -387,29 +411,34 @@ proc asn1EncodeComposite*(dest: var openarray[byte], ## ``value``, then result of encoding will not be stored in ``dest`` ## but number of bytes (octets) required will be returned. var buffer: array[16, byte] - var lenlen = asn1EncodeLength(buffer, len(value.buffer)) - result = 1 + lenlen + len(value.buffer) - if len(dest) >= result: + let lenlen = asn1EncodeLength(buffer, uint64(len(value.buffer))) + let res = 1 + lenlen + len(value.buffer) + if len(dest) >= res: dest[0] = value.tag.code() copyMem(addr dest[1], addr buffer[0], lenlen) copyMem(addr dest[1 + lenlen], unsafeAddr value.buffer[0], len(value.buffer)) + res proc asn1EncodeContextTag*(dest: var openarray[byte], value: openarray[byte], tag: int): int = ## Encode ASN.1 DER `CONTEXT SPECIFIC TAG` ``tag`` for value ``value`` and ## return number of bytes (octets) used. ## + ## Note: Only values in [0, 15] range can be used as context tag ``tag`` + ## values. + ## ## If length of ``dest`` is less then number of required bytes to encode ## ``value``, then result of encoding will not be stored in ``dest`` ## but number of bytes (octets) required will be returned. var buffer: array[16, byte] - var lenlen = asn1EncodeLength(buffer, len(value)) - result = 1 + lenlen + len(value) - if len(dest) >= result: - dest[0] = 0xA0'u8 or (cast[byte](tag) and 0x0F) + let lenlen = asn1EncodeLength(buffer, uint64(len(value))) + let res = 1 + lenlen + len(value) + if len(dest) >= res: + dest[0] = 0xA0'u8 or (byte(tag and 0xFF) and 0x0F'u8) copyMem(addr dest[1], addr buffer[0], lenlen) copyMem(addr dest[1 + lenlen], unsafeAddr value[0], len(value)) + res proc getLength(ab: var Asn1Buffer): Asn1Result[uint64] = ## Decode length part of ASN.1 TLV triplet. @@ -458,197 +487,300 @@ proc read*(ab: var Asn1Buffer): Asn1Result[Asn1Field] = field: Asn1Field tag, ttag, offset: int length, tlength: uint64 - klass: Asn1Class + aclass: Asn1Class inclass: bool inclass = false while true: offset = ab.offset - klass = ? ab.getTag(tag) + aclass = ? ab.getTag(tag) - if klass == Asn1Class.ContextSpecific: + case aclass + of Asn1Class.ContextSpecific: if inclass: return err(Asn1Error.Incorrect) - - inclass = true - ttag = tag - tlength = ? ab.getLength() - - elif klass == Asn1Class.Universal: + else: + inclass = true + ttag = tag + tlength = ? ab.getLength() + of Asn1Class.Universal: length = ? ab.getLength() if inclass: if length >= tlength: return err(Asn1Error.Incorrect) - if cast[byte](tag) == Asn1Tag.Boolean.code(): + case byte(tag) + of Asn1Tag.Boolean.code(): # BOOLEAN if length != 1: return err(Asn1Error.Incorrect) - if not ab.isEnough(cast[int](length)): + + if not ab.isEnough(int(length)): return err(Asn1Error.Incomplete) + let b = ab.buffer[ab.offset] if b != 0xFF'u8 and b != 0x00'u8: return err(Asn1Error.Incorrect) - field = Asn1Field(kind: Asn1Tag.Boolean, klass: klass, - index: ttag, offset: cast[int](ab.offset), + field = Asn1Field(kind: Asn1Tag.Boolean, klass: aclass, + index: ttag, offset: int(ab.offset), length: 1) shallowCopy(field.buffer, ab.buffer) field.vbool = (b == 0xFF'u8) ab.offset += 1 return ok(field) - elif cast[byte](tag) == Asn1Tag.Integer.code(): + + of Asn1Tag.Integer.code(): # INTEGER - if not ab.isEnough(cast[int](length)): - return err(Asn1Error.Incomplete) - if ab.buffer[ab.offset] == 0x00'u8: - length -= 1 - ab.offset += 1 - field = Asn1Field(kind: Asn1Tag.Integer, klass: klass, - index: ttag, offset: cast[int](ab.offset), - length: cast[int](length)) - shallowCopy(field.buffer, ab.buffer) - if length <= 8: - for i in 0.. 1: + return err(Asn1Error.Incorrect) + + if zc == 0: + # Negative or Positive integer + field = Asn1Field(kind: Asn1Tag.Integer, klass: aclass, + index: ttag, offset: int(ab.offset), + length: int(length)) + shallowCopy(field.buffer, ab.buffer) + if (ab.buffer[ab.offset] and 0x80'u8) == 0x80'u8: + # Negative integer + if length <= 8: + # We need this transformation because our field.vint is uint64. + for i in 0 ..< 8: + if i < 8 - int(length): + field.vint = (field.vint shl 8) or 0xFF'u64 + else: + let offset = ab.offset + i - (8 - int(length)) + field.vint = (field.vint shl 8) or uint64(ab.buffer[offset]) + else: + # Positive integer + if length <= 8: + for i in 0 ..< int(length): + field.vint = (field.vint shl 8) or + uint64(ab.buffer[ab.offset + i]) + ab.offset += int(length) + return ok(field) + else: + if length == 1: + # Zero value integer + field = Asn1Field(kind: Asn1Tag.Integer, klass: aclass, + index: ttag, offset: int(ab.offset), + length: int(length), vint: 0'u64) + shallowCopy(field.buffer, ab.buffer) + ab.offset += int(length) + return ok(field) + else: + # Positive integer with leading zero + field = Asn1Field(kind: Asn1Tag.Integer, klass: aclass, + index: ttag, offset: int(ab.offset) + 1, + length: int(length) - 1) + shallowCopy(field.buffer, ab.buffer) + if length <= 9: + for i in 1 ..< int(length): + field.vint = (field.vint shl 8) or + uint64(ab.buffer[ab.offset + i]) + ab.offset += int(length) + return ok(field) + + of Asn1Tag.BitString.code(): # BIT STRING - if not ab.isEnough(cast[int](length)): + if length == 0: + # BIT STRING should include `unused` bits field, so length should be + # bigger then 1. + return err(Asn1Error.Incorrect) + + elif length == 1: + if ab.buffer[ab.offset] != 0x00'u8: + return err(Asn1Error.Incorrect) + else: + # Zero-length BIT STRING. + field = Asn1Field(kind: Asn1Tag.BitString, klass: aclass, + index: ttag, offset: int(ab.offset + 1), + length: 0, ubits: 0) + shallowCopy(field.buffer, ab.buffer) + ab.offset += int(length) + return ok(field) + + else: + if not ab.isEnough(int(length)): + return err(Asn1Error.Incomplete) + + let unused = ab.buffer[ab.offset] + if unused > 0x07'u8: + # Number of unused bits should not be bigger then `7`. + return err(Asn1Error.Incorrect) + + let mask = (1'u8 shl int(unused)) - 1'u8 + if (ab.buffer[ab.offset + int(length) - 1] and mask) != 0x00'u8: + ## All unused bits should be set to `0`. + return err(Asn1Error.Incorrect) + + field = Asn1Field(kind: Asn1Tag.BitString, klass: aclass, + index: ttag, offset: int(ab.offset + 1), + length: int(length - 1), ubits: int(unused)) + shallowCopy(field.buffer, ab.buffer) + ab.offset += int(length) + return ok(field) + + of Asn1Tag.OctetString.code(): + # OCTET STRING + if not ab.isEnough(int(length)): return err(Asn1Error.Incomplete) - field = Asn1Field(kind: Asn1Tag.BitString, klass: klass, - index: ttag, offset: cast[int](ab.offset + 1), - length: cast[int](length - 1)) + + field = Asn1Field(kind: Asn1Tag.OctetString, klass: aclass, + index: ttag, offset: int(ab.offset), + length: int(length)) shallowCopy(field.buffer, ab.buffer) - field.ubits = cast[int](((length - 1) shl 3) - ab.buffer[ab.offset]) - ab.offset += cast[int](length) + ab.offset += int(length) return ok(field) - elif cast[byte](tag) == Asn1Tag.OctetString.code(): - # OCT STRING - if not ab.isEnough(cast[int](length)): - return err(Asn1Error.Incomplete) - field = Asn1Field(kind: Asn1Tag.OctetString, klass: klass, - index: ttag, offset: cast[int](ab.offset), - length: cast[int](length)) - shallowCopy(field.buffer, ab.buffer) - ab.offset += cast[int](length) - return ok(field) - elif cast[byte](tag) == Asn1Tag.Null.code(): + + of Asn1Tag.Null.code(): # NULL if length != 0: return err(Asn1Error.Incorrect) - field = Asn1Field(kind: Asn1Tag.Null, klass: klass, - index: ttag, offset: cast[int](ab.offset), - length: 0) + + field = Asn1Field(kind: Asn1Tag.Null, klass: aclass, index: ttag, + offset: int(ab.offset), length: 0) shallowCopy(field.buffer, ab.buffer) - ab.offset += cast[int](length) + ab.offset += int(length) return ok(field) - elif cast[byte](tag) == Asn1Tag.Oid.code(): + + of Asn1Tag.Oid.code(): # OID - if not ab.isEnough(cast[int](length)): + if not ab.isEnough(int(length)): return err(Asn1Error.Incomplete) - field = Asn1Field(kind: Asn1Tag.Oid, klass: klass, - index: ttag, offset: cast[int](ab.offset), - length: cast[int](length)) + + field = Asn1Field(kind: Asn1Tag.Oid, klass: aclass, + index: ttag, offset: int(ab.offset), + length: int(length)) shallowCopy(field.buffer, ab.buffer) - ab.offset += cast[int](length) + ab.offset += int(length) return ok(field) - elif cast[byte](tag) == Asn1Tag.Sequence.code(): + + of Asn1Tag.Sequence.code(): # SEQUENCE - if not ab.isEnough(cast[int](length)): + if not ab.isEnough(int(length)): return err(Asn1Error.Incomplete) - field = Asn1Field(kind: Asn1Tag.Sequence, klass: klass, - index: ttag, offset: cast[int](ab.offset), - length: cast[int](length)) + + field = Asn1Field(kind: Asn1Tag.Sequence, klass: aclass, + index: ttag, offset: int(ab.offset), + length: int(length)) shallowCopy(field.buffer, ab.buffer) - ab.offset += cast[int](length) + ab.offset += int(length) return ok(field) + else: return err(Asn1Error.NoSupport) + inclass = false ttag = 0 else: return err(Asn1Error.NoSupport) -proc getBuffer*(field: Asn1Field): Asn1Buffer = +proc getBuffer*(field: Asn1Field): Asn1Buffer {.inline.} = ## Return ``field`` as Asn1Buffer to enter composite types. - shallowCopy(result.buffer, field.buffer) - result.offset = field.offset - result.length = field.length + Asn1Buffer(buffer: field.buffer, offset: field.offset, length: field.length) proc `==`*(field: Asn1Field, data: openarray[byte]): bool = ## Compares field ``field`` data with ``data`` and returns ``true`` if both ## buffers are equal. let length = len(field.buffer) - if length > 0: - if field.length == len(data): - result = equalMem(unsafeAddr field.buffer[field.offset], - unsafeAddr data[0], field.length) + if length == 0 and len(data) == 0: + true + else: + if length > 0: + if field.length == len(data): + CT.isEqual( + field.buffer.toOpenArray(field.offset, + field.offset + field.length - 1), + data.toOpenArray(0, field.length - 1)) + else: + false + else: + false proc init*(t: typedesc[Asn1Buffer], data: openarray[byte]): Asn1Buffer = ## Initialize ``Asn1Buffer`` from array of bytes ``data``. - result.buffer = @data + Asn1Buffer(buffer: @data) proc init*(t: typedesc[Asn1Buffer], data: string): Asn1Buffer = ## Initialize ``Asn1Buffer`` from hexadecimal string ``data``. - result.buffer = ncrutils.fromHex(data) + Asn1Buffer(buffer: ncrutils.fromHex(data)) proc init*(t: typedesc[Asn1Buffer]): Asn1Buffer = ## Initialize empty ``Asn1Buffer``. - result.buffer = newSeq[byte]() + Asn1Buffer(buffer: newSeq[byte]()) proc init*(t: typedesc[Asn1Composite], tag: Asn1Tag): Asn1Composite = ## Initialize ``Asn1Composite`` with tag ``tag``. - result.tag = tag - result.buffer = newSeq[byte]() + Asn1Composite(tag: tag, buffer: newSeq[byte]()) proc init*(t: typedesc[Asn1Composite], idx: int): Asn1Composite = ## Initialize ``Asn1Composite`` with tag context-specific id ``id``. - result.tag = Asn1Tag.Context - result.idx = idx - result.buffer = newSeq[byte]() + Asn1Composite(tag: Asn1Tag.Context, idx: idx, buffer: newSeq[byte]()) proc `$`*(buffer: Asn1Buffer): string = ## Return string representation of ``buffer``. - result = ncrutils.toHex(buffer.toOpenArray()) + ncrutils.toHex(buffer.toOpenArray()) proc `$`*(field: Asn1Field): string = ## Return string representation of ``field``. - result = "[" - result.add($field.kind) - result.add("]") - if field.kind == Asn1Tag.NoSupport: - result.add(" ") - result.add(ncrutils.toHex(field.toOpenArray())) - elif field.kind == Asn1Tag.Boolean: - result.add(" ") - result.add($field.vbool) - elif field.kind == Asn1Tag.Integer: - result.add(" ") + var res = "[" + res.add($field.kind) + res.add("]") + case field.kind + of Asn1Tag.Boolean: + res.add(" ") + res.add($field.vbool) + res + of Asn1Tag.Integer: + res.add(" ") if field.length <= 8: - result.add($field.vint) + res.add($field.vint) else: - result.add(ncrutils.toHex(field.toOpenArray())) - elif field.kind == Asn1Tag.BitString: - result.add(" ") - result.add("(") - result.add($field.ubits) - result.add(" bits) ") - result.add(ncrutils.toHex(field.toOpenArray())) - elif field.kind == Asn1Tag.OctetString: - result.add(" ") - result.add(ncrutils.toHex(field.toOpenArray())) - elif field.kind == Asn1Tag.Null: - result.add(" NULL") - elif field.kind == Asn1Tag.Oid: - result.add(" ") - result.add(ncrutils.toHex(field.toOpenArray())) - elif field.kind == Asn1Tag.Sequence: - result.add(" ") - result.add(ncrutils.toHex(field.toOpenArray())) + res.add(ncrutils.toHex(field.toOpenArray())) + res + of Asn1Tag.BitString: + res.add(" ") + res.add("(") + res.add($field.ubits) + res.add(" bits) ") + res.add(ncrutils.toHex(field.toOpenArray())) + res + of Asn1Tag.OctetString: + res.add(" ") + res.add(ncrutils.toHex(field.toOpenArray())) + res + of Asn1Tag.Null: + res.add(" NULL") + res + of Asn1Tag.Oid: + res.add(" ") + res.add(ncrutils.toHex(field.toOpenArray())) + res + of Asn1Tag.Sequence: + res.add(" ") + res.add(ncrutils.toHex(field.toOpenArray())) + res + of Asn1Tag.Context: + res.add(" ") + res.add(ncrutils.toHex(field.toOpenArray())) + res + else: + res.add(" ") + res.add(ncrutils.toHex(field.toOpenArray())) + res proc write*[T: Asn1Buffer|Asn1Composite](abc: var T, tag: Asn1Tag) = ## Write empty value to buffer or composite with ``tag``. @@ -656,7 +788,7 @@ proc write*[T: Asn1Buffer|Asn1Composite](abc: var T, tag: Asn1Tag) = ## This procedure must be used to write `NULL`, `0` or empty `BIT STRING`, ## `OCTET STRING` types. doAssert(tag in {Asn1Tag.Null, Asn1Tag.Integer, Asn1Tag.BitString, - Asn1Tag.OctetString}) + Asn1Tag.OctetString}) var length: int if tag == Asn1Tag.Null: length = asn1EncodeNull(abc.toOpenArray()) diff --git a/tests/testminasn1.nim b/tests/testminasn1.nim new file mode 100644 index 000000000..fe027a7e3 --- /dev/null +++ b/tests/testminasn1.nim @@ -0,0 +1,214 @@ +## Nim-Libp2p +## Copyright (c) 2018 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. +import unittest +import ../libp2p/crypto/minasn1 +import nimcrypto/utils as ncrutils + +when defined(nimHasUsed): {.used.} + +const Asn1EdgeValues = [ + 0'u64, (1'u64 shl 7) - 1'u64, + (1'u64 shl 7), (1'u64 shl 8) - 1'u64, + (1'u64 shl 8), (1'u64 shl 16) - 1'u64, + (1'u64 shl 16), (1'u64 shl 24) - 1'u64, + (1'u64 shl 24), (1'u64 shl 32) - 1'u64, + (1'u64 shl 32), (1'u64 shl 40) - 1'u64, + (1'u64 shl 40), (1'u64 shl 48) - 1'u64, + (1'u64 shl 48), (1'u64 shl 56) - 1'u64, + (1'u64 shl 56), 0xFFFF_FFFF_FFFF_FFFF'u64 +] + +const Asn1EdgeExpects = [ + "00", "7F", + "8180", "81FF", + "820100", "82FFFF", + "83010000", "83FFFFFF", + "8401000000", "84FFFFFFFF", + "850100000000", "85FFFFFFFFFF", + "86010000000000", "86FFFFFFFFFFFF", + "8701000000000000", "87FFFFFFFFFFFFFF", + "880100000000000000", "88FFFFFFFFFFFFFFFF", +] + +const Asn1UIntegerValues8 = [ + 0x00'u8, 0x7F'u8, 0x80'u8, 0xFF'u8, +] + +const Asn1UIntegerExpects8 = [ + "020100", "02017F", "02020080", "020200FF" +] + +const Asn1UIntegerValues16 = [ + 0x00'u16, 0x7F'u16, 0x80'u16, 0xFF'u16, + 0x7FFF'u16, 0x8000'u16, 0xFFFF'u16 +] + +const Asn1UIntegerExpects16 = [ + "020100", "02017F", "02020080", "020200FF", "02027FFF", + "0203008000", "020300FFFF" +] + +const Asn1UIntegerValues32 = [ + 0x00'u32, 0x7F'u32, 0x80'u32, 0xFF'u32, + 0x7FFF'u32, 0x8000'u32, 0xFFFF'u32, + 0x7FFF_FFFF'u32, 0x8000_0000'u32, 0xFFFF_FFFF'u32 +] + +const Asn1UIntegerExpects32 = [ + "020100", "02017F", "02020080", "020200FF", "02027FFF", + "0203008000", "020300FFFF", "02047FFFFFFF", "02050080000000", + "020500FFFFFFFF" +] + +const Asn1UIntegerValues64 = [ + 0x00'u64, 0x7F'u64, 0x80'u64, 0xFF'u64, + 0x7FFF'u64, 0x8000'u64, 0xFFFF'u64, + 0x7FFF_FFFF'u64, 0x8000_0000'u64, 0xFFFF_FFFF'u64, + 0x7FFF_FFFF_FFFF_FFFF'u64, 0x8000_0000_0000_0000'u64, + 0xFFFF_FFFF_FFFF_FFFF'u64 +] + +const Asn1UIntegerExpects64 = [ + "020100", "02017F", "02020080", "020200FF", "02027FFF", + "0203008000", "020300FFFF", "02047FFFFFFF", "02050080000000", + "020500FFFFFFFF", "02087FFFFFFFFFFFFFFF", "0209008000000000000000", + "020900FFFFFFFFFFFFFFFF" +] + +suite "Minimal ASN.1 encode/decode suite": + test "Length encoding edge values": + var empty = newSeq[byte](0) + for i in 0 ..< len(Asn1EdgeValues): + var value = newSeq[byte](9) + let r1 = asn1EncodeLength(empty, Asn1EdgeValues[i]) + let r2 = asn1EncodeLength(value, Asn1EdgeValues[i]) + value.setLen(r2) + check: + r1 == (len(Asn1EdgeExpects[i]) shr 1) + r2 == (len(Asn1EdgeExpects[i]) shr 1) + check: + ncrutils.fromHex(Asn1EdgeExpects[i]) == value + + test "ASN.1 DER INTEGER encoding/decoding of native unsigned values test": + proc decodeBuffer(data: openarray[byte]): uint64 = + var ab = Asn1Buffer.init(data) + let fres = ab.read() + doAssert(fres.isOk() and fres.get().kind == Asn1Tag.Integer) + fres.get().vint + + proc encodeInteger[T](value: T): seq[byte] = + var buffer = newSeq[byte](16) + let res = asn1EncodeInteger(buffer, value) + buffer.setLen(res) + buffer + + for i in 0 ..< len(Asn1UIntegerValues8): + let buffer = encodeInteger(Asn1UIntegerValues8[i]) + check: + toHex(buffer) == Asn1UIntegerExpects8[i] + decodeBuffer(buffer) == uint64(Asn1UIntegerValues8[i]) + + for i in 0 ..< len(Asn1UIntegerValues16): + let buffer = encodeInteger(Asn1UIntegerValues16[i]) + check: + toHex(buffer) == Asn1UIntegerExpects16[i] + decodeBuffer(buffer) == uint64(Asn1UIntegerValues16[i]) + + for i in 0 ..< len(Asn1UIntegerValues32): + let buffer = encodeInteger(Asn1UIntegerValues32[i]) + check: + toHex(buffer) == Asn1UIntegerExpects32[i] + decodeBuffer(buffer) == uint64(Asn1UIntegerValues32[i]) + + for i in 0 ..< len(Asn1UIntegerValues64): + let buffer = encodeInteger(Asn1UIntegerValues64[i]) + check: + toHex(buffer) == Asn1UIntegerExpects64[i] + decodeBuffer(buffer) == uint64(Asn1UIntegerValues64[i]) + + test "ASN.1 DER INTEGER incorrect values decoding test": + proc decodeBuffer(data: string): Asn1Result[Asn1Field] = + var ab = Asn1Buffer.init(fromHex(data)) + ab.read() + + check: + decodeBuffer("0200").error == Asn1Error.Incorrect + decodeBuffer("0201").error == Asn1Error.Incomplete + decodeBuffer("02020000").error == Asn1Error.Incorrect + decodeBuffer("0203000001").error == Asn1Error.Incorrect + + test "ASN.1 DER BITSTRING encoding/decoding with unused bits test": + proc encodeBits(value: string, bitsUsed: int): seq[byte] = + var buffer = newSeq[byte](16) + let res = asn1EncodeBitString(buffer, fromHex(value), bitsUsed) + buffer.setLen(res) + buffer + + proc decodeBuffer(data: string): Asn1Field = + var ab = Asn1Buffer.init(fromHex(data)) + let fres = ab.read() + doAssert(fres.isOk() and fres.get().kind == Asn1Tag.BitString) + fres.get() + + check: + toHex(encodeBits("FF", 7)) == "03020780" + toHex(encodeBits("FF", 6)) == "030206C0" + toHex(encodeBits("FF", 5)) == "030205E0" + toHex(encodeBits("FF", 4)) == "030204F0" + toHex(encodeBits("FF", 3)) == "030203F8" + toHex(encodeBits("FF", 2)) == "030202FC" + toHex(encodeBits("FF", 1)) == "030201FE" + toHex(encodeBits("FF", 0)) == "030200FF" + + let f0 = decodeBuffer("030200FF") + let f0b = @(f0.buffer.toOpenArray(f0.offset, f0.offset + f0.length - 1)) + let f1 = decodeBuffer("030201FE") + let f1b = @(f1.buffer.toOpenArray(f1.offset, f1.offset + f1.length - 1)) + let f2 = decodeBuffer("030202FC") + let f2b = @(f2.buffer.toOpenArray(f2.offset, f2.offset + f2.length - 1)) + let f3 = decodeBuffer("030203F8") + let f3b = @(f3.buffer.toOpenArray(f3.offset, f3.offset + f3.length - 1)) + let f4 = decodeBuffer("030204F0") + let f4b = @(f4.buffer.toOpenArray(f4.offset, f4.offset + f4.length - 1)) + let f5 = decodeBuffer("030205E0") + let f5b = @(f5.buffer.toOpenArray(f5.offset, f5.offset + f5.length - 1)) + let f6 = decodeBuffer("030206C0") + let f6b = @(f6.buffer.toOpenArray(f6.offset, f6.offset + f6.length - 1)) + let f7 = decodeBuffer("03020780") + let f7b = @(f7.buffer.toOpenArray(f7.offset, f7.offset + f7.length - 1)) + + check: + f0.ubits == 0 + toHex(f0b) == "FF" + f1.ubits == 1 + toHex(f1b) == "FE" + f2.ubits == 2 + toHex(f2b) == "FC" + f3.ubits == 3 + toHex(f3b) == "F8" + f4.ubits == 4 + toHex(f4b) == "F0" + f5.ubits == 5 + toHex(f5b) == "E0" + f6.ubits == 6 + toHex(f6b) == "C0" + f7.ubits == 7 + toHex(f7b) == "80" + + test "ASN.1 DER BITSTRING incorrect values decoding test": + proc decodeBuffer(data: string): Asn1Result[Asn1Field] = + var ab = Asn1Buffer.init(fromHex(data)) + ab.read() + + check: + decodeBuffer("0300").error == Asn1Error.Incorrect + decodeBuffer("030180").error == Asn1Error.Incorrect + decodeBuffer("030107").error == Asn1Error.Incorrect + decodeBuffer("030200").error == Asn1Error.Incomplete + decodeBuffer("030208FF").error == Asn1Error.Incorrect diff --git a/tests/testnative.nim b/tests/testnative.nim index 9f75ffec6..2a6668ed4 100644 --- a/tests/testnative.nim +++ b/tests/testnative.nim @@ -2,7 +2,8 @@ import testvarint, testminprotobuf, teststreamseq -import testrsa, +import testminasn1, + testrsa, testecnist, tested25519, testsecp256k1,