From 73c38cb8b06d601d2b1a9ae4cf2f644db17518af Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Sun, 16 Apr 2023 10:46:00 +1000 Subject: [PATCH] Allow passing Order when creating CSR --- Sources/AcmeSwift/APIs/AcmeSwift+Csr.swift | 52 ++++++++++++++++++++++ Sources/AcmeSwift/x509/AcmeX509Csr.swift | 40 +++++++++++++++-- 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 Sources/AcmeSwift/APIs/AcmeSwift+Csr.swift diff --git a/Sources/AcmeSwift/APIs/AcmeSwift+Csr.swift b/Sources/AcmeSwift/APIs/AcmeSwift+Csr.swift new file mode 100644 index 0000000..c3ea041 --- /dev/null +++ b/Sources/AcmeSwift/APIs/AcmeSwift+Csr.swift @@ -0,0 +1,52 @@ +import Foundation +import Crypto + +extension AcmeSwift { + /// APIs related to CSRs. + public var csr: CsrAPI { + .init(client: self) + } + + public struct CsrAPI { + fileprivate var client: AcmeSwift + + /// Downloads the certificate chain for a finalized Order. + /// The certificates are returned a a list of PEM strings. + /// The first item is the final certificate for the domain. + /// The second item, if any, is the issuer certificate. + public func rsa(`for` order: AcmeOrderInfo) async throws -> [String] { + try await self.client.ensureLoggedIn() + + guard order.status == .valid, let certURL = order.certificate else { + throw AcmeError.certificateNotReady(order.status, "Order must have a `valid` status. Some challenges might not have been completed yet") + } + + let separator = "-----END CERTIFICATE-----\n" + let ep = DownloadCertificateEndpoint(certURL: certURL) + let (certificateChain, _) = try await self.client.run(ep, privateKey: self.client.login!.key, accountURL: client.accountURL!) + var certificates: [String] = [] + for certificate in certificateChain.components(separatedBy: separator) { + if certificate != "" { + certificates.append("\(certificate)\(separator)".trimmingCharacters(in: .newlines)) + } + } + return certificates + } + + /// Revokes a previously issued certificate. + /// - Parameters: + /// - certificatePem: The Certificate **in PEM format**. + public func ecdsa(certificatePem: String, reason: AcmeRevokeReason? = nil) async throws { + try await self.client.ensureLoggedIn() + + let csrBytes = certificatePem.pemToData() + let pemStr = csrBytes.toBase64UrlString() + + let ep = RevokeCertificateEndpoint( + directory: self.client.directory, + spec: .init(certificate: pemStr, reason: reason) + ) + let (_, _) = try await self.client.run(ep, privateKey: self.client.login!.key, accountURL: client.accountURL!) + } + } +} diff --git a/Sources/AcmeSwift/x509/AcmeX509Csr.swift b/Sources/AcmeSwift/x509/AcmeX509Csr.swift index 82bf704..7424582 100644 --- a/Sources/AcmeSwift/x509/AcmeX509Csr.swift +++ b/Sources/AcmeSwift/x509/AcmeX509Csr.swift @@ -23,12 +23,29 @@ public struct AcmeX509Csr { return csr } - + + public static func rsa(key: _CryptoExtras._RSA.Signing.PrivateKey = try! .init(keySize: .bits2048), subject: X509Subject? = nil, order: AcmeOrderInfo, keyUsage: X509KeyUsage? = nil, extendedKeyUsage: [X509ExtendedKeyUsageOID]? = nil) throws -> AcmeX509Csr { + guard order.identifiers.count > 0 else { + throw X509Error.noDomains("At least 1 DNS name is required") + } + let domains = order.identifiers.map{$0.value} + let rsa = try RsaCSR.init(key: key, subject: subject, domains: domains, keyUsage: keyUsage, extendedKeyUsage: extendedKeyUsage) + let csr = self.init(privateKey: rsa.key.derRepresentation, privateKeyPem: rsa.key.pemEncoded(), asn1Csr: rsa.asn1Csr) + + return csr + } + public static func rsa(keyPem: String, subject: X509Subject? = nil, domains: [String]) throws -> AcmeX509Csr { let key = try _CryptoExtras._RSA.Signing.PrivateKey.init(pemRepresentation: keyPem) return try rsa(key: key, subject: subject, domains: domains) } - + + public static func rsa(keyPem: String, subject: X509Subject? = nil, order: AcmeOrderInfo) throws -> AcmeX509Csr { + let key = try _CryptoExtras._RSA.Signing.PrivateKey.init(pemRepresentation: keyPem) + let domains = order.identifiers.map{$0.value} + return try rsa(key: key, subject: subject, domains: domains) + } + /// A CSR using a P256 private key public static func ecdsa(key: Crypto.P256.Signing.PrivateKey = .init(), subject: X509Subject? = nil, domains: [String], keyUsage: X509KeyUsage? = nil, extendedKeyUsage: [X509ExtendedKeyUsageOID]? = nil) throws -> AcmeX509Csr { guard domains.count > 0 else { @@ -37,13 +54,30 @@ public struct AcmeX509Csr { let ecdsa = try EcdsaCSR.init(key: key, subject: subject, domains: domains, keyUsage: keyUsage, extendedKeyUsage: extendedKeyUsage) return self.init(privateKey: ecdsa.key.derRepresentation, privateKeyPem: ecdsa.key.pemEncoded(), asn1Csr: ecdsa.asn1Csr) } - + + /// A CSR using a P256 private key + public static func ecdsa(key: Crypto.P256.Signing.PrivateKey = .init(), subject: X509Subject? = nil, order: AcmeOrderInfo, keyUsage: X509KeyUsage? = nil, extendedKeyUsage: [X509ExtendedKeyUsageOID]? = nil) throws -> AcmeX509Csr { + guard order.identifiers.count > 0 else { + throw X509Error.noDomains("At least 1 DNS name is required") + } + let domains = order.identifiers.map{$0.value} + let ecdsa = try EcdsaCSR.init(key: key, subject: subject, domains: domains, keyUsage: keyUsage, extendedKeyUsage: extendedKeyUsage) + return self.init(privateKey: ecdsa.key.derRepresentation, privateKeyPem: ecdsa.key.pemEncoded(), asn1Csr: ecdsa.asn1Csr) + } + /// A CSR using a P256 private key public static func ecdsa(keyPem: String, subject: X509Subject? = nil, domains: [String]) throws -> AcmeX509Csr { let key = try Crypto.P256.Signing.PrivateKey.init(pemRepresentation: keyPem) return try ecdsa(key: key, subject: subject, domains: domains) } + /// A CSR using a P256 private key + public static func ecdsa(keyPem: String, subject: X509Subject? = nil, order: AcmeOrderInfo) throws -> AcmeX509Csr { + let key = try Crypto.P256.Signing.PrivateKey.init(pemRepresentation: keyPem) + let domains = order.identifiers.map{$0.value} + return try ecdsa(key: key, subject: subject, domains: domains) + } + /// Returns the CSR as DER Data public func derEncoded() throws -> Data { let encoder = ASN1Encoder(schema: Asn1CertificateSigningRequest.schema)