Skip to content

Commit

Permalink
Add support for parsing BER
Browse files Browse the repository at this point in the history
  • Loading branch information
davidzech committed Sep 29, 2023
1 parent 1ffc413 commit 7a66b54
Show file tree
Hide file tree
Showing 17 changed files with 2,232 additions and 1,144 deletions.
1,314 changes: 185 additions & 1,129 deletions Sources/SwiftASN1/ASN1.swift

Large diffs are not rendered by default.

634 changes: 634 additions & 0 deletions Sources/SwiftASN1/BER.swift

Large diffs are not rendered by default.

36 changes: 35 additions & 1 deletion Sources/SwiftASN1/Basic ASN1 Types/ASN1Any.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
///
/// The only things users can do with ASN.1 ANYs is to try to decode them as something else,
/// to create them from something else, or to serialize them.
public struct ASN1Any: DERParseable, DERSerializable, Hashable, Sendable {
public struct ASN1Any: DERParseable, BERParseable, DERSerializable, BERSerializable, Hashable, Sendable {
@usableFromInline
var _serializedBytes: ArraySlice<UInt8>

Expand Down Expand Up @@ -56,6 +56,11 @@ public struct ASN1Any: DERParseable, DERSerializable, Hashable, Sendable {
self._serializedBytes = ArraySlice(serializer._serializedBytes)
}

@inlinable
public init(berEncoded rootNode: ASN1Node) {
self = .init(derEncoded: rootNode)
}

@inlinable
public func serialize(into coder: inout DER.Serializer) throws {
// Dangerous to just reach in there like this, but it's the right way to serialize this.
Expand Down Expand Up @@ -98,3 +103,32 @@ extension DERImplicitlyTaggable {
try self.init(derEncoded: asn1Any._serializedBytes, withIdentifier: identifier)
}
}

extension BERParseable {
/// Construct this node from an ASN.1 ANY object.
///
/// This operation works by asking the type to decode itself from the serialized representation
/// of this ASN.1 ANY node.
///
/// - parameters:
/// berASN1Any: The ASN.1 ANY object to reinterpret.
@inlinable
public init(berASN1Any: ASN1Any) throws {
try self.init(berEncoded: berASN1Any._serializedBytes)
}
}

extension BERImplicitlyTaggable {
/// Construct this node from an ASN.1 ANY object.
///
/// This operation works by asking the type to decode itself from the serialized representation
/// of this ASN.1 ANY node.
///
/// - parameters:
/// berASN1Any: The ASN.1 ANY object to reinterpret.
/// identifier: The tag to use with this node.
@inlinable
public init(berASN1Any: ASN1Any, withIdentifier identifier: ASN1Identifier) throws {
try self.init(berEncoded: berASN1Any._serializedBytes, withIdentifier: identifier)
}
}
19 changes: 18 additions & 1 deletion Sources/SwiftASN1/Basic ASN1 Types/ASN1BitString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
///
/// In the case of a bitset, DER has additional requirements as to how to represent the object. This type does not
/// enforce those additional rules: users are expected to implement that validation themselves.
public struct ASN1BitString: DERImplicitlyTaggable {
public struct ASN1BitString: DERImplicitlyTaggable, BERImplicitlyTaggable {

/// The default identifier for this type.
///
/// Evaluates to ``ASN1Identifier/bitString``.
Expand Down Expand Up @@ -72,6 +73,22 @@ public struct ASN1BitString: DERImplicitlyTaggable {
try self._validate()
}

public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
guard node.identifier == identifier else {
throw ASN1Error.unexpectedFieldType(node.identifier)
}

switch node.content {
case .constructed(_):
// BER allows constructed ASN1 BitStrings, that is, you can construct a BitString that is represented by a composition of many individual Primitive (non-constructed) BitStrings
throw ASN1Error.invalidASN1Object(reason: "Constructed encoding of ASN1BitString not yet supported")

case .primitive(_):
self = try Self(derEncoded: node, withIdentifier: identifier)
}

}

/// Construct an ``ASN1BitString`` from raw components.
///
/// - parameters:
Expand Down
22 changes: 21 additions & 1 deletion Sources/SwiftASN1/Basic ASN1 Types/ASN1Boolean.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

extension Bool: DERImplicitlyTaggable {
extension Bool: DERImplicitlyTaggable, BERImplicitlyTaggable {
@inlinable
public static var defaultIdentifier: ASN1Identifier {
.boolean
Expand Down Expand Up @@ -41,6 +41,26 @@ extension Bool: DERImplicitlyTaggable {
}
}

@inlinable
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
guard node.identifier == identifier else {
throw ASN1Error.unexpectedFieldType(node.identifier)
}

guard case .primitive(let bytes) = node.content, bytes.count == 1 else {
throw ASN1Error.invalidASN1Object(reason: "Invalid content for ASN1Bool")
}

switch bytes[bytes.startIndex] {
case 0:
// Boolean false
self = false
default:
// Boolean true in BER
self = true
}
}

@inlinable
public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
coder.appendPrimitiveNode(identifier: identifier) { bytes in
Expand Down
39 changes: 38 additions & 1 deletion Sources/SwiftASN1/Basic ASN1 Types/ASN1Integer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/// UInt64 or Int64. While both of those types conform by default, users can conform their preferred
/// arbitrary-width integer type as well, or use `ArraySlice<UInt8>` to store the raw bytes of the
/// integer directly.
public protocol ASN1IntegerRepresentable: DERImplicitlyTaggable {
public protocol ASN1IntegerRepresentable: DERImplicitlyTaggable, BERImplicitlyTaggable {
associatedtype IntegerBytes: RandomAccessCollection where IntegerBytes.Element == UInt8

/// Whether this type can represent signed integers.
Expand All @@ -32,6 +32,10 @@ public protocol ASN1IntegerRepresentable: DERImplicitlyTaggable {
/// according to DER requirements.
init(derIntegerBytes: ArraySlice<UInt8>) throws

/// Construct the integer value form the integer bytes. These will be big-endian, and encoded
/// accroding to BER requirements.
init(berIntegerBytes: ArraySlice<UInt8>) throws

/// Provide the big-endian bytes corresponding to this integer.
func withBigEndianIntegerBytes<ReturnType>(_ body: (IntegerBytes) throws -> ReturnType) rethrows -> ReturnType
}
Expand Down Expand Up @@ -82,6 +86,34 @@ extension ASN1IntegerRepresentable {
self = try Self(derIntegerBytes: dataBytes)
}

@inlinable
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
guard node.identifier == identifier else {
throw ASN1Error.unexpectedFieldType(node.identifier)
}

guard case .primitive(var dataBytes) = node.content else {
preconditionFailure("ASN.1 parser generated primitive node with constructed content")
}

// Zero bytes of integer is not an acceptable encoding.
guard dataBytes.count > 0 else {
throw ASN1Error.invalidASN1IntegerEncoding(reason: "INTEGER encoded with zero bytes")
}

// If the type we're trying to decode is unsigned, and the top byte is zero, we should strip it.
// If the top bit is set, however, this is an invalid conversion: the number needs to be positive!
if !Self.isSigned, let first = dataBytes.first {
if first == 0x00 {
dataBytes = dataBytes.dropFirst()
} else if first & 0x80 == 0x80 {
throw ASN1Error.invalidASN1IntegerEncoding(reason: "INTEGER encoded with top bit set!")
}
}

self = try Self(berIntegerBytes: dataBytes)
}

@inlinable
public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
coder.appendPrimitiveNode(identifier: identifier) { bytes in
Expand Down Expand Up @@ -122,6 +154,11 @@ extension ASN1IntegerRepresentable where Self: FixedWidthInteger {
}
}

@inlinable
public init(berIntegerBytes bytes: ArraySlice<UInt8>) throws {
self = try .init(derIntegerBytes: bytes)
}

@inlinable
public func withBigEndianIntegerBytes<ReturnType>(
_ body: (IntegerBytesCollection<Self>) throws -> ReturnType
Expand Down
7 changes: 6 additions & 1 deletion Sources/SwiftASN1/Basic ASN1 Types/ASN1Null.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===//

/// An ASN1 NULL represents nothing.
public struct ASN1Null: DERImplicitlyTaggable, Hashable, Sendable {
public struct ASN1Null: DERImplicitlyTaggable, BERImplicitlyTaggable, Hashable, Sendable {
@inlinable
public static var defaultIdentifier: ASN1Identifier {
.null
Expand All @@ -34,6 +34,11 @@ public struct ASN1Null: DERImplicitlyTaggable, Hashable, Sendable {
}
}

@inlinable
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
self = try .init(derEncoded: node, withIdentifier: identifier)
}

@inlinable
public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) {
coder.appendPrimitiveNode(identifier: identifier, { _ in })
Expand Down
50 changes: 49 additions & 1 deletion Sources/SwiftASN1/Basic ASN1 Types/ASN1OctetString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
//===----------------------------------------------------------------------===//

/// An OCTET STRING is a representation of a string of octets.
public struct ASN1OctetString: DERImplicitlyTaggable {
public struct ASN1OctetString: DERImplicitlyTaggable, BERImplicitlyTaggable {

@inlinable
public static var defaultIdentifier: ASN1Identifier {
.octetString
Expand All @@ -35,6 +36,53 @@ public struct ASN1OctetString: DERImplicitlyTaggable {
self.bytes = content
}

@inlinable
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
guard node.identifier == identifier else {
throw ASN1Error.unexpectedFieldType(node.identifier)
}

switch node.content {
case .constructed(let nodes):
// BER allows constructed ASN1 BitStrings, that is, you can construct a BitString that is represented by a composition of many individual recursively encoded (primitive or constructed) BitStrings

// We have to allocate here, since we need to flatten all of the sub octet-strings into a contiguous view
// Maybe it's possible in the future something like [chain](https://github.com/apple/swift-algorithms/blob/main/Guides/Chain.md)
// could be used to eliminate allocations, but we need an ArraySlice
let (count, maxLength) = nodes.reduce((0, 0)) { acc, elem in
let (countAcc, lenAcc) = acc
return (countAcc + 1, lenAcc + elem.encodedBytes.count)
}

if count == 0 {
self.bytes = []
return
}

if count == 1 {
// this recursive call might allocate if the inner string is also constructed, which means the recursive portions have returned a flattened view.
for node in nodes {
let substring = try ASN1OctetString(berEncoded: node)
self.bytes = substring.bytes
return
}
}

var flattened: [UInt8] = []
// we are going to reserve capacity a bit over what reality will be, since we are hinting the allocation based on the entire encoded bytes, which includes tags and sizes
flattened.reserveCapacity(maxLength)
for node in nodes {
let substring = try ASN1OctetString(berEncoded: node)
flattened += substring.bytes
}

self.bytes = flattened[...]

case .primitive(let content):
self.bytes = content
}
}

/// Construct an OCTET STRING from a sequence of bytes.
///
/// - parameters:
Expand Down
Loading

0 comments on commit 7a66b54

Please sign in to comment.