Skip to content
ecdsa.com

Convert DER ⇄ raw · Swift

How to convert an ECDSA signature between DER and raw in Swift

To convert an ECDSA signature between DER and raw in Swift, no byte manipulation is needed: CryptoKit's ECDSASignature (swift-crypto on Linux) is built from either encoding and exposes both as properties. Two one-line functions cover the direction from OpenSSL to JWS and back, including the sign-byte and padding rules.

Tested with swift-crypto 5.0.3 (Swift 6.4.1 on Ubuntu 24.04 (BoringSSL backend)).

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: "convert", dependencies: [.product(name: "Crypto", package: "swift-crypto")]),
]
)
Sources/convert/main.swift — DER ⇄ r‖s through P256.Signing.ECDSASignature
import Crypto
import Foundation
/// DER (OpenSSL, Java, Go) to the fixed 64-byte r‖s of JWS and WebCrypto, for P-256.
func derToRaw(_ der: Data) throws -> Data {
try P256.Signing.ECDSASignature(derRepresentation: der).rawRepresentation
}
/// r‖s back to minimal DER.
func rawToDer(_ raw: Data) throws -> Data {
try P256.Signing.ECDSASignature(rawRepresentation: raw).derRepresentation
}
func hex(_ data: Data) -> String { data.map { String(format: "%02x", $0) }.joined() }
func bytes(_ hex: String) -> Data {
Data(stride(from: 0, to: hex.count, by: 2).map { offset in
let start = hex.index(hex.startIndex, offsetBy: offset)
return UInt8(hex[start..<hex.index(start, offsetBy: 2)], radix: 16)!
})
}
let der = bytes("30450221009f4118b059a34a2f344c39a1d085d93bab560402938281dc1cb2f3dfb8eee58602200bd54b34a72042551c3316e10f461a1d69036ba4f0d72d95d42465340b56def8")
let raw = try derToRaw(der)
print("raw: \(hex(raw))")
print("round trip: \(try rawToDer(raw) == der)")
print("JWS form: \(raw.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: ""))")

How it works

  1. ECDSASignature(derRepresentation:) parses DER and rawRepresentation returns the 64-byte r‖s, each half left-padded to 32 bytes.
  2. ECDSASignature(rawRepresentation:) takes exactly 64 bytes and derRepresentation returns minimal DER, with a 0x00 sign byte only where the top bit is set.
  3. Both constructors validate the input, so a converted signature is at least structurally a P-256 signature; the run converts two real signatures with a 31-byte r and a 31-byte s.
  4. For JWS, base64-encode the raw bytes and switch to the URL-safe alphabet without padding.

Gotchas

  • Errors are terse: raw bytes given to the DER constructor throw invalidFieldIdentifier, DER given to the raw constructor throws incorrectParameterSize. Both mean "wrong format", not a broken signature.
  • The type fixes the curve. A P-384 signature needs P384.Signing.ECDSASignature (96-byte raw), P-521 P521.Signing.ECDSASignature (132-byte raw).
  • Data(base64Encoded:) rejects base64url and unpadded input, so a JWS signature segment must be translated (- → +, _ → /, padding restored) before it can be decoded.
  • Conversion keeps s unchanged. CryptoKit signatures are high-S about half the time; normalize separately if the consumer demands low-S.

Related recipes