Skip to content
ecdsa.com

Certificate expiry · Swift

How to check SSL certificate expiration date in Swift

To check SSL certificate expiration date in Swift, parse the certificate with Apple's swift-certificates package and read notValidAfter, a Foundation Date. The package is pure Swift and works on Linux and Apple platforms alike; this recipe checks PEM and DER files, an expired certificate, and the earliest expiry in a chain bundle.

Tested with swift-certificates 1.21.0 (Swift 6.4.1 on Ubuntu 24.04) and OpenSSL 3.0.13 (DER copy of the certificate).

Package.swift — swift-certificates
// swift-tools-version:6.2
import PackageDescription
let package = Package(
name: "howto",
platforms: [.macOS(.v15)],
dependencies: [
.package(url: "https://github.com/apple/swift-certificates.git", exact: "1.21.0"),
],
targets: [
.executableTarget(name: "expiry", dependencies: [.product(name: "X509", package: "swift-certificates")]),
]
)
Sources/expiry/main.swift — notAfter of PEM and DER files, and of a bundle
import Foundation
import SwiftASN1
import X509
/// notAfter of a certificate file, PEM or DER (.cer files are often DER).
func notAfter(path: String) throws -> Date {
let data = try Data(contentsOf: URL(fileURLWithPath: path))
if let text = String(data: data, encoding: .utf8), text.contains("-----BEGIN CERTIFICATE-----") {
return try Certificate(pemEncoded: text).notValidAfter
}
return try Certificate(derEncoded: Array(data)).notValidAfter
}
for path in ["leaf.pem", "expired.pem", "leaf.der"] {
let end = try notAfter(path: path)
let daysLeft = Int((end.timeIntervalSinceNow / 86_400).rounded(.down))
print("\(path): notAfter \(end.ISO8601Format()), \(daysLeft) days left")
}
// A bundle (leaf + intermediates): the chain stops working when its first certificate expires
let bundle = try String(contentsOfFile: "chain.pem", encoding: .utf8)
let chain = try PEMDocument.parseMultiple(pemString: bundle).map { try Certificate(pemDocument: $0) }
let earliest = chain.map(\.notValidAfter).min()!
print("chain.pem: \(chain.count) certificates, earliest notAfter \(earliest.ISO8601Format())")

How it works

  1. Certificate(pemEncoded:) parses a BEGIN CERTIFICATE block and Certificate(derEncoded:) the binary form; notValidAfter is a Date.
  2. notAfter(path:) reads the file as bytes and picks the decoder by looking for the PEM header, so .pem, .crt and DER .cer files all work.
  3. timeIntervalSinceNow is negative once the date has passed; dividing by 86 400 and rounding down gives whole days left, and ISO8601Format() prints the instant in UTC.
  4. PEMDocument.parseMultiple splits a bundle into its blocks; the chain stops working as soon as any of its certificates expires, so the earliest notValidAfter is the one to monitor.
  5. For certificates that servers present, use the TLS stack of your platform — URLSession delegates on Apple platforms, SwiftNIO SSL on servers — and parse the leaf with the same Certificate type.

Gotchas

  • rounded(.down) matters for expired certificates: the expired test certificate reports -568 days, where truncation toward zero (Int(…) alone) would round the negative value up to -567.
  • .cer and .der files are often binary DER; the run reads leaf.der through the derEncoded: branch. Code that only calls String(contentsOfFile:) and Certificate(pemEncoded:) handles PEM files alone.
  • A bundle holds several certificates with different dates: here the leaf ends in 2035 and the intermediate in 2041, and PEMDocument.parseMultiple is what finds both.

Related recipes