1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftASN1 open source project
//
// Copyright (c) 2019-2020 Apple Inc. and the SwiftASN1 project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftASN1 project authors
//
// 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-----
/// <base 64 DER representation of this object>
/// -----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-----
/// <base 64 DER representation of this object>
/// -----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: Hashable, Sendable {
fileprivate static let lineLength = 64
/// 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-----
/// <base 64 encoded derBytes>
/// -----END discriminator-----
/// ```
public var discriminator: String
public var derBytes: [UInt8]
public init(pemString: String) throws {
var pemString = pemString.utf8[...]
guard let document = try pemString.readNextPEMDocument()?.decode() else {
throw ASN1Error.invalidPEMDocument(reason: "could not find PEM start marker")
}
guard try pemString.readNextPEMDocument() == nil else {
throw ASN1Error.invalidPEMDocument(reason: "Multiple PEMDocuments found")
}
self = document
}
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-----
/// <base 64 encoded derBytes>
/// -----END discriminator-----
/// ```
public var pemString: String {
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.discriminator)-----")
while encoded.count > 0 {
let prefixIndex =
encoded.index(encoded.startIndex, offsetBy: Self.lineLength, limitedBy: encoded.endIndex)
?? encoded.endIndex
pemLines.append(encoded[..<prefixIndex])
encoded = encoded[prefixIndex...]
}
pemLines.append("-----END \(self.discriminator)-----")
return pemLines.joined(separator: "\n")
}
}
extension PEMDocument {
/// Attempts to parse and decode multiple PEM documents inside a single String.
/// - Parameter pemString: The PEM-encoded string containing zero or more ``PEMDocument``s
/// - Returns: parsed and decoded PEM documents
public static func parseMultiple(pemString: String) throws -> [PEMDocument] {
var pemString = pemString.utf8[...]
var pemDocuments = [PEMDocument]()
while let lazyPEMDocument = try pemString.readNextPEMDocument() {
pemDocuments.append(try lazyPEMDocument.decode())
}
return pemDocuments
}
}
/// A PEM document that has not yet decoded the base64 string found between the start and end marker.
struct LazyPEMDocument {
/// `discriminator` found after BEGIN and END markers
var discriminator: Substring.UTF8View
/// The `base64EncodedDERString` is as found in the original string and will still contain new lines if present in the original.
var base64EncodedDERString: Substring.UTF8View
func decode() throws -> PEMDocument {
guard let type = String(discriminator) else {
throw ASN1Error.invalidPEMDocument(reason: "discriminator is not valid UTF-8")
}
guard let base64EncodedDERString = String(base64EncodedDERString) else {
throw ASN1Error.invalidPEMDocument(reason: "base64EncodedDERString is not valid UTF-8")
}
guard let data = Data(base64Encoded: base64EncodedDERString, options: .ignoreUnknownCharacters) else {
throw ASN1Error.invalidPEMDocument(reason: "PEMDocument not correctly base64 encoded")
}
if data.isEmpty {
throw ASN1Error.invalidPEMDocument(reason: "PEMDocument has an empty body")
}
let derBytes = Array(data)
return PEMDocument(type: type, derBytes: derBytes)
}
}
extension Substring.UTF8View {
/// A PEM document looks like this:
/// ```
/// -----BEGIN <SOME DISCRIMINATOR>-----
/// <base64 encoded bytes, 64 characters per line>
/// -----END <SOME DISCRIMINATOR>-----
/// ```
/// This function attempts find the BEGIN and END marker.
/// It then tries to extract the discriminator and the base64 encoded string between the START and END marker.
fileprivate mutating func readNextPEMDocument() throws -> LazyPEMDocument? {
/// First find the BEGIN marker: `-----BEGIN <SOME DISCRIMINATOR>-----`
guard
let (
beginDiscriminatorPrefix,
beginDiscriminatorInfix,
beginDiscriminatorSuffix
) = self.firstRangesOf(
prefix: "-----BEGIN ",
suffix: "-----\n"
)
else {
return nil
}
let beginDiscriminator = self[beginDiscriminatorInfix]
/// and then find the END marker: `-----END <SOME DISCRIMINATOR>-----`
guard
let (
endDiscriminatorPrefix,
endDiscriminatorInfix,
endDiscriminatorSuffix
) = self[beginDiscriminatorSuffix.upperBound...].firstRangesOf(
prefix: "-----END ",
suffix: "-----"
)
else {
let pemBegin = self[beginDiscriminatorPrefix.lowerBound..<beginDiscriminatorSuffix.upperBound]
let pemEnd = "-----END \(beginDiscriminator)-----"
throw ASN1Error.invalidPEMDocument(
reason: "PEMDocument has \(String(reflecting: String(pemBegin))) but not \(String(reflecting: pemEnd))"
)
}
let endDiscriminator = self[endDiscriminatorInfix]
/// discriminator found in the BEGIN and END markers need to match
guard beginDiscriminator.elementsEqual(endDiscriminator) else {
throw ASN1Error.invalidPEMDocument(
reason:
"PEMDocument begin and end discriminator don't match. BEGIN: \(String(reflecting: String(beginDiscriminator))). END: \(String(reflecting: String(endDiscriminator)))"
)
}
/// everything between the BEGIN and END markers is considered the base64 encoded string
let base64EncodedDERString = self[beginDiscriminatorSuffix.upperBound..<endDiscriminatorPrefix.lowerBound]
try base64EncodedDERString.checkLineLengthsOfBase64EncodedString()
/// move `self` to the end of the END marker
self = self[endDiscriminatorSuffix.upperBound...]
return LazyPEMDocument(discriminator: beginDiscriminator, base64EncodedDERString: base64EncodedDERString)
}
/// verify line length limits according to RFC
///
/// [4.3.2.4 Step 4: Printable Encoding](https://www.rfc-editor.org/rfc/rfc1421#section-4.3)
///
/// [...]
/// To represent the encapsulated text of a PEM message, the encoding
/// function's output is delimited into text lines (using local
/// conventions), with each line except the last containing exactly 64
/// printable characters and the final line containing 64 or fewer
/// printable characters.
///
private func checkLineLengthsOfBase64EncodedString() throws {
var message = self
let lastIndex = message.index(before: message.endIndex)
while !message.isEmpty {
let expectedNewLineIndex =
message.index(message.startIndex, offsetBy: 64, limitedBy: lastIndex) ?? lastIndex
guard
let actualNewLineIndex = message.firstIndex(of: UInt8(ascii: "\n")),
actualNewLineIndex == expectedNewLineIndex
else {
throw ASN1Error.invalidPEMDocument(reason: "PEMDocument has incorrect line lengths")
}
let nextLineStart = message.index(after: expectedNewLineIndex)
message = message[nextLineStart...]
}
}
}
extension Substring.UTF8View {
func firstRangesOf(
`prefix`: Substring,
suffix: Substring
) -> (
prefix: Range<Index>,
infix: Range<Index>,
suffix: Range<Index>
)? {
self.firstRangesOf(prefix: `prefix`.utf8, suffix: suffix.utf8)
}
func firstRangesOf(
`prefix`: Self,
suffix: Self
) -> (
prefix: Range<Index>,
infix: Range<Index>,
suffix: Range<Index>
)? {
guard
let prefixRange = self.firstRange(of: `prefix`),
let suffixRange = self[prefixRange.upperBound...].firstRange(of: suffix)
else {
return nil
}
return (
prefix: prefixRange,
infix: prefixRange.upperBound..<suffixRange.lowerBound,
suffix: suffixRange
)
}
func firstRange(of other: Self) -> Range<Index>? {
guard other.count >= 1 else {
return self.startIndex..<self.startIndex
}
let otherWithoutFirst = other.dropFirst()
var currentSearchRange = self
while currentSearchRange.count >= other.count {
// find the first occurrence of first element in other
guard let firstIndexOfOther = currentSearchRange.firstIndex(of: other.first!) else {
return nil
}
// this is now the start of a potential match.
// we have already checked the first element so we can skip that and
// continue our search from the second element
let secondIndexOfOther = currentSearchRange.index(after: firstIndexOfOther)
guard
let searchEndIndex = currentSearchRange.index(
firstIndexOfOther,
offsetBy: other.count,
limitedBy: currentSearchRange.endIndex
)
else {
// not enough elements remaining
return nil
}
// check that all elements are equal
if currentSearchRange[secondIndexOfOther..<searchEndIndex].elementsEqual(otherWithoutFirst) {
// we found a match
return firstIndexOfOther..<searchEndIndex
} else {
// we continue our search one after the current match
currentSearchRange = self[secondIndexOfOther...]
}
}
return nil
}
}
#endif
|