Skip to content
ecdsa.com

Self-signed certificate · Swift

How to create a self-signed EC P-256 certificate with SANs in Swift

Apple's swift-certificates builds certificates on Apple platforms and on Linux: Certificate takes the version, a random serial, the public key, the validity, issuer and subject and the extensions from a result builder, and signs with the issuer's private key — here the same P-256 key, which makes the certificate self-signed. The run builds with SwiftPM on Linux, decodes cert.pem with OpenSSL and verifies it for localhost and 127.0.0.1.

Tested with swift-certificates 1.21.0 (Swift 6.4.1, Linux, swift-crypto 4.5.2, swift-asn1 1.7.3).

Package.swift — swift-certificates, swift-crypto and swift-asn1
// swift-tools-version:6.0
import 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"),
]),
]
)
main.swift — P-256 key and self-signed server certificate
import Crypto
import Foundation
import SwiftASN1
import 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 future
let 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

  1. P256.Signing.PrivateKey() creates the key; Certificate.PublicKey(key.publicKey) and Certificate.PrivateKey(key) wrap it for the certificate.
  2. Certificate.Extensions { … } collects the extensions; Critical(…) marks basicConstraints and keyUsage as critical, and .ipAddress takes the address as raw bytes in an ASN1OctetString.
  3. Certificate.SerialNumber() is 20 random bytes; issuer and subject get the same DistinguishedName.
  4. serializeAsPEM().pemString gives cert.pem, and key.pemRepresentation writes 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 with certificate is not yet valid right 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 the Certificate.Extensions builder throws too; both need try.
  • On Apple platforms import Crypto simply re-exports CryptoKit; on Linux, swift-crypto brings its own implementation on a vendored BoringSSL, so the same code builds on both.

Related recipes