Digital Signature — Swift source
Sign messages with Ed25519 or ECDSA and verify signatures. Prove authorship without revealing your private key.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// digital-signature — Ed25519 / ECDSA P-256 message signing and verification.
//
// Language: Swift 5.9+ (Foundation + CryptoKit)
// Ported from src/lib/digital-signature.ts
// display source — part of CosmoDev's polyglot tool pages
//
// Key encoding: keys are exchanged as hex of the standard DER structures -
// SPKI (SubjectPublicKeyInfo) for public keys, PKCS8 for private keys - so
// they interoperate with OpenSSL, SSH, JOSE tooling and the TS reference's
// Web Crypto keys. CryptoKit speaks raw key bytes, so this port carries the
// two fixed DER wrappers Web Crypto emits and unwraps them on import.
// Signatures are hex of the raw signature bytes: 64 bytes for both algorithms
// (Ed25519 R||S; ECDSA P-256 in IEEE P1363 r||s layout, NOT ASN.1 DER) —
// which is exactly CryptoKit's rawRepresentation. ECDSA signs the SHA-256
// digest of the message.
import Foundation
import CryptoKit
// MARK: - Types
enum SignAlgorithm: String, CaseIterable {
case ed25519
case ecdsaP256 = "ecdsa-p256"
/// Human label used in error messages ("Ed25519" / "ECDSA P-256").
var label: String { self == .ed25519 ? "Ed25519" : "ECDSA P-256" }
}
/// A signing key pair, both keys hex-encoded DER (KeyPair in the TS reference).
struct SigningKeyPair: Equatable {
/// Hex-encoded SPKI (SubjectPublicKeyInfo) public key. Safe to share.
let publicKey: String
/// Hex-encoded PKCS8 private key. Keep it secret - it IS the identity.
let privateKey: String
}
enum SignError: Error, LocalizedError {
case unknownAlgorithm
case unsupportedAlgorithm(String)
case keyGenFailed(String)
case emptyMessage
case invalidHex(String)
case invalidPrivateKey(String)
case invalidPublicKey(String)
var errorDescription: String? {
switch self {
case .unknownAlgorithm:
return "Unknown algorithm. Use \"ed25519\" or \"ecdsa-p256\"."
case .unsupportedAlgorithm(let a):
return a
case .keyGenFailed(let a):
return a
case .emptyMessage:
return "Message must not be empty."
case .invalidHex(let what):
return "\(what) must be a non-empty hex string (pairs of 0-9 / a-f digits)."
case .invalidPrivateKey(let a):
return "Invalid private key for \(a) (expected hex PKCS8)."
case .invalidPublicKey(let a):
return "Invalid public key for \(a) (expected hex SPKI)."
}
}
}
// MARK: - Hex codec
func hexToBytes(_ hex: String, _ what: String) throws -> [UInt8] {
let chars = Array(hex)
guard !chars.isEmpty, chars.count % 2 == 0,
chars.allSatisfy({ c in
("0"..."9").contains(c) || ("a"..."f").contains(c) || ("A"..."F").contains(c)
})
else { throw SignError.invalidHex(what) }
var out = [UInt8]()
out.reserveCapacity(chars.count / 2)
var i = 0
while i < chars.count {
let byte = UInt32(String(chars[i]), radix: 16)! * 16 + UInt32(String(chars[i + 1]), radix: 16)!
out.append(UInt8(byte))
i += 2
}
return out
}
func toHex(_ bytes: [UInt8]) -> String {
bytes.map { String(format: "%02x", $0) }.joined()
}
// MARK: - DER wrappers (the exact SPKI / PKCS8 prefixes Web Crypto emits)
let ED25519_SPKI_PREFIX: [UInt8] = [0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00]
let ED25519_PKCS8_PREFIX: [UInt8] = [0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]
let P256_SPKI_PREFIX: [UInt8] = [
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00]
let P256_PKCS8_PREFIX: [UInt8] = [
0x30, 0x81, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48,
0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01,
0x07, 0x04, 0x30, 0x30, 0x02, 0x01, 0x01, 0x04, 0x20]
/// Strip `prefix` from `der`, throwing when it does not lead.
func unwrapDER(_ der: [UInt8], prefix: [UInt8]) -> [UInt8] {
guard der.count >= prefix.count, Array(der.prefix(prefix.count)) == prefix else { return [] }
return Array(der.dropFirst(prefix.count))
}
// MARK: - Key generation
/// Generate a fresh signing key pair, exported as hex SPKI / PKCS8 keys.
func generateKeyPair(algorithm: SignAlgorithm) throws -> SigningKeyPair {
switch algorithm {
case .ed25519:
let key = Curve25519.Signing.PrivateKey()
let spki = ED25519_SPKI_PREFIX + Array(key.publicKey.rawRepresentation)
// Web Crypto's PKCS8 Ed25519 export ends with the private scalar only.
let pkcs8 = ED25519_PKCS8_PREFIX + Array(key.rawRepresentation)
return SigningKeyPair(publicKey: toHex(spki), privateKey: toHex(pkcs8))
case .ecdsaP256:
let key = P256.Signing.PrivateKey()
// x963Representation of a P-256 public key is the uncompressed point
// (0x04 || X || Y) — what the SPKI suffix carries.
let spki = P256_SPKI_PREFIX + Array(key.publicKey.x963Representation)
// ...and of the private key, the raw scalar.
let pkcs8 = P256_PKCS8_PREFIX + Array(key.x963Representation)
return SigningKeyPair(publicKey: toHex(spki), privateKey: toHex(pkcs8))
}
}
// MARK: - Key import
func importEd25519PrivateKey(_ der: [UInt8]) throws -> Curve25519.Signing.PrivateKey {
// The scalar follows the 04 20 (OCTET STRING, 32 bytes) marker.
guard der.count == ED25519_PKCS8_PREFIX.count + 32,
Array(der.prefix(ED25519_PKCS8_PREFIX.count)) == ED25519_PKCS8_PREFIX
else { throw SignError.invalidPrivateKey("Ed25519") }
return try Curve25519.Signing.PrivateKey(
rawRepresentation: Data(der.dropFirst(ED25519_PKCS8_PREFIX.count)))
}
func importEd25519PublicKey(_ der: [UInt8]) throws -> Curve25519.Signing.PublicKey {
let raw = unwrapDER(der, prefix: ED25519_SPKI_PREFIX)
guard raw.count == 32 else { throw SignError.invalidPublicKey("Ed25519") }
return try Curve25519.Signing.PublicKey(rawRepresentation: Data(raw))
}
func importP256PrivateKey(_ der: [UInt8]) throws -> P256.Signing.PrivateKey {
let raw = unwrapDER(der, prefix: P256_PKCS8_PREFIX)
guard raw.count >= 32 else { throw SignError.invalidPrivateKey("ECDSA P-256") }
// The optional trailing [1] BIT STRING (public key) is ignored — the
// scalar alone fully determines the key.
return try P256.Signing.PrivateKey(rawRepresentation: Data(raw.prefix(32)))
}
func importP256PublicKey(_ der: [UInt8]) throws -> P256.Signing.PublicKey {
let raw = unwrapDER(der, prefix: P256_SPKI_PREFIX)
// The point is 0x04 || X || Y (65 bytes) — x963Representation.
guard raw.count == 65 else { throw SignError.invalidPublicKey("ECDSA P-256") }
return try P256.Signing.PublicKey(x963Representation: Data(raw))
}
// MARK: - Sign / verify
/// Sign `message` with a hex PKCS8 private key. Returns the hex signature.
func signMessage(_ message: String, privateKeyHex: String, algorithm: SignAlgorithm) throws -> String {
if message.isEmpty { throw SignError.emptyMessage }
let der = try hexToBytes(privateKeyHex, "Private key")
let data = Data(message.utf8)
switch algorithm {
case .ed25519:
let key = try importEd25519PrivateKey(der)
// Ed25519 signs the message itself; the signature IS R||S (64 bytes).
return toHex(Array(try key.signature(for: data)))
case .ecdsaP256:
let key = try importP256PrivateKey(der)
// P256.Signing signs SHA-256 internally; rawRepresentation is the
// IEEE P1363 r||s layout — the same as Web Crypto's output here.
let sig = try key.signature(for: data)
return toHex(Array(sig.rawRepresentation))
}
}
/// Verify `signatureHex` against `message` with a hex SPKI public key.
/// Returns false when the signature, message or key simply do not match;
/// throws only for malformed input (non-hex / wrong key type).
func verifySignature(message: String, signatureHex: String, publicKeyHex: String,
algorithm: SignAlgorithm) throws -> Bool {
let der = try hexToBytes(signatureHex, "Signature")
let keyDer = try hexToBytes(publicKeyHex, "Public key")
let data = Data(message.utf8)
switch algorithm {
case .ed25519:
let pub = try importEd25519PublicKey(keyDer)
return pub.isValidSignature(Data(der), for: data)
case .ecdsaP256:
let pub = try importP256PublicKey(keyDer)
guard let sig = try? P256.Signing.ECDSASignature(rawRepresentation: Data(der)) else {
return false
}
return pub.isValidSignature(sig, for: data)
}
}
Also available in 8 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →