Skip to content

Commit

Permalink
Use swift-certificates to generate CSR
Browse files Browse the repository at this point in the history
Resolves #1
  • Loading branch information
florentmorin committed Sep 10, 2023
1 parent 9b91872 commit 53012d8
Show file tree
Hide file tree
Showing 27 changed files with 273 additions and 973 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Build directory
.build/

## Xcode ser settings
xcuserdata/

## macOS files
.DS_Store
122 changes: 122 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{
"pins" : [
{
"identity" : "async-http-client",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/async-http-client.git",
"state" : {
"revision" : "16f7e62c08c6969899ce6cc277041e868364e5cf",
"version" : "1.19.0"
}
},
{
"identity" : "jwt-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/jwt-kit.git",
"state" : {
"branch" : "main",
"revision" : "54b9c740b1593e7bf0f4a890cca88152313a84b3"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "ef712c42680022238aeb7a0df75c5b2979218c05",
"version" : "1.0.0-beta.1"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10",
"version" : "1.1.0"
}
},
{
"identity" : "swift-certificates",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-certificates.git",
"state" : {
"revision" : "ef10b819c7a42c39e1fdf39564168e1c39c3731f",
"version" : "1.0.0-beta.1"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
"version" : "1.0.4"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894",
"version" : "2.6.0"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed",
"version" : "1.5.3"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "cf281631ff10ec6111f2761052aa81896a83a007",
"version" : "2.58.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "0e0d0aab665ff1a0659ce75ac003081f2b1c8997",
"version" : "1.19.0"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "a8ccf13fa62775277a5d56844878c828bbb3be1a",
"version" : "1.27.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9",
"version" : "2.25.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "e7403c35ca6bb539a7ca353b91cc2d8ec0362d58",
"version" : "1.19.0"
}
}
],
"version" : 2
}
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
Loading

0 comments on commit 53012d8

Please sign in to comment.