Skip to content

Commit

Permalink
Merge pull request #2 from florentmorin/migrate-to-swift-certificates
Browse files Browse the repository at this point in the history
Use `swift-certificates` to generate CSR
  • Loading branch information
m-barthelemy authored Sep 24, 2023
2 parents 9b91872 + 252b656 commit 6d36f86
Show file tree
Hide file tree
Showing 26 changed files with 155 additions and 973 deletions.
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Build directory
.build/

## Xcode ser settings
xcuserdata/
.swiftpm/xcode

## macOS files
.DS_Store

## Resolved package dependencies
Package.resolved
7 changes: 4 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ let package = Package(
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.10.0"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.1.0"),
.package(url: "https://github.com/vapor/jwt-kit.git", branch: "main"),
// x509
.package(url: "https://github.com/outfoxx/PotentCodables.git", from: "2.2.0"),
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.0.0-beta.1"),
.package(url: "https://github.com/apple/swift-asn1.git", from: "1.0.0-beta.1")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand All @@ -29,7 +29,8 @@ let package = Package(
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "_CryptoExtras", package: "swift-crypto"),
.product(name: "JWTKit", package: "jwt-kit"),
"PotentCodables"
.product(name: "X509", package: "swift-certificates"),
.product(name: "SwiftASN1", package: "swift-asn1")
]),
.testTarget(
name: "AcmeSwiftTests",
Expand Down
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,12 @@ let finalizedOrder = try await acme.orders.finalize(order: order, withPemCsr: ".
If you want AcmeSwift to generate one for you:
```swift
// ECDSA key and certificate
let csr = try AcmeX509Csr.ecdsa(domains: ["mydomain.com", "www.mydomain.com"])
let (privateKey, csr, finalizedOrder) = try await acme.orders.finalizeWithEcdsa(order: order, domains: ["mydomain.com", "www.mydomain.com"])
// .. or, good old RSA
let csr = try AcmeX509Csr.rsa(domains: ["mydomain.com", "www.mydomain.com"])
let (privateKey, csr, finalizedOrder) = try await acme.orders.finalizeWithRsa(order: order, domains: ["mydomain.com", "www.mydomain.com"])

let finalizedOrder = try await acme.orders.finalize(order: order, withCsr: csr)
// You can access the private key used to generate the CSR (and to use once you get the certificate)
print("\n• Private key: \(csr.privateKeyPem)")
print("\n• Private key: \(try privateKey.serializeAsPEM().pemString)")
```

<br/>
Expand Down Expand Up @@ -255,10 +254,8 @@ guard failed.count == 0 else {
}

// Let's create a private key and CSR using the rudimentary feature provided by AcmeSwift
let csr = try AcmeX509Csr.ecdsa(domains: domains)

// If the validation didn't throw any error, we can now send our Certificate Signing Request...
let finalized = try await acme.orders.finalize(order: order, withCsr: csr)
let (privateKey, csr, finalized) = try await acme.orders.finalizeWithRsa(order: order, domains: domains)

// ... and the certificate is ready to download!
let certs = try await acme.certificates.download(for: finalized)
Expand All @@ -268,7 +265,7 @@ try certs.joined(separator: "\n").write(to: URL(fileURLWithPath: "cert.pem"), at

// Now we also need to export the private key, encoded as PEM
// If your server doesn't accept it, append a line return to it.
try csr.privateKeyPem.write(to: URL(fileURLWithPath: "key.pem"), atomically: true, encoding: .utf8)
try privateKey.serializeAsPEM().pemString.write(to: URL(fileURLWithPath: "key.pem"), atomically: true, encoding: .utf8)
```


Expand Down
92 changes: 88 additions & 4 deletions Sources/AcmeSwift/APIs/AcmeSwift+Orders.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Foundation
import Crypto
import _CryptoExtras
import JWTKit
import SwiftASN1
import X509

extension AcmeSwift {

Expand Down Expand Up @@ -33,6 +36,8 @@ extension AcmeSwift {
/// - Parameters:
/// - url: The URL of the Order.
public func get(url: URL) async throws -> AcmeOrderInfo {
try await self.client.ensureLoggedIn()

let ep = GetOrderEndpoint(url: url)
var (info, headers) = try await self.client.run(ep, privateKey: self.client.login!.key, accountURL: client.accountURL!)
info.url = URL(string: headers["Location"].first ?? "")
Expand Down Expand Up @@ -96,17 +101,96 @@ extension AcmeSwift {
let (info, _) = try await self.client.run(ep, privateKey: self.client.login!.key, accountURL: client.accountURL!)
return info
}

/// Finalizes an Order and send the ECDSA CSR.
/// - Parameters:
/// - order: The `AcmeOrderInfo` returned by the call to `.create()`
/// - subject: Subject of certificate
/// - domains: Domains for certificate
/// - Throws: Errors that can occur when executing the request.
/// - Returns: Returns `Certificate.PrivateKey`, `CertificateSigningRequest` and `Account`.
public func finalizeWithEcdsa(order: AcmeOrderInfo, subject: String? = nil, domains: [String]) async throws -> (Certificate.PrivateKey, CertificateSigningRequest, AcmeOrderInfo) {
guard domains.count > 0 else {
throw AcmeError.noDomains("At least 1 DNS name is required")
}

let p256 = P256.Signing.PrivateKey()
let privateKey = Certificate.PrivateKey(p256)
let commonName = subject ?? domains[0]
let name = try DistinguishedName {
CommonName(commonName)
}
let extensions = try Certificate.Extensions {
SubjectAlternativeNames(domains.map({ GeneralName.dnsName($0) }))
}
let extensionRequest = ExtensionRequest(extensions: extensions)
let attributes = try CertificateSigningRequest.Attributes(
[.init(extensionRequest)]
)
let csr = try CertificateSigningRequest(
version: .v1,
subject: name,
privateKey: privateKey,
attributes: attributes,
signatureAlgorithm: .ecdsaWithSHA256
)

let account = try await finalize(order: order, withCsr: csr)

return (privateKey, csr, account)
}

/// Finalizes an Order and send the RSA CSR.
/// - Parameters:
/// - order: The `AcmeOrderInfo` returned by the call to `.create()`
/// - subject: Subject of certificate
/// - domains: Domains for certificate
/// - Throws: Errors that can occur when executing the request.
/// - Returns: Returns `Certificate.PrivateKey`, `CertificateSigningRequest` and `Account`.
public func finalizeWithRsa(order: AcmeOrderInfo, subject: String? = nil, domains: [String]) async throws -> (Certificate.PrivateKey, CertificateSigningRequest, AcmeOrderInfo) {
guard domains.count > 0 else {
throw AcmeError.noDomains("At least 1 DNS name is required")
}

let p256 = try _CryptoExtras._RSA.Signing.PrivateKey(keySize: .bits2048)
let privateKey = Certificate.PrivateKey(p256)
let commonName = subject ?? domains[0]
let name = try DistinguishedName {
CommonName(commonName)
}
let extensions = try Certificate.Extensions {
SubjectAlternativeNames(domains.map({ GeneralName.dnsName($0) }))
}
let extensionRequest = ExtensionRequest(extensions: extensions)
let attributes = try CertificateSigningRequest.Attributes(
[.init(extensionRequest)]
)
let csr = try CertificateSigningRequest(
version: .v1,
subject: name,
privateKey: privateKey,
attributes: attributes,
signatureAlgorithm: .sha256WithRSAEncryption
)

let account = try await finalize(order: order, withCsr: csr)

return (privateKey, csr, account)
}

/// Finalizes an Order and send the CSR.
/// - Parameters:
/// - order: The `AcmeOrderInfo` returned by the call to `.create()`
/// - withCsr: An instance of an `AcmeX509Csr`.
/// - withCsr: An instance of an `Certificate`.
/// - Throws: Errors that can occur when executing the request.
/// - Returns: Returns the `Account`.
public func finalize(order: AcmeOrderInfo, withCsr: AcmeX509Csr) async throws -> AcmeOrderInfo {
public func finalize(order: AcmeOrderInfo, withCsr csr: CertificateSigningRequest) async throws -> AcmeOrderInfo {
try await self.client.ensureLoggedIn()

let csrBytes = try withCsr.derEncoded()

var serializer = DER.Serializer()
try serializer.serialize(csr)

let csrBytes = Data(serializer.serializedBytes)
let pemStr = csrBytes.toBase64UrlString()
let ep = FinalizeOrderEndpoint(orderURL: order.finalize, spec: .init(csr: pemStr))

Expand Down
2 changes: 2 additions & 0 deletions Sources/AcmeSwift/Models/AcmeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public enum AcmeError: Error {

/// A resource should have a URL, returned in a response "Location" header, but couldn't find or parse the header.
case noResourceUrl

case noDomains(String)
}

public struct AcmeResponseError: Codable, Error {
Expand Down
176 changes: 0 additions & 176 deletions Sources/AcmeSwift/x509/AcmeX509Csr.swift

This file was deleted.

Loading

0 comments on commit 6d36f86

Please sign in to comment.