Browse Source

ASN1 Parsing Support

Brandon Toms 3 years ago
parent
commit
59caf28915

+ 95 - 0
Sources/CryptoSwift/ASN1/ASN1.swift

@@ -0,0 +1,95 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+//  ASN1 Code inspired by Asn1Parser.swift from SwiftyRSA
+
+import Foundation
+
+enum ASN1 {
+  internal enum IDENTIFIERS: UInt8, Equatable {
+    case SEQUENCE = 0x30
+    case INTERGER = 0x02
+    case OBJECTID = 0x06
+    case NULL = 0x05
+    case BITSTRING = 0x03
+    case OCTETSTRING = 0x04
+    case EC_OBJECT = 0xA0
+    case EC_BITS = 0xA1
+
+    static func == (lhs: UInt8, rhs: IDENTIFIERS) -> Bool {
+      lhs == rhs.rawValue
+    }
+
+    var bytes: [UInt8] {
+      switch self {
+        case .NULL:
+          return [self.rawValue, 0x00]
+        default:
+          return [self.rawValue]
+      }
+    }
+  }
+
+  /// An ASN1 node
+  internal enum Node: CustomStringConvertible {
+    /// An array of more `ASN1.Node`s
+    case sequence(nodes: [Node])
+    /// An integer
+    case integer(data: Data)
+    /// An objectIdentifier
+    case objectIdentifier(data: Data)
+    /// A null object
+    case null
+    /// A bitString
+    case bitString(data: Data)
+    /// An octetString
+    case octetString(data: Data)
+
+    //Exteneded Params
+
+    /// Elliptic Curve specific objectIdentifier
+    case ecObject(data: Data)
+    /// Elliptic Curve specific bitString
+    case ecBits(data: Data)
+
+    var description: String {
+      ASN1.printNode(self, level: 0)
+    }
+  }
+
+  internal static func printNode(_ node: ASN1.Node, level: Int) -> String {
+    var str: [String] = []
+    let prefix = String(repeating: "\t", count: level)
+    switch node {
+      case .integer(let int):
+        str.append("\(prefix)Integer: \(int.toHexString())")
+      case .bitString(let bs):
+        str.append("\(prefix)BitString: \(bs.toHexString())")
+      case .null:
+        str.append("\(prefix)NULL")
+      case .objectIdentifier(let oid):
+        str.append("\(prefix)ObjectID: \(oid.toHexString())")
+      case .octetString(let os):
+        str.append("\(prefix)OctetString: \(os.toHexString())")
+      case .ecObject(let ecObj):
+        str.append("\(prefix)EC Object: \(ecObj.toHexString())")
+      case .ecBits(let ecBits):
+        str.append("\(prefix)EC Bits: \(ecBits.toHexString())")
+      case .sequence(let nodes):
+        str.append("\(prefix)Sequence:")
+        nodes.forEach { str.append(printNode($0, level: level + 1)) }
+    }
+    return str.joined(separator: "\n")
+  }
+}

+ 122 - 0
Sources/CryptoSwift/ASN1/ASN1Decoder.swift

@@ -0,0 +1,122 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+//  ASN1 Code inspired by Asn1Parser.swift from SwiftyRSA
+
+import Foundation
+
+extension ASN1 {
+  /// A simple ASN1 parser that will recursively iterate over a root node and return a Node tree.
+  /// The root node can be any of the supported nodes described in `Node`. If the parser encounters a sequence
+  /// it will recursively parse its children.
+  enum Decoder {
+
+    enum DecodingError: Error {
+      case noType
+      case invalidType(value: UInt8)
+    }
+
+    /// Parses ASN1 data and returns its root node.
+    ///
+    /// - Parameter data: ASN1 data to parse
+    /// - Returns: Root ASN1 Node
+    /// - Throws: A DecodingError if anything goes wrong, or if an unknown node was encountered
+    static func decode(data: Data) throws -> Node {
+      let scanner = ASN1.Scanner(data: data)
+      let node = try decodeNode(scanner: scanner)
+      return node
+    }
+
+    /// Parses an ASN1 given an existing scanner.
+    /// @warning: this will modify the state (ie: position) of the provided scanner.
+    ///
+    /// - Parameter scanner: Scanner to use to consume the data
+    /// - Returns: Parsed node
+    /// - Throws: A DecodingError if anything goes wrong, or if an unknown node was encountered
+    private static func decodeNode(scanner: ASN1.Scanner) throws -> Node {
+
+      let firstByte = try scanner.consume(length: 1).firstByte
+
+      switch firstByte {
+      case IDENTIFIERS.SEQUENCE.rawValue:
+        let length = try scanner.consumeLength()
+        let data = try scanner.consume(length: length)
+        let nodes = try decodeSequence(data: data)
+        return .sequence(nodes: nodes)
+        
+      case IDENTIFIERS.INTERGER.rawValue:
+        let length = try scanner.consumeLength()
+        let data = try scanner.consume(length: length)
+        return .integer(data: data)
+        
+      case IDENTIFIERS.OBJECTID.rawValue:
+        let length = try scanner.consumeLength()
+        let data = try scanner.consume(length: length)
+        return .objectIdentifier(data: data)
+        
+      case IDENTIFIERS.NULL.rawValue:
+        _ = try scanner.consume(length: 1)
+        return .null
+        
+      case IDENTIFIERS.BITSTRING.rawValue:
+        let length = try scanner.consumeLength()
+
+        // There's an extra byte (0x00) after the bit string length in all the keys I've encountered.
+        // I couldn't find a specification that referenced this extra byte, but let's consume it and discard it.
+        _ = try scanner.consume(length: 1)
+
+        let data = try scanner.consume(length: length - 1)
+        return .bitString(data: data)
+      
+      case IDENTIFIERS.OCTETSTRING.rawValue:
+        let length = try scanner.consumeLength()
+        let data = try scanner.consume(length: length)
+        return .octetString(data: data)
+        
+      case IDENTIFIERS.EC_OBJECT.rawValue:
+        let length = try scanner.consumeLength()
+        let data = try scanner.consume(length: length)
+        return .objectIdentifier(data: data)
+        
+      case IDENTIFIERS.EC_BITS.rawValue:
+        let length = try scanner.consumeLength()
+
+        // There's an extra byte (0x00) after the bit string length in all the keys I've encountered.
+        // I couldn't find a specification that referenced this extra byte, but let's consume it and discard it.
+        _ = try scanner.consume(length: 1)
+
+        let data = try scanner.consume(length: length - 1)
+        return .ecBits(data: data)
+        
+      default:
+        throw DecodingError.invalidType(value: firstByte)
+      }
+    }
+
+    /// Parses an ASN1 sequence and returns its child nodes
+    ///
+    /// - Parameter data: ASN1 data
+    /// - Returns: A list of ASN1 nodes
+    /// - Throws: A DecodingError if anything goes wrong, or if an unknown node was encountered
+    private static func decodeSequence(data: Data) throws -> [Node] {
+      let scanner = ASN1.Scanner(data: data)
+      var nodes: [Node] = []
+      while !scanner.isComplete {
+        let node = try decodeNode(scanner: scanner)
+        nodes.append(node)
+      }
+      return nodes
+    }
+  }
+}

+ 64 - 0
Sources/CryptoSwift/ASN1/ASN1Encoder.swift

@@ -0,0 +1,64 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+//  ASN1 Code inspired by Asn1Parser.swift from SwiftyRSA
+
+import Foundation
+
+extension ASN1 {
+  enum Encoder {
+    /// Encodes an ASN1Node into it's byte representation
+    ///
+    /// - Parameter node: The Node to encode
+    /// - Returns: The encoded bytes as a UInt8 array
+    public static func encode(_ node: ASN1.Node) -> [UInt8] {
+      switch node {
+        case .integer(let integer):
+          return IDENTIFIERS.INTERGER.bytes + self.asn1LengthPrefixed(integer.bytes)
+        case .bitString(let bits):
+          return IDENTIFIERS.BITSTRING.bytes + self.asn1LengthPrefixed([0x00] + bits.bytes)
+        case .octetString(let octet):
+          return IDENTIFIERS.OCTETSTRING.bytes + self.asn1LengthPrefixed(octet.bytes)
+        case .null:
+          return IDENTIFIERS.NULL.bytes
+        case .objectIdentifier(let oid):
+          return IDENTIFIERS.OBJECTID.bytes + self.asn1LengthPrefixed(oid.bytes)
+        case .ecObject(let ecObj):
+          return IDENTIFIERS.EC_OBJECT.bytes + self.asn1LengthPrefixed(ecObj.bytes)
+        case .ecBits(let ecBits):
+          return IDENTIFIERS.EC_BITS.bytes + self.asn1LengthPrefixed(ecBits.bytes)
+        case .sequence(let nodes):
+          return IDENTIFIERS.SEQUENCE.bytes + self.asn1LengthPrefixed( nodes.reduce(into: Array<UInt8>(), { partialResult, node in
+            partialResult += encode(node)
+          }))
+      }
+    }
+
+    /// Calculates and returns the ASN.1 length Prefix for a chunk of data
+    private static func asn1LengthPrefix(_ bytes: [UInt8]) -> [UInt8] {
+      if bytes.count >= 0x80 {
+        var lengthAsBytes = withUnsafeBytes(of: bytes.count.bigEndian, Array<UInt8>.init)
+        while lengthAsBytes.first == 0 { lengthAsBytes.removeFirst() }
+        return [0x80 + UInt8(lengthAsBytes.count)] + lengthAsBytes
+      } else {
+        return [UInt8(bytes.count)]
+      }
+    }
+
+    /// Returns the provided bytes with the appropriate ASN.1 length prefix prepended
+    private static func asn1LengthPrefixed(_ bytes: [UInt8]) -> [UInt8] {
+      self.asn1LengthPrefix(bytes) + bytes
+    }
+  }
+}

+ 123 - 0
Sources/CryptoSwift/ASN1/ASN1Scanner.swift

@@ -0,0 +1,123 @@
+//
+//  CryptoSwift
+//
+//  Copyright (C) Marcin Krzyżanowski <marcin@krzyzanowskim.com>
+//  This software is provided 'as-is', without any express or implied warranty.
+//
+//  In no event will the authors be held liable for any damages arising from the use of this software.
+//
+//  Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+//
+//  - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+//  - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+//  - This notice may not be removed or altered from any source or binary distribution.
+//
+//  ASN1 Scanner code is from Asn1Parser.swift from SwiftyRSA
+
+import Foundation
+
+extension ASN1 {
+  /// Simple data scanner that consumes bytes from a raw data and keeps an updated position.
+  internal class Scanner {
+
+    enum ScannerError: Error {
+      case outOfBounds
+    }
+
+    let data: Data
+    var index: Int = 0
+
+    /// Returns whether there is no more data to consume
+    var isComplete: Bool {
+      return self.index >= self.data.count
+    }
+
+    /// Creates a scanner with provided data
+    ///
+    /// - Parameter data: Data to consume
+    init(data: Data) {
+      self.data = data
+    }
+
+    /// Consumes data of provided length and returns it
+    ///
+    /// - Parameter length: length of the data to consume
+    /// - Returns: data consumed
+    /// - Throws: ScannerError.outOfBounds error if asked to consume too many bytes
+    func consume(length: Int) throws -> Data {
+
+      guard length > 0 else {
+        return Data()
+      }
+
+      guard self.index + length <= self.data.count else {
+        throw ScannerError.outOfBounds
+      }
+
+      let subdata = self.data.subdata(in: self.index..<self.index + length)
+      self.index += length
+      return subdata
+    }
+
+    /// Consumes a primitive, definite ASN1 length and returns its value.
+    ///
+    /// See http://luca.ntop.org/Teaching/Appunti/asn1.html,
+    ///
+    /// - Short form. One octet. Bit 8 has value "0" and bits 7-1 give the length.
+    /// - Long form. Two to 127 octets. Bit 8 of first octet has value "1" and
+    ///   bits 7-1 give the number of additional length octets.
+    ///   Second and following octets give the length, base 256, most significant digit first.
+    ///
+    /// - Returns: Length that was consumed
+    /// - Throws: ScannerError.outOfBounds error if asked to consume too many bytes
+    func consumeLength() throws -> Int {
+
+      let lengthByte = try consume(length: 1).firstByte
+
+      // If the first byte's value is less than 0x80, it directly contains the length
+      // so we can return it
+      guard lengthByte >= 0x80 else {
+        return Int(lengthByte)
+      }
+
+      // If the first byte's value is more than 0x80, it indicates how many following bytes
+      // will describe the length. For instance, 0x85 indicates that 0x85 - 0x80 = 0x05 = 5
+      // bytes will describe the length, so we need to read the 5 next bytes and get their integer
+      // value to determine the length.
+      let nextByteCount = lengthByte - 0x80
+      let length = try consume(length: Int(nextByteCount))
+
+      return length.integer
+    }
+  }
+}
+
+internal extension Data {
+
+  /// Returns the first byte of the current data
+  var firstByte: UInt8 {
+    var byte: UInt8 = 0
+    copyBytes(to: &byte, count: MemoryLayout<UInt8>.size)
+    return byte
+  }
+
+  /// Returns the integer value of the current data.
+  /// - Warning: This only supports data up to 4 bytes, as we can only extract 32-bit integers.
+  var integer: Int {
+
+    guard count > 0 else {
+      return 0
+    }
+
+    var int: UInt32 = 0
+    var offset: Int32 = Int32(count - 1)
+    forEach { byte in
+      let byte32 = UInt32(byte)
+      let shifted = byte32 << (UInt32(offset) * 8)
+      int = int | shifted
+      offset -= 1
+    }
+
+    return Int(int)
+  }
+}