// swift-tools-version:6.2import 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")]), ]) import Cryptoimport 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
ECDSASignature(derRepresentation:)parses DER andrawRepresentationreturns the 64-byter‖s, each half left-padded to 32 bytes.ECDSASignature(rawRepresentation:)takes exactly 64 bytes andderRepresentationreturns minimal DER, with a0x00sign byte only where the top bit is set.- 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.
- 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 throwsincorrectParameterSize. 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-521P521.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
sunchanged. CryptoKit signatures are high-S about half the time; normalize separately if the consumer demands low-S.