- OpenSSL
- Getting Started with OpenSSL in Swift
Article
Getting Started with OpenSSL in Swift
A task-oriented walkthrough of the four shipping capabilities in OpenSSL: SHA-256 hashing, Base64URL encoding, RSA PEM ingestion, and runtime version auditing.
Overview
Each section below solves one concrete task. All executable examples come from files under Snippets/ that compile on every swift build, so the code you see here stays in lock-step with the public API.
Adding OpenSSL to Your Project
Add swift-openssl as a Swift Package Manager dependency. Because the package is pre-1.0, pin an exact version to avoid unexpected breaking changes when resolving (SemVer major version zero reserves this as the “anything may change at any time” range):
// Package.swift
.package(url: "https://github.com/21-DOT-DEV/swift-openssl.git", exact: "0.1.0"),
// Target dependencies
.target(name: "<target>", dependencies: [
.product(name: "OpenSSL", package: "swift-openssl"),
]),
Then import OpenSSL in any Swift file that needs it. For the raw libcrypto and libssl C binding products (used when linking OpenSSL into another Swift package that exposes its own C sources), see Choosing Between OpenSSL, libcrypto, and libssl. For production-readiness caveats before depending on the package, read Security Considerations — vulnerability reports go through SECURITY.md.
Computing a SHA-256 Digest
FIPS PUB 180-4 SHA-256 produces a fixed 32-byte digest. The Swift API exposes two entry points — one for arbitrary bytes (hash(data:)) and one for UTF-8 strings (hash(string:)) — both returning a typed SHA256.SHA256Digest with a hexString accessor that matches the canonical lowercase hex representation used in JWT fingerprints, Nostr NIP-01 event IDs, and Git-style blob hashes.
Use hash(data:) whenever the input is already a Data value — it is byte-exact and matches every RFC 6234 §8.5 test vector. Reach for hash(string:) only when hashing Swift strings directly; remember that it hashes the UTF-8 byte sequence, so Unicode-normalization differences change the digest (see the > Warning: on hash(string:)).
Encoding and Decoding Base64URL
Base64URL, defined in RFC 4648 §5, is the URL-safe unpadded encoding used by JSON Web Tokens (RFC 7519), JSON Web Signatures (RFC 7515), WebAuthn credential blobs, and Nostr NIP-19 identifiers. The alphabet replaces + with - and / with _ relative to standard Base64, and strips the = padding — producing strings that can be dropped into URL path segments, query parameters, HTTP headers, and filename components without escaping.
encode(_:) never emits +, /, or =; decode(_:) accepts both padded and unpadded inputs and returns nil on malformed text rather than throwing.
Parsing RSA Keys from PEM
Warning
Signing and verification are not yet functional. The steps below parse PEM key material and round-trip it through typed RSA.PrivateKey / RSA.PublicKey values, but they cannot produce or verify signatures. That path requires the OpenSSL provider layer, which is not integrated in this MVP. See Security Considerations for the complete MVP gap list.
OpenSSL accepts PEM-encoded RSA keys in the traditional PKCS#1 framing (RFC 8017) and the modern PKCS#8 framing (RFC 5958, encoded per RFC 7468). Public keys must use the SubjectPublicKeyInfo (SPKI) form, -----BEGIN PUBLIC KEY----- from RFC 5280 §4.1.2.7; the legacy PKCS#1 public framing is not accepted — convert with openssl rsa -RSAPublicKey_in -pubout first.
Parsing succeeds when the outer PEM frame is intact; the underlying DER is not structurally validated until the provider layer lands. Use pemData and pemData to round-trip the original bytes to storage.
Auditing the Runtime OpenSSL Version
versionString exposes the value of OpenSSL_version(OPENSSL_VERSION) from the libcrypto runtime compiled into this package. Cross-reference the major/minor/patch triple against the OpenSSL security advisories before cutting a release build — the string is the canonical way to tell which CVE surface your deployed binary carries. Runtime auditing applies equally to transitive consumers: a package that depends on swift-tor, which depends on swift-openssl, sees the same linked runtime.
When to Reach for OpenSSL Instead of CryptoKit
OpenSSL complements swift-crypto and Apple’s CryptoKit rather than replacing them. Use this decision table:
| Use case |
Framework |
| SHA-256 / SHA-2 family, AES-GCM, ChaCha20-Poly1305, HKDF, Curve25519 |
swift-crypto / CryptoKit |
| RSA with explicit PKCS#1 v1.5 or PSS padding |
OpenSSL (post-provider integration) |
| PEM parsing and round-tripping |
OpenSSL |
| Base64URL encoding with JOSE-compliant alphabet |
OpenSSL |
| Legacy ciphers (AES-CBC with custom IV, DES variants) |
OpenSSL |
| Runtime OpenSSL version auditing |
OpenSSL |
| TLS client or server from scratch |
swift-nio-ssl |
| Interop with existing OpenSSL-based C or C++ code |
libcrypto / libssl (this package) |
Using swift-openssl as a Runtime Dependency
Other Swift packages can consume libcrypto and libssl from this package directly without going through the OpenSSL Swift API. The canonical example is swift-tor, which declares swift-openssl as a branch-pinned dependency and wires libcrypto and libssl into its libtor target:
// From swift-tor's Package.swift
dependencies: [
.package(url: "https://github.com/21-DOT-DEV/swift-openssl.git", branch: "main"),
.package(url: "https://github.com/21-DOT-DEV/swift-event.git", branch: "main"),
],
targets: [
.target(
name: "libtor",
dependencies: [
.product(name: "libcrypto", package: "swift-openssl"),
.product(name: "libssl", package: "swift-openssl"),
.product(name: "libevent", package: "swift-event"),
],
// ...
),
],
This pattern is the intended way to bring a full OpenSSL runtime into a Swift package that has its own C sources (Tor’s event-driven networking, for example) without bundling duplicate OpenSSL builds across the dependency graph. See Choosing Between OpenSSL, libcrypto, and libssl for the full product-selection rationale.
Next Steps