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[..