Skip to content
ecdsa.com

Sign a message · Swift

How to sign a message with ECDSA in Swift

To sign a message with ECDSA in Swift, load the key as P256.Signing.PrivateKey and call signature(for:) — CryptoKit's API, available on Linux through swift-crypto. The resulting ECDSASignature gives you both encodings: derRepresentation for OpenSSL, Java and Go, rawRepresentation (64 bytes) for JWS and WebCrypto.

Tested with swift-crypto 5.0.3 (Swift 6.4.1 on Ubuntu 24.04 (BoringSSL backend)) and OpenSSL 3.0.13 (openssl dgst cross-check).

Package.swift — swift-crypto for Linux (Apple platforms ship CryptoKit)
// swift-tools-version:6.2
import PackageDescription
let package = Package(
name: "howto",
platforms: [.macOS(.v15)],
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", exact: "5.0.3"),
],
targets: [
.executableTarget(name: "sign", dependencies: [.product(name: "Crypto", package: "swift-crypto")]),
]
)
Sources/sign/main.swift — sign with SHA-256, write DER and raw
import Crypto
import Foundation
// "BEGIN PRIVATE KEY" (PKCS#8) or "BEGIN EC PRIVATE KEY" (SEC 1)
let privateKey = try P256.Signing.PrivateKey(pemRepresentation: String(contentsOfFile: "key.pem", encoding: .utf8))
let message = try Data(contentsOf: URL(fileURLWithPath: "message.txt"))
let signature = try privateKey.signature(for: message) // SHA-256, then ECDSA with a fresh nonce
try signature.derRepresentation.write(to: URL(fileURLWithPath: "sig.der")) // for OpenSSL, Java, Go
try signature.rawRepresentation.write(to: URL(fileURLWithPath: "sig.raw")) // r‖s for JWS, WebCrypto
print("DER: \(signature.derRepresentation.count) bytes, raw: \(signature.rawRepresentation.count) bytes")
print("base64 (DER): \(signature.derRepresentation.base64EncodedString())")

How it works

  1. P256.Signing.PrivateKey(pemRepresentation:) reads PKCS#8 (BEGIN PRIVATE KEY) and SEC 1 (BEGIN EC PRIVATE KEY) files.
  2. signature(for:) hashes the data with SHA-256 and signs; passing a SHA256Digest instead signs a digest you computed yourself.
  3. derRepresentation is ASN.1 DER — the run verifies it with openssl dgst -verify — and rawRepresentation is the fixed 64-byte r‖s.
  4. base64EncodedString() on the DER bytes is the form to paste into JSON, HTTP headers or the signature verifier.

Gotchas

  • Every call returns different bytes: CryptoKit randomizes the nonce, and it offers no RFC 6979 deterministic mode. Tests must verify signatures rather than compare them.
  • CryptoKit does not normalize s, so about half the signatures are high-S. Generic verifiers accept them; Bitcoin-style ones (and k256 in Rust) reject them.
  • On Apple platforms, a SecureEnclave.P256.Signing.PrivateKey signs the same way but has no exportable PEM — its dataRepresentation is an opaque handle valid only on that device.
  • Passing message.utf8 of a String and the bytes read from a file can differ (a trailing newline added by an editor is enough). Sign exactly the bytes the verifier will hash.

Related recipes