Skip to content

Commit 28d0e07

Browse files
authored
Add support for parsing BER (#42)
1 parent 1ffc413 commit 28d0e07

17 files changed

+2232
-1144
lines changed

Sources/SwiftASN1/ASN1.swift

Lines changed: 185 additions & 1129 deletions
Large diffs are not rendered by default.

Sources/SwiftASN1/BER.swift

Lines changed: 634 additions & 0 deletions
Large diffs are not rendered by default.

Sources/SwiftASN1/Basic ASN1 Types/ASN1Any.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
///
2020
/// The only things users can do with ASN.1 ANYs is to try to decode them as something else,
2121
/// to create them from something else, or to serialize them.
22-
public struct ASN1Any: DERParseable, DERSerializable, Hashable, Sendable {
22+
public struct ASN1Any: DERParseable, BERParseable, DERSerializable, BERSerializable, Hashable, Sendable {
2323
@usableFromInline
2424
var _serializedBytes: ArraySlice<UInt8>
2525

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

59+
@inlinable
60+
public init(berEncoded rootNode: ASN1Node) {
61+
self = .init(derEncoded: rootNode)
62+
}
63+
5964
@inlinable
6065
public func serialize(into coder: inout DER.Serializer) throws {
6166
// Dangerous to just reach in there like this, but it's the right way to serialize this.
@@ -98,3 +103,32 @@ extension DERImplicitlyTaggable {
98103
try self.init(derEncoded: asn1Any._serializedBytes, withIdentifier: identifier)
99104
}
100105
}
106+
107+
extension BERParseable {
108+
/// Construct this node from an ASN.1 ANY object.
109+
///
110+
/// This operation works by asking the type to decode itself from the serialized representation
111+
/// of this ASN.1 ANY node.
112+
///
113+
/// - parameters:
114+
/// berASN1Any: The ASN.1 ANY object to reinterpret.
115+
@inlinable
116+
public init(berASN1Any: ASN1Any) throws {
117+
try self.init(berEncoded: berASN1Any._serializedBytes)
118+
}
119+
}
120+
121+
extension BERImplicitlyTaggable {
122+
/// Construct this node from an ASN.1 ANY object.
123+
///
124+
/// This operation works by asking the type to decode itself from the serialized representation
125+
/// of this ASN.1 ANY node.
126+
///
127+
/// - parameters:
128+
/// berASN1Any: The ASN.1 ANY object to reinterpret.
129+
/// identifier: The tag to use with this node.
130+
@inlinable
131+
public init(berASN1Any: ASN1Any, withIdentifier identifier: ASN1Identifier) throws {
132+
try self.init(berEncoded: berASN1Any._serializedBytes, withIdentifier: identifier)
133+
}
134+
}

Sources/SwiftASN1/Basic ASN1 Types/ASN1BitString.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
///
2020
/// In the case of a bitset, DER has additional requirements as to how to represent the object. This type does not
2121
/// enforce those additional rules: users are expected to implement that validation themselves.
22-
public struct ASN1BitString: DERImplicitlyTaggable {
22+
public struct ASN1BitString: DERImplicitlyTaggable, BERImplicitlyTaggable {
23+
2324
/// The default identifier for this type.
2425
///
2526
/// Evaluates to ``ASN1Identifier/bitString``.
@@ -72,6 +73,22 @@ public struct ASN1BitString: DERImplicitlyTaggable {
7273
try self._validate()
7374
}
7475

76+
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
77+
guard node.identifier == identifier else {
78+
throw ASN1Error.unexpectedFieldType(node.identifier)
79+
}
80+
81+
switch node.content {
82+
case .constructed(_):
83+
// 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
84+
throw ASN1Error.invalidASN1Object(reason: "Constructed encoding of ASN1BitString not yet supported")
85+
86+
case .primitive(_):
87+
self = try Self(derEncoded: node, withIdentifier: identifier)
88+
}
89+
90+
}
91+
7592
/// Construct an ``ASN1BitString`` from raw components.
7693
///
7794
/// - parameters:

Sources/SwiftASN1/Basic ASN1 Types/ASN1Boolean.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
extension Bool: DERImplicitlyTaggable {
15+
extension Bool: DERImplicitlyTaggable, BERImplicitlyTaggable {
1616
@inlinable
1717
public static var defaultIdentifier: ASN1Identifier {
1818
.boolean
@@ -41,6 +41,26 @@ extension Bool: DERImplicitlyTaggable {
4141
}
4242
}
4343

44+
@inlinable
45+
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
46+
guard node.identifier == identifier else {
47+
throw ASN1Error.unexpectedFieldType(node.identifier)
48+
}
49+
50+
guard case .primitive(let bytes) = node.content, bytes.count == 1 else {
51+
throw ASN1Error.invalidASN1Object(reason: "Invalid content for ASN1Bool")
52+
}
53+
54+
switch bytes[bytes.startIndex] {
55+
case 0:
56+
// Boolean false
57+
self = false
58+
default:
59+
// Boolean true in BER
60+
self = true
61+
}
62+
}
63+
4464
@inlinable
4565
public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
4666
coder.appendPrimitiveNode(identifier: identifier) { bytes in

Sources/SwiftASN1/Basic ASN1 Types/ASN1Integer.swift

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
/// UInt64 or Int64. While both of those types conform by default, users can conform their preferred
2020
/// arbitrary-width integer type as well, or use `ArraySlice<UInt8>` to store the raw bytes of the
2121
/// integer directly.
22-
public protocol ASN1IntegerRepresentable: DERImplicitlyTaggable {
22+
public protocol ASN1IntegerRepresentable: DERImplicitlyTaggable, BERImplicitlyTaggable {
2323
associatedtype IntegerBytes: RandomAccessCollection where IntegerBytes.Element == UInt8
2424

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

35+
/// Construct the integer value form the integer bytes. These will be big-endian, and encoded
36+
/// accroding to BER requirements.
37+
init(berIntegerBytes: ArraySlice<UInt8>) throws
38+
3539
/// Provide the big-endian bytes corresponding to this integer.
3640
func withBigEndianIntegerBytes<ReturnType>(_ body: (IntegerBytes) throws -> ReturnType) rethrows -> ReturnType
3741
}
@@ -82,6 +86,34 @@ extension ASN1IntegerRepresentable {
8286
self = try Self(derIntegerBytes: dataBytes)
8387
}
8488

89+
@inlinable
90+
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
91+
guard node.identifier == identifier else {
92+
throw ASN1Error.unexpectedFieldType(node.identifier)
93+
}
94+
95+
guard case .primitive(var dataBytes) = node.content else {
96+
preconditionFailure("ASN.1 parser generated primitive node with constructed content")
97+
}
98+
99+
// Zero bytes of integer is not an acceptable encoding.
100+
guard dataBytes.count > 0 else {
101+
throw ASN1Error.invalidASN1IntegerEncoding(reason: "INTEGER encoded with zero bytes")
102+
}
103+
104+
// If the type we're trying to decode is unsigned, and the top byte is zero, we should strip it.
105+
// If the top bit is set, however, this is an invalid conversion: the number needs to be positive!
106+
if !Self.isSigned, let first = dataBytes.first {
107+
if first == 0x00 {
108+
dataBytes = dataBytes.dropFirst()
109+
} else if first & 0x80 == 0x80 {
110+
throw ASN1Error.invalidASN1IntegerEncoding(reason: "INTEGER encoded with top bit set!")
111+
}
112+
}
113+
114+
self = try Self(berIntegerBytes: dataBytes)
115+
}
116+
85117
@inlinable
86118
public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) throws {
87119
coder.appendPrimitiveNode(identifier: identifier) { bytes in
@@ -122,6 +154,11 @@ extension ASN1IntegerRepresentable where Self: FixedWidthInteger {
122154
}
123155
}
124156

157+
@inlinable
158+
public init(berIntegerBytes bytes: ArraySlice<UInt8>) throws {
159+
self = try .init(derIntegerBytes: bytes)
160+
}
161+
125162
@inlinable
126163
public func withBigEndianIntegerBytes<ReturnType>(
127164
_ body: (IntegerBytesCollection<Self>) throws -> ReturnType

Sources/SwiftASN1/Basic ASN1 Types/ASN1Null.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
/// An ASN1 NULL represents nothing.
16-
public struct ASN1Null: DERImplicitlyTaggable, Hashable, Sendable {
16+
public struct ASN1Null: DERImplicitlyTaggable, BERImplicitlyTaggable, Hashable, Sendable {
1717
@inlinable
1818
public static var defaultIdentifier: ASN1Identifier {
1919
.null
@@ -34,6 +34,11 @@ public struct ASN1Null: DERImplicitlyTaggable, Hashable, Sendable {
3434
}
3535
}
3636

37+
@inlinable
38+
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
39+
self = try .init(derEncoded: node, withIdentifier: identifier)
40+
}
41+
3742
@inlinable
3843
public func serialize(into coder: inout DER.Serializer, withIdentifier identifier: ASN1Identifier) {
3944
coder.appendPrimitiveNode(identifier: identifier, { _ in })

Sources/SwiftASN1/Basic ASN1 Types/ASN1OctetString.swift

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
/// An OCTET STRING is a representation of a string of octets.
16-
public struct ASN1OctetString: DERImplicitlyTaggable {
16+
public struct ASN1OctetString: DERImplicitlyTaggable, BERImplicitlyTaggable {
17+
1718
@inlinable
1819
public static var defaultIdentifier: ASN1Identifier {
1920
.octetString
@@ -35,6 +36,53 @@ public struct ASN1OctetString: DERImplicitlyTaggable {
3536
self.bytes = content
3637
}
3738

39+
@inlinable
40+
public init(berEncoded node: ASN1Node, withIdentifier identifier: ASN1Identifier) throws {
41+
guard node.identifier == identifier else {
42+
throw ASN1Error.unexpectedFieldType(node.identifier)
43+
}
44+
45+
switch node.content {
46+
case .constructed(let nodes):
47+
// 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
48+
49+
// We have to allocate here, since we need to flatten all of the sub octet-strings into a contiguous view
50+
// Maybe it's possible in the future something like [chain](https://github.com/apple/swift-algorithms/blob/main/Guides/Chain.md)
51+
// could be used to eliminate allocations, but we need an ArraySlice
52+
let (count, maxLength) = nodes.reduce((0, 0)) { acc, elem in
53+
let (countAcc, lenAcc) = acc
54+
return (countAcc + 1, lenAcc + elem.encodedBytes.count)
55+
}
56+
57+
if count == 0 {
58+
self.bytes = []
59+
return
60+
}
61+
62+
if count == 1 {
63+
// this recursive call might allocate if the inner string is also constructed, which means the recursive portions have returned a flattened view.
64+
for node in nodes {
65+
let substring = try ASN1OctetString(berEncoded: node)
66+
self.bytes = substring.bytes
67+
return
68+
}
69+
}
70+
71+
var flattened: [UInt8] = []
72+
// 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
73+
flattened.reserveCapacity(maxLength)
74+
for node in nodes {
75+
let substring = try ASN1OctetString(berEncoded: node)
76+
flattened += substring.bytes
77+
}
78+
79+
self.bytes = flattened[...]
80+
81+
case .primitive(let content):
82+
self.bytes = content
83+
}
84+
}
85+
3886
/// Construct an OCTET STRING from a sequence of bytes.
3987
///
4088
/// - parameters:

0 commit comments

Comments
 (0)