Skip to content
ecdsa.com

Decode a certificate · Swift

How to decode an X.509 certificate in Swift

To decode an X.509 certificate in Swift on any platform, use Apple's swift-certificates package: Certificate(pemEncoded:) parses the file into a value type with typed accessors for subject, validity, extensions and the public key. It is pure Swift, works on Linux and Apple platforms alike, and pairs with swift-crypto for the fingerprint and key checks.

Tested with swift-certificates 1.21.0 (Swift 6.4.1 on Ubuntu 24.04, swift-asn1 1.7.3) and swift-crypto 5.0.3 (fingerprint and key type).

Package.swift — swift-certificates and swift-crypto
// 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"),
.package(url: "https://github.com/apple/swift-crypto.git", exact: "5.0.3"),
],
targets: [
.executableTarget(
name: "decode",
dependencies: [
.product(name: "X509", package: "swift-certificates"),
.product(name: "Crypto", package: "swift-crypto"),
]
),
]
)
Sources/decode/main.swift — fields, SANs, key usage, key type and fingerprint
import Crypto
import Foundation
import SwiftASN1
import X509
let pem = try String(contentsOfFile: "cert.pem", encoding: .utf8)
let certificate = try Certificate(pemEncoded: pem) // Certificate(derEncoded:) for binary input
print("subject: \(certificate.subject)")
let commonName = certificate.subject.flatMap { $0 }.first { $0.type == .RDNAttributeType.commonName }
print("common name: \(commonName.map { String(describing: $0.value) } ?? "-")")
print("issuer: \(certificate.issuer)")
print("serial: \(certificate.serialNumber)")
print("valid: \(certificate.notValidBefore) -> \(certificate.notValidAfter)")
print("signed with: \(certificate.signatureAlgorithm)") // the issuer's algorithm
// typed extension accessors: nil when absent, throwing when malformed
if let names = try certificate.extensions.subjectAlternativeNames {
for case .dnsName(let host) in names { print("DNS name: \(host)") }
}
if let keyUsage = try certificate.extensions.keyUsage { print("key usage: \(keyUsage)") }
if P256.Signing.PublicKey(certificate.publicKey) != nil { print("key: ECDSA P-256") }
var serializer = DER.Serializer()
try serializer.serialize(certificate)
let fingerprint = SHA256.hash(data: serializer.serializedBytes) // the whole DER certificate
print("SHA-256 fingerprint: \(fingerprint.map { String(format: "%02X", $0) }.joined(separator: ":"))")
print("self-signed signature valid: \(certificate.publicKey.isValidSignature(certificate.signature, for: certificate))")

How it works

  1. Certificate(pemEncoded:) parses a BEGIN CERTIFICATE block; Certificate(derEncoded:) takes the binary form.
  2. subject and issuer are DistinguishedName values; flattening their RDNs and matching .RDNAttributeType.commonName extracts the CN without string parsing.
  3. Extension accessors such as subjectAlternativeNames and keyUsage are typed: nil when the extension is absent, a thrown error when it is malformed.
  4. P256.Signing.PublicKey(certificate.publicKey) converts the key to a CryptoKit type when it is P-256 (and returns nil otherwise), ready for isValidSignature.
  5. The fingerprint hashes the DER of the whole certificate, which DER.Serializer reproduces from the parsed value; publicKey.isValidSignature(_:for:) checks who signed it.

Gotchas

  • description of the serial number prints colon-separated hex without leading zeros (4:a1:b2:…:7), not OpenSSL's 04A1B2C3D4E5F607. Compare the serial's bytes, not strings.
  • Names print as RFC 4514 strings (C=US,O=…,CN=…), which list the attributes in reverse of the order stored in the certificate — and of OpenSSL's one-line form. Another reason to compare structured values.
  • Checking one signature is not chain validation. For trust decisions use the package's Verifier with a CertificateStore of roots and a policy such as RFC5280Policy.
  • SubjectAlternativeNames is its own collection type, so try extensions.subjectAlternativeNames ?? [] does not compile; unwrap it with if let, as above.

Related recipes