// swift-tools-version:6.0import PackageDescription let package = Package( name: "cert-demo", platforms: [.macOS(.v13)], dependencies: [ .package(url: "https://github.com/apple/swift-certificates.git", exact: "1.21.0"), .package(url: "https://github.com/apple/swift-crypto.git", exact: "4.5.2"), .package(url: "https://github.com/apple/swift-asn1.git", exact: "1.7.3"), ], targets: [ .executableTarget(name: "cert-demo", dependencies: [ .product(name: "X509", package: "swift-certificates"), .product(name: "Crypto", package: "swift-crypto"), .product(name: "SwiftASN1", package: "swift-asn1"), ]), ])import Cryptoimport Foundationimport SwiftASN1import X509 let key = P256.Signing.PrivateKey()let name = try DistinguishedName { CommonName("localhost") } let extensions = try Certificate.Extensions { SubjectAlternativeNames([ .dnsName("localhost"), .ipAddress(ASN1OctetString(contentBytes: [127, 0, 0, 1])), // IP SANs are the raw address bytes ]) Critical(BasicConstraints.notCertificateAuthority) Critical(KeyUsage(digitalSignature: true)) try ExtendedKeyUsage([.serverAuth])} // The library rounds times to the nearest second; rounding down keeps notBefore// from landing up to half a second in the futurelet notBefore = Date(timeIntervalSince1970: Date().timeIntervalSince1970.rounded(.down))let certificate = try Certificate( version: .v3, serialNumber: Certificate.SerialNumber(), // random, positive publicKey: Certificate.PublicKey(key.publicKey), notValidBefore: notBefore, notValidAfter: notBefore.addingTimeInterval(365 * 24 * 60 * 60), issuer: name, // self-signed: issuer = subject subject: name, signatureAlgorithm: .ecdsaWithSHA256, extensions: extensions, issuerPrivateKey: Certificate.PrivateKey(key)) try certificate.serializeAsPEM().pemString.write(toFile: "cert.pem", atomically: true, encoding: .utf8)try key.pemRepresentation.write(toFile: "key.pem", atomically: true, encoding: .utf8)How it works
P256.Signing.PrivateKey()creates the key;Certificate.PublicKey(key.publicKey)andCertificate.PrivateKey(key)wrap it for the certificate.Certificate.Extensions { … }collects the extensions;Critical(…)marks basicConstraints and keyUsage as critical, and.ipAddresstakes the address as raw bytes in anASN1OctetString.Certificate.SerialNumber()is 20 random bytes; issuer and subject get the sameDistinguishedName.serializeAsPEM().pemStringgives cert.pem, andkey.pemRepresentationwrites the key as PKCS#8.
Gotchas
- swift-certificates rounds the validity dates to the nearest second, so
notValidBefore: Date()can lie up to half a second in the future: one run of this recipe failed withcertificate is not yet validright after creation. Round the start down, as here. - IP SANs are raw bytes — 4 for IPv4, 16 for IPv6 — so 127.0.0.1 is written as
[127, 0, 0, 1]; there is no string form. ExtendedKeyUsage([.serverAuth])throws on duplicate usages, and theCertificate.Extensionsbuilder throws too; both needtry.- On Apple platforms
import Cryptosimply re-exports CryptoKit; on Linux, swift-crypto brings its own implementation on a vendored BoringSSL, so the same code builds on both.