// swift-tools-version:6.2import 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")]), ]) import Foundationimport SwiftASN1import 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 expireslet 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
Certificate(pemEncoded:)parses aBEGIN CERTIFICATEblock andCertificate(derEncoded:)the binary form;notValidAfteris aDate.notAfter(path:)reads the file as bytes and picks the decoder by looking for the PEM header, so.pem,.crtand DER.cerfiles all work.timeIntervalSinceNowis negative once the date has passed; dividing by 86 400 and rounding down gives whole days left, andISO8601Format()prints the instant in UTC.PEMDocument.parseMultiplesplits a bundle into its blocks; the chain stops working as soon as any of its certificates expires, so the earliestnotValidAfteris the one to monitor.- For certificates that servers present, use the TLS stack of your platform —
URLSessiondelegates on Apple platforms, SwiftNIO SSL on servers — and parse the leaf with the sameCertificatetype.
Gotchas
rounded(.down)matters for expired certificates: the expired test certificate reports-568days, where truncation toward zero (Int(…)alone) would round the negative value up to-567..cerand.derfiles are often binary DER; the run readsleaf.derthrough thederEncoded:branch. Code that only callsString(contentsOfFile:)andCertificate(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.parseMultipleis what finds both.