Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for parsing BER #42

Merged
merged 1 commit into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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