diff --git a/Sources/SwiftASN1/Basic ASN1 Types/PEMDocument.swift b/Sources/SwiftASN1/Basic ASN1 Types/PEMDocument.swift
index 64ae360..e2b5bc8 100644
--- a/Sources/SwiftASN1/Basic ASN1 Types/PEMDocument.swift
+++ b/Sources/SwiftASN1/Basic ASN1 Types/PEMDocument.swift
@@ -11,16 +11,126 @@
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
+
#if canImport(Foundation)
import Foundation
+/// Defines a type that can be serialized in PEM-encoded form.
+///
+/// Users implementing this type are expected to just provide the ``defaultPEMDiscriminator``
+///
+/// A PEM `String` can be serialized by constructing a ``PEMDocument`` by calling ``PEMSerializable/serializeAsPEM()`` and then accessing the ``PEMDocument/pemString`` preropty.
+public protocol PEMSerializable: DERSerializable {
+ /// The PEM discriminator identifying this object type.
+ ///
+ /// The PEM discriminator is in the first line of a PEM string after `BEGIN` and at the end of the string after `END` e.g.
+ /// ```
+ /// -----BEGIN defaultPEMDiscriminator-----
+ ///
+ /// -----END defaultPEMDiscriminator-----
+ /// ```
+ static var defaultPEMDiscriminator: String { get }
+
+ func serializeAsPEM(discriminator: String) throws -> PEMDocument
+}
+
+/// Defines a type that can be parsed from a PEM-encoded form.
+///
+/// Users implementing this type are expected to just provide the ``defaultPEMDiscriminator``.
+///
+/// Objects that are ``PEMParseable`` can be construct from a PEM `String` through ``PEMParseable/init(pemEncoded:)``.
+public protocol PEMParseable: DERParseable {
+ /// The PEM discriminator identifying this object type.
+ ///
+ /// The PEM discriminator is in the first line of a PEM string after `BEGIN` and at the end of the string after `END` e.g.
+ /// ```
+ /// -----BEGIN defaultPEMDiscriminator-----
+ ///
+ /// -----END defaultPEMDiscriminator-----
+ /// ```
+ static var defaultPEMDiscriminator: String { get }
+
+ init(pemDocument: PEMDocument) throws
+}
+
+/// Defines a type that can be serialized in and parsed from PEM-encoded form.
+///
+/// Users implementing this type are expected to just provide the ``PEMParseable/defaultPEMDiscriminator``.
+///
+/// Objects that are ``PEMRepresentable`` can be construct from a PEM `String` through ``PEMParseable/init(pemEncoded:)``.
+///
+/// A PEM `String` can be serialized by constructing a ``PEMDocument`` by calling ``PEMSerializable/serializeAsPEM()`` and then accessing the ``PEMDocument/pemString`` preropty.
+public typealias PEMRepresentable = PEMSerializable & PEMParseable
+
+extension PEMParseable {
+
+ /// Initialize this object from a serialized PEM representation.
+ ///
+ /// This will check that the discriminator matches ``PEMParseable/defaultPEMDiscriminator``, decode the base64 encoded string and
+ /// then decode the DER encoded bytes using ``DERParseable/init(derEncoded:)-i2rf``.
+ ///
+ /// - parameters:
+ /// - pemEncoded: The PEM-encoded string representing this object.
+ @inlinable
+ public init(pemEncoded pemString: String) throws {
+ try self.init(pemDocument: try PEMDocument(pemString: pemString))
+ }
+
+ /// Initialize this object from a serialized PEM representation.
+ /// This will check that the ``PEMParseable/pemDiscriminator`` matches and
+ /// forward the DER encoded bytes to ``DERParseable/init(derEncoded:)-i2rf``.
+ ///
+ /// - parameters:
+ /// - pemDocument: DER-encoded PEM document
+ @inlinable
+ public init(pemDocument: PEMDocument) throws {
+ guard pemDocument.discriminator == Self.defaultPEMDiscriminator else {
+ throw ASN1Error.invalidPEMDocument(reason: "PEMDocument has incorrect discriminator \(pemDocument.discriminator). Expected \(Self.defaultPEMDiscriminator) instead")
+ }
+
+ try self.init(derEncoded: pemDocument.derBytes)
+ }
+}
+
+extension PEMSerializable {
+ /// Serializes `self` as a PEM document with given `discriminator`.
+ /// - Parameter discriminator: PEM discriminator used in for the BEGIN and END encapsulation boundaries.
+ /// - Returns: DER encoded PEM document
+ @inlinable
+ public func serializeAsPEM(discriminator: String) throws -> PEMDocument {
+ var serializer = DER.Serializer()
+ try serializer.serialize(self)
+
+ return PEMDocument(type: discriminator, derBytes: serializer.serializedBytes)
+ }
+
+ /// Serializes `self` as a PEM document with the ``defaultPEMDiscriminator``.
+ @inlinable
+ public func serializeAsPEM() throws -> PEMDocument {
+ try self.serializeAsPEM(discriminator: Self.defaultPEMDiscriminator)
+ }
+}
+
/// A PEM document is some data, and a discriminator type that is used to advertise the content.
public struct PEMDocument {
- private static let lineLength = 64
-
- public var type: String
+ fileprivate static let lineLength = 64
- public var derBytes: Data
+
+ @available(*, deprecated, renamed: "discriminator")
+ public var type: String {
+ get { discriminator }
+ set { discriminator = newValue }
+ }
+
+ /// The PEM discriminator is in the first line of a PEM string after `BEGIN` and at the end of the string after `END` e.g.
+ /// ```
+ /// -----BEGIN discriminator-----
+ ///
+ /// -----END discriminator-----
+ /// ```
+ public var discriminator: String
+
+ public var derBytes: [UInt8]
public init(pemString: String) throws {
// A PEM document looks like this:
@@ -53,30 +163,38 @@ public struct PEMDocument {
throw ASN1Error.invalidPEMDocument(reason: "PEMDocument not correctly base64 encoded")
}
- self.type = discriminator
- self.derBytes = derBytes
+ self.discriminator = discriminator
+ self.derBytes = Array(derBytes)
}
- public init(type: String, derBytes: Data) {
- self.type = type
+ public init(type: String, derBytes: [UInt8]) {
+ self.discriminator = type
self.derBytes = derBytes
}
+ /// PEM string is a base 64 encoded string of ``derBytes`` enclosed in BEGIN and END encapsulation boundaries with the specified ``discriminator`` type.
+ ///
+ /// Example PEM string:
+ /// ```
+ /// -----BEGIN discriminator-----
+ ///
+ /// -----END discriminator-----
+ /// ```
public var pemString: String {
- var encoded = self.derBytes.base64EncodedString()[...]
- let pemLineCount = (encoded.utf8.count + PEMDocument.lineLength) / PEMDocument.lineLength
+ var encoded = Data(self.derBytes).base64EncodedString()[...]
+ let pemLineCount = (encoded.utf8.count + Self.lineLength) / Self.lineLength
var pemLines = [Substring]()
pemLines.reserveCapacity(pemLineCount + 2)
- pemLines.append("-----BEGIN \(self.type)-----")
+ pemLines.append("-----BEGIN \(self.discriminator)-----")
while encoded.count > 0 {
- let prefixIndex = encoded.index(encoded.startIndex, offsetBy: PEMDocument.lineLength, limitedBy: encoded.endIndex) ?? encoded.endIndex
+ let prefixIndex = encoded.index(encoded.startIndex, offsetBy: Self.lineLength, limitedBy: encoded.endIndex) ?? encoded.endIndex
pemLines.append(encoded[..
+-
### Parsing DER
@@ -83,3 +85,9 @@ These moving pieces combine to provide support for the DER representation of ASN
- ``ASN1IA5String``
- ``ASN1TeletexString``
- ``ASN1UniversalString``
+
+### Parsing and Serializing PEM
+- ``PEMRepresentable``
+- ``PEMParseable``
+- ``PEMSerializable``
+- ``PEMDocument``
diff --git a/Tests/SwiftASN1Tests/ASN1Tests.swift b/Tests/SwiftASN1Tests/ASN1Tests.swift
index 24875a2..376857b 100644
--- a/Tests/SwiftASN1Tests/ASN1Tests.swift
+++ b/Tests/SwiftASN1Tests/ASN1Tests.swift
@@ -414,7 +414,7 @@ class ASN1Tests: XCTestCase {
}
}
- func testStraightforwardPEMParsing() throws {
+ func testStraightforwardPEMDocumentParsing() throws {
let simplePEM = """
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIBHli4jaj+JwWQlU0yhZUu+TdMPVhZ3wR2PS416Sz/K/oAoGCCqGSM49
@@ -423,10 +423,10 @@ O9zxi7HTvuXyQr7QKSBtdCGmHym+WoPsbA==
-----END EC PRIVATE KEY-----
"""
let document = try PEMDocument(pemString: simplePEM)
- XCTAssertEqual(document.type, "EC PRIVATE KEY")
+ XCTAssertEqual(document.discriminator, "EC PRIVATE KEY")
XCTAssertEqual(document.derBytes.count, 121)
- let parsed = try DER.parse(Array(document.derBytes))
+ let parsed = try DER.parse(document.derBytes)
let pkey = try SEC1PrivateKey(derEncoded: parsed)
let reserialized = document.pemString
@@ -434,9 +434,28 @@ O9zxi7HTvuXyQr7QKSBtdCGmHym+WoPsbA==
var serializer = DER.Serializer()
XCTAssertNoThrow(try serializer.serialize(pkey))
- let reserialized2 = PEMDocument(type: "EC PRIVATE KEY", derBytes: Data(serializer.serializedBytes))
+ let reserialized2 = PEMDocument(type: "EC PRIVATE KEY", derBytes: serializer.serializedBytes)
XCTAssertEqual(reserialized2.pemString, simplePEM)
}
+
+ func testStraightforwardPEMParsing() throws {
+ let simplePEM = """
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIBHli4jaj+JwWQlU0yhZUu+TdMPVhZ3wR2PS416Sz/K/oAoGCCqGSM49
+AwEHoUQDQgAEOhvJhbc3zM4SJooCaWdyheY2E6wWkISg7TtxJYgb/S0Zz7WruJzG
+O9zxi7HTvuXyQr7QKSBtdCGmHym+WoPsbA==
+-----END EC PRIVATE KEY-----
+"""
+ let pkey = try SEC1PrivateKey(pemEncoded: simplePEM)
+
+ let reserialized = try pkey.serializeAsPEM().pemString
+ XCTAssertEqual(reserialized, simplePEM)
+
+ var serializer = DER.Serializer()
+ XCTAssertNoThrow(try serializer.serialize(pkey))
+ let reserialized2 = try SEC1PrivateKey(derEncoded: serializer.serializedBytes)
+ XCTAssertEqual(try reserialized2.serializeAsPEM().pemString, simplePEM)
+ }
func testTruncatedPEMDocumentsAreRejected() throws {
// We drip feed the PEM one extra character at a time. It never parses successfully.
@@ -451,9 +470,13 @@ O9zxi7HTvuXyQr7QKSBtdCGmHym+WoPsbA==
XCTAssertThrowsError(try PEMDocument(pemString: String(simplePEM[..