Skip to content
ecdsa.com
Swift

swift-crypto

swift-crypto, Apple's open-source CryptoKit API for Linux and other platforms without CryptoKit: the same Swift types on a vendored BoringSSL — with deterministic Ed25519 where CryptoKit on macOS randomizes it, and no Secure Enclave.
License
Apache-2.0
Checked version
5.0.3 ·
Source
github.com/apple/swift-crypto
Documentation
swiftpackageindex.com/apple/swift-crypto/documentation/crypto

Compatibility at a glance

ECDSAEdDSAPost-quantum

ECDSA curves
P-256P-384P-521
EdDSA
Ed25519
Post-quantum algorithms
ML-KEMML-DSAX-Wing
Public key formats
spki-pemspki-dersec1-uncompressedsec1-compressedraw-xyraw-32
Private key formats
pkcs8-pempkcs8-dersec1-pemsec1-derraw-scalarseed-32

swift-crypto lets server-side and cross-platform Swift code use CryptoKit's API through import Crypto. Its README describes it as an implementation of a substantial portion of that API for Linux and ARM64 Windows; on Apple platforms the module re-exports CryptoKit itself (#if canImport(CryptoKit)), so the facts here describe the other platforms. They were checked with swift-crypto 5.0.3 and swift-asn1 1.7.3 on Linux in the official Swift 6.4.1 image, where the code runs on a vendored copy of BoringSSL.

The same source can behave differently on the two platforms. Ed25519 signatures are deterministic here — the RFC 8032 test key reproduced the RFC's signature exactly — while CryptoKit on macOS randomizes them; both kinds verified with OpenSSL, so tests should verify signatures instead of comparing bytes. Parse errors matched: the same raw r‖s bytes given to ECDSASignature(derRepresentation:) threw truncatedASN1Field on both, but which CryptoKitASN1Error case appears depends on the input bytes, so match on failure, not on one error case.

Everything else measured matched CryptoKit. ECDSA signatures expose rawRepresentation (64-byte r‖s, as in JWS) and derRepresentation (as in X.509); convert with the DER ⇄ raw converter. Nonces are random and s is not normalized; high-S signatures verify. signature(for: data) hashes with the curve's hash, but the digest API accepted SHA-256 on a P-384 key and truncated a SHA-512 digest on P-256, which OpenSSL confirmed. For Ed25519, non-canonical S is rejected while the identity point is accepted as a public key, together with a signature R = identity, S = 0.

The post-quantum types match CryptoKit 26: ML-KEM-768 and ML-KEM-1024, ML-DSA-65 and ML-DSA-87, and the X-Wing hybrid KEM, also as an HPKE ciphersuite. ML-DSA signing is hedged and supports FIPS 204 context strings, ML-KEM decapsulation of a damaged ciphertext returns a different key without an error, and private keys are kept as seeds (64 bytes for ML-KEM, 32 for ML-DSA). Compare with the ML-KEM tool and the post-quantum signature tool.

There is no SecureEnclave in these sources, so code that uses Secure Enclave keys must be compiled only for Apple platforms. Key formats match CryptoKit: P-256 private keys are written as PKCS#8 and read from PKCS#8 or SEC1, PEM or DER, but not from encrypted PKCS#8; Ed25519 keys are 32 raw bytes.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/swift-crypto/run.sh ran swift-crypto 5.0.3 in the container image swift:6.4.1-noble on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    structured

    ECDSASignature has a 64-byte rawRepresentation and a derRepresentation; its bytes (withUnsafeBytes) are the raw form.

    Recorded by the check
    fact ecdsa-signature-encoding = structured
    evidence ecdsa-signature-encoding: signature(for:) returns P256.Signing.ECDSASignature: rawRepresentation 64 bytes, derRepresentation starts with 0x30 and converts back: yes, withUnsafeBytes yields the raw form: yes
  • ECDSA nonce

    random

    Two signatures of one message differed, and the RFC 6979 A.2.5 key and message did not produce the RFC's r.

    Recorded by the check
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: two signatures of one message are different; r for the RFC 6979 A.2.5 key and message equals the RFC's r: no
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: signatures with s > n/2 among 64: yes
  • High-S when verifying

    accepts-high-s

    A signature with s replaced by n − s verified.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: isValidSignature with s replaced by n - s: true
  • What the signing call hashes

    hashes-message-or-digest

    signature(for: data) hashes; signature(for: SHA256.hash(data: message)) verified against the message.

    Recorded by the check
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: signature(for: SHA256.hash(data: message)) verifies with isValidSignature(_:for: message): yes
  • Hash applied to messages

    curve-matched

    Signatures over data verified as signatures over SHA-256 on P-256, SHA-384 on P-384 and SHA-512 on P-521.

    Recorded by the check
    fact ecdsa-message-hash = curve-matched
    evidence ecdsa-message-hash: signature(for: data) verifies as a signature over SHA-256 on P-256: yes, SHA-384 on P-384: yes, SHA-512 on P-521: yes
  • Hash bound to the curve

    none

    A P-384 key signed a SHA-256 digest, and the signature verified.

    Recorded by the check
    fact ecdsa-hash-curve-binding = none
    evidence ecdsa-hash-curve-binding: a P-384 key signed a SHA-256 digest, and the signature verifies: yes
  • Digest longer than the curve order

    truncated

    A P-256 key signed a SHA-512 digest; openssl pkeyutl verified the signature over the digest's first 32 bytes.

    Recorded by the check
    fact ecdsa-long-input = truncated
    evidence ecdsa-long-input: a P-256 key signed a SHA-512 digest without an error
    evidence ecdsa-long-input: openssl pkeyutl verifies that signature over the first 32 digest bytes: yes
  • Malformed ECDSA signature

    throws

    init(derRepresentation:) threw truncatedASN1Field for a raw r‖s signature and for 3 bytes; init(rawRepresentation:) threw incorrectParameterSize for DER and for 3 bytes.

    Recorded by the check
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: ECDSASignature(derRepresentation:) with the RFC 6979 A.2.5 signature as raw r||s: threw CryptoKitASN1Error.truncatedASN1Field; with 3 bytes: threw CryptoKitASN1Error.truncatedASN1Field
    evidence ecdsa-malformed-signature: ECDSASignature(rawRepresentation:) with DER: threw CryptoKitError.incorrectParameterSize; with 3 bytes: threw CryptoKitError.incorrectParameterSize
    evidence ecdsa-malformed-signature: isValidSignature for a valid signature over another message: false
  • ECDSA curves

    P-256P-384P-521

    P-256, P-384 and P-521 signed and verified; the sources declare no other ECDSA curve.

    Recorded by the check
    fact ecdsa-curves = P-256, P-384, P-521
    evidence ecdsa-curves: key types declared in Sources/Crypto and Sources/CryptoExtras: Curve25519 MLDSA65 MLDSA87 MLKEM1024 MLKEM768 P256 P384 P521 XWingMLKEM768X25519
  • EdDSA

    Ed25519

    Curve25519.Signing (Ed25519); the sources declare no Ed448 type.

    Recorded by the check
    fact eddsa = Ed25519
  • Deterministic Ed25519 signatures

    yes

    Two signatures of one message were identical and equal to the RFC 8032 test 1 signature; OpenSSL verified a swift-crypto signature.

    Recorded by the check
    fact ed25519-deterministic = yes
    evidence ed25519-deterministic: RFC 8032 test 1: the derived public key matches: yes, the RFC signature verifies: yes
    evidence ed25519-deterministic: swift-crypto's signature with the same key and message equals the RFC signature: yes; two signatures are identical: yes; both verify: yes
    evidence ed25519-deterministic: OpenSSL 3.0.13 pkeyutl on a swift-crypto Ed25519 signature: Signature Verified Successfully
  • Ed25519 signature with non-canonical S

    rejected

    The RFC 8032 test 1 signature with S replaced by S + L was rejected.

    Recorded by the check
    fact ed25519-non-canonical-s = rejected
    evidence ed25519-non-canonical-s: the RFC 8032 test 1 signature with S replaced by S + L
  • Ed25519 small-order public key

    accepted

    The identity point imported as a public key, and R = identity with S = 0 verified.

    Recorded by the check
    fact ed25519-small-order-key = accepted
    evidence ed25519-small-order-key: the identity point as public key: import accepted; signature R = identity, S = 0 verifies: yes
  • Malformed Ed25519 signature

    rejected

    64 bytes of 0xab and a 63-byte signature returned false.

    Recorded by the check
    fact ed25519-malformed-signature = rejected
    evidence ed25519-malformed-signature: 64 bytes of 0xab: false; 63 bytes of the RFC signature: false
  • Post-quantum algorithms

    ML-KEMML-DSAX-Wing

    ML-KEM and X-Wing encapsulations round-tripped and ML-DSA signatures verified; X-Wing also worked through HPKE.

    Recorded by the check
    fact pqc = ML-KEM, ML-DSA, X-Wing
    evidence pqc: ML-KEM-768: public key 1184 bytes, ciphertext 1088 bytes, shared secrets match: yes
    evidence pqc: ML-KEM-1024: public key 1568 bytes, ciphertext 1568 bytes, shared secrets match: yes
    evidence pqc: ML-DSA-65: public key 1952 bytes, signature 3309 bytes, verifies: yes; ML-DSA-87: signature 4627 bytes, verifies: yes
    evidence pqc: X-Wing (XWingMLKEM768X25519): public key 1216 bytes, ciphertext 1120 bytes, shared secrets match: yes
    evidence pqc: HPKE with XWingMLKEM768X25519_SHA256_AES_GCM_256 opens its own message: yes
  • ML-KEM parameter sets

    ML-KEM-768ML-KEM-1024

    MLKEM768 and MLKEM1024 round-tripped; the sources declare no ML-KEM-512 type.

    Recorded by the check
    fact ml-kem-parameter-sets = ML-KEM-768, ML-KEM-1024
  • ML-DSA parameter sets

    ML-DSA-65ML-DSA-87

    MLDSA65 and MLDSA87 signed and verified; the sources declare no ML-DSA-44 type.

    Recorded by the check
    fact ml-dsa-parameter-sets = ML-DSA-65, ML-DSA-87
  • ML-DSA signing mode

    hedged

    Two ML-DSA-65 signatures of one message differed (hedged signing).

    Recorded by the check
    fact ml-dsa-signing = hedged
    evidence ml-dsa-signing: two ML-DSA-65 signatures of one message are different
  • ML-DSA context string

    supported

    A signature with the context invoice-v1 verified with it and failed with invoice-v2 and without a context.

    Recorded by the check
    fact ml-dsa-context-string = supported
    evidence ml-dsa-context-string: signed with context "invoice-v1": verifies with it yes, with "invoice-v2" no, without a context no
  • Malformed ML-DSA signature

    returns-false

    A truncated signature, 3 bytes and a wrong message returned false.

    Recorded by the check
    fact ml-dsa-malformed-signature = returns-false
    evidence ml-dsa-malformed-signature: isValidSignature: truncated signature false, 3 bytes false, wrong message false
  • Modified ML-KEM ciphertext

    implicit-rejection

    With one ciphertext bit flipped, decapsulate returned a key different from the sender's.

    Recorded by the check
    fact ml-kem-modified-ciphertext = implicit-rejection
    evidence ml-kem-modified-ciphertext: ML-KEM-768 with one ciphertext bit flipped: decapsulate returned a key that differs from the sender's
  • Post-quantum private keys

    seed-only

    seedRepresentation is 64 bytes for ML-KEM-768 and 32 for ML-DSA-65, and keys rebuilt from it matched.

    Recorded by the check
    fact pq-private-keys = seed-only
    evidence pq-private-keys: ML-KEM-768 seedRepresentation 64 bytes, integrityCheckedRepresentation 96 bytes; ML-DSA-65 seedRepresentation 32 bytes, integrityCheckedRepresentation 64 bytes
    evidence pq-private-keys: keys rebuilt from the seed alone have the same public key: ML-KEM-768 yes, ML-DSA-65 yes
  • Public key formats

    spki-pemspki-dersec1-uncompressedsec1-compressedraw-xyraw-32

    SPKI PEM and DER, SEC1 uncompressed and compressed, and raw x‖y for P-256, plus 32 raw bytes for Ed25519, all written and read back.

    Recorded by the check
    fact public-key-formats = spki-pem, spki-der, sec1-uncompressed, sec1-compressed, raw-xy, raw-32
    evidence public-key-formats: P-256 public key sizes: raw 64, x963 65, compressed 33, DER 91; PEM header -----BEGIN PUBLIC KEY-----; Ed25519 raw 32
    evidence public-key-formats: written and read back: spki-pem yes, spki-der yes, sec1-uncompressed yes, sec1-compressed yes, raw-xy yes, raw-32 yes
  • Private key formats

    pkcs8-pempkcs8-dersec1-pemsec1-derraw-scalarseed-32

    Reads PKCS#8 and SEC1, PEM or DER, the raw P-256 scalar and the Ed25519 seed; writes PKCS#8; encrypted PKCS#8 is refused.

    Recorded by the check
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, sec1-der, raw-scalar, seed-32
    evidence private-key-formats: P-256 private key written as: derRepresentation PKCS#8 (138 bytes), pemRepresentation -----BEGIN PRIVATE KEY-----, rawRepresentation 32 bytes, x963Representation 97 bytes
    evidence private-key-formats: OpenSSL-written P-256 keys read: pkcs8-pem yes, pkcs8-der yes, encrypted-pkcs8 no, sec1-pem yes, sec1-der yes; encrypted PKCS#8: threw CryptoKitASN1Error.invalidPEMDocument

Usage

Sign and verify with Ed25519 and run an ML-KEM-768 key exchange on Linux.
import Crypto
import Foundation

let message = Data("order #1042 approved".utf8)

// Ed25519 signatures are deterministic here but randomized under Apple's CryptoKit:
// compare signatures by verifying them, never byte for byte.
let signingKey = Curve25519.Signing.PrivateKey()
let signature = try signingKey.signature(for: message)
print("Ed25519 verified: \(signingKey.publicKey.isValidSignature(signature, for: message))")

// ML-KEM-768: the receiver publishes a public key, the sender encapsulates a fresh secret to it.
let receiver = try MLKEM768.PrivateKey()
let sent = try receiver.publicKey.encapsulate()
let received = try receiver.decapsulate(sent.encapsulated)
print("ML-KEM-768 secrets match: \(received == sent.sharedSecret)")

The snippet harness runs every sample verbatim in the container image swift:6.4.1-noble, and the run fails unless the output matches.

Command
swift run -c release usage
Output must match (regular expression)
Ed25519 verified: true\nML-KEM-768 secrets match: true

Libraries