Profile
Back to NewsBack
GitHub Trending 7 min
Reader Mode
21-DOT-DEV/swift-secp256k1: Swift cryptography library for Bitcoin and Nostr. ECDSA, Schnorr, ECDH, and zero-knowledge proofs on secp256k1. Apple platforms and Linux.

21-DOT-DEV/swift-secp256k1: Swift cryptography library for Bitcoin and Nostr. ECDSA, Schnorr, ECDH, and zero-knowledge proofs on secp256k1. Apple platforms and Linux.

6 hours ago

Latest Release</a> MIT License</a> Swift Versions</a> Platforms</a>

🔐 swift-secp256k1

Previously known as GigaBitcoin/secp256k1.swift (module renamed to P256K).

Swift cryptography library for Bitcoin and Nostr. ECDSA, Schnorr Signatures, Elliptic Curve Diffie-Hellman (ECDH), and MuSig2 multisignatures on secp256k1, Bitcoin's elliptic curve. The P256K module wraps libsecp256k1 from Bitcoin Core.

🌐 Project page · 📚 Documentation

[!IMPORTANT]
secp256k1 is not Apple's P256 curve. CryptoKit's P256 is NIST secp256r1 (the r is for random); this library's P256K is secp256k1 (the k is for Koblitz). Keys and signatures don't interoperate between the two. CryptoSwift and Apple's CommonCrypto also do not implement secp256k1. See Why CryptoKit's P256 can't sign Bitcoin or Nostr.

Contents

Features

  • Provide lightweight ECDSA & Schnorr Signatures functionality
  • Support simple and advanced usage, including BIP-327 and BIP-340
  • Expose libsecp256k1 bindings for full control of the implementation
  • Offer a familiar API design inspired by Swift Crypto
  • Maintain automatic updates for Swift and libsecp256k1
  • Ensure availability for Linux and Apple platform ecosystems

Installation

Add swift-secp256k1 to your Package.swift:

.package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", .upToNextMinor(from: "0.24.0")),

SwiftPM's from: requirement resolves forward through every 0.x release below 1.0.0, which would auto-update into breaking changes — while the package is pre-1.0, the minor version is the stability boundary, so .upToNextMinor still picks up patch releases within the minor. This example moves to 0.25.0 when that tag is cut.

Then include P256K as a target dependency:

.target(name: "<target>", dependencies: [
    .product(name: "P256K", package: "swift-secp256k1")
]),

Using CocoaPods or evaluating in a playground first? See Getting Started.

Package Traits

This package uses SE-0450 Package Traits (Swift 6.1+) for module selection. The defaults — ecdh, musig, recovery, schnorrsig — cover most use cases. To opt into other modules (ellswift, uint256), see Choosing modules with package traits in Getting Started.

Swift Versions

| swift-secp256k1 | Minimum Swift Version | Minimum Xcode Version | |-----------------------|-----------------------|-----------------------| | 0.1.0 ..< 0.4.0 | 5.0 | 10.2 | | 0.4.0 ..< 0.5.0 | 5.1 | 11.0 | | 0.5.0 ..< 0.8.0 | 5.5 | 13.0 | | 0.8.0 ..< 0.14.0 | 5.6 | 13.3 | | 0.14.0 ..< 0.18.0 | 5.8 | 14.3 | | 0.18.0 ..< 0.22.0 | 6.0 | 16.0 | | 0.22.0 ... | 6.1 | 16.3 |

Prebuilt XCFramework releases (CocoaPods and manual downloads) built on Xcode 27+ require Swift 6.3 / Xcode 26.4+ to consume: the emitted .swiftinterface files use Module::Type selectors that older compilers can't parse. Source (SwiftPM) builds are unaffected.

Usage Examples

ECDSA

import P256K

// Private key let privateBytes = try! "14E4A74438858920D8A35FB2D88677580B6A2EE9BE4E711AE34EC6B396D87B5C".bytes let privateKey = try! P256K.Signing.PrivateKey(dataRepresentation: privateBytes)

// Public key print(String(bytes: privateKey.publicKey.dataRepresentation))

// ECDSA signature let messageData = "We're all Satoshi.".data(using: .utf8)! let signature = privateKey.signature(for: messageData)

// DER signature print(signature.derRepresentation.base64EncodedString())

→ Full API: docs.21.dev/documentation/p256k/p256k/signing

Schnorr

// Schnorr signing takes a 32-byte message — hash longer inputs first
let privateKey = try! P256K.Schnorr.PrivateKey()

// Caller-supplied 32-byte auxiliary randomness for BIP-340 nonce // derivation — omit the parameter to draw fresh randomness internally let auxRand = try! "C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906".bytes let messageDigest = try! "7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C".bytes

let signature = try! privateKey.signature(message: messageDigest, auxiliaryRand: auxRand)

// Arbitrary-length messages sign through signature(customMessage:…) instead

→ Full API: docs.21.dev/documentation/p256k/p256k/schnorr

Tweak

let privateKey = try! P256K.Signing.PrivateKey()

// Adding a tweak to the private key and public key let tweak = try! "5f0da318c6e02f653a789950e55756ade9f194e1ec228d7f368de1bd821322b6".bytes let tweakedPrivateKey = try! privateKey.add(tweak) let tweakedPublicKeyKey = try! privateKey.publicKey.add(tweak)

→ Full API: docs.21.dev/documentation/p256k/tweakingkeys

Elliptic Curve Diffie Hellman

let privateKey = try! P256K.KeyAgreement.PrivateKey()
let publicKey = try! P256K.KeyAgreement.PrivateKey().publicKey

// Create a compressed shared secret with a private key from only a public key let sharedSecret = privateKey.sharedSecretFromKeyAgreement(with: publicKey, format: .compressed)

// By default, libsecp256k1 hashes the x-coordinate with version information. let symmetricKey = SHA256.hash(data: sharedSecret.bytes)

→ Full guide: docs.21.dev/documentation/p256k/ellipticcurvediffiehellman

Silent Payments Scheme

let privateSign1 = try! P256K.Signing.PrivateKey()
let privateSign2 = try! P256K.Signing.PrivateKey()

let privateKey1 = try! P256K.KeyAgreement.PrivateKey(dataRepresentation: privateSign1.dataRepresentation) let privateKey2 = try! P256K.KeyAgreement.PrivateKey(dataRepresentation: privateSign2.dataRepresentation)

let sharedSecret1 = privateKey1.sharedSecretFromKeyAgreement(with: privateKey2.publicKey) let sharedSecret2 = privateKey2.sharedSecretFromKeyAgreement(with: privateKey1.publicKey)

let symmetricKey1 = SHA256.hash(data: sharedSecret1.bytes) let symmetricKey2 = SHA256.hash(data: sharedSecret2.bytes)

let sharedSecretSign1 = try! P256K.Signing.PrivateKey(dataRepresentation: symmetricKey1.bytes) let sharedSecretSign2 = try! P256K.Signing.PrivateKey(dataRepresentation: symmetricKey2.bytes)

// Spendable Silent Payment private key let privateTweak1 = try! sharedSecretSign1.add(privateSign1.publicKey.xonly.bytes) let publicTweak2 = try! sharedSecretSign2.publicKey.add(privateSign1.publicKey.xonly.bytes)

let schnorrPrivate = try! P256K.Schnorr.PrivateKey(dataRepresentation: sharedSecretSign2.dataRepresentation) // Payable Silent Payment public key let xonlyTweak2 = try! schnorrPrivate.xonly.add(privateSign1.publicKey.xonly.bytes)

→ Full guide: docs.21.dev/documentation/p256k/silentpayments — BIP-352 walkthrough with sender derivation, receiver scanning, scan/spend key separation, and tagged-hash specifics.

Recovery

let privateKey = try! P256K.Recovery.PrivateKey()
let messageData = "We're all Satoshi.".data(using: .utf8)!

// Create a recoverable ECDSA signature let recoverySignature = privateKey.signature(for: messageData)

// Recover an ECDSA public key from a signature let publicKey = try! P256K.Recovery.PublicKey(messageData, signature: recoverySignature)

// Convert a recoverable signature into a normal signature let signature = recoverySignature.normalize

→ Full API: docs.21.dev/documentation/p256k/p256k/recovery

Combine Public Keys

let privateKey = try! P256K.Signing.PrivateKey()
let publicKey = try! P256K.Signing.PrivateKey().publicKey

// The Combine API arguments are an array of PublicKey objects and an optional format publicKey.combine([privateKey.publicKey], format: .uncompressed)

→ Full API: docs.21.dev/documentation/p256k/p256k/signing/publickey

PEM Key Format

let privateKeyString = """
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBXwHPDpec6b07GeLbnwetT0dvWzp0nV3MR+4pPKXIc7oAcGBSuBBAAK
oUQDQgAEt2uDn+2GqqYs/fmkBr5+rCQ3oiFSIJMAcjHIrTDS6HEELgguOatmFBOp
2wU4P2TAl/0Ihiq+nMkrAIV69m2W8g==
-----END EC PRIVATE KEY-----
"""

// Import keys generated from OpenSSL let privateKey = try! P256K.Signing.PrivateKey(pemRepresentation: privateKeyString)

→ Full API: docs.21.dev/documentation/p256k/serializingkeys

MuSig2

// Initialize private keys for two signers
let firstPrivateKey = try! P256K.Schnorr.PrivateKey()
let secondPrivateKey = try! P256K.Schnorr.PrivateKey()

// Aggregate the public keys using MuSig let aggregateKey = try! P256K.MuSig.aggregate([firstPrivateKey.publicKey, secondPrivateKey.publicKey])

// Message to be signed let message = "Vires in Numeris.".data(using: .utf8)! let messageHash = SHA256.hash(data: message)

// Generate nonces for each signer let firstNonce = try! P256K.MuSig.Nonce.generate( secretKey: firstPrivateKey, publicKey: firstPrivateKey.publicKey, msg32: Array(messageHash) )

let secondNonce = try! P256K.MuSig.Nonce.generate( secretKey: secondPrivateKey, publicKey: secondPrivateKey.publicKey, msg32: Array(messageHash) )

// Aggregate nonces let aggregateNonce = try! P256K.MuSig.Nonce(aggregating: [firstNonce.pubnonce, secondNonce.pubnonce])

// Create partial signatures let firstPartialSignature = try! firstPrivateKey.partialSignature( for: messageHash, pubnonce: firstNonce.pubnonce, secureNonce: firstNonce.secnonce, publicNonceAggregate: aggregateNonce, publicKeyAggregate: aggregateKey )

let secondPartialSignature = try! secondPrivateKey.partialSignature( for: messageHash, pubnonce: secondNonce.pubnonce, secureNonce: secondNonce.secnonce, publicNonceAggregate: aggregateNonce, publicKeyAggregate: aggregateKey )

// Aggregate partial signatures into a verified full signature let aggregateSignature = try! P256K.MuSig.aggregateSignatures( [firstPartialSignature, secondPartialSignature], for: messageHash, publicNonceAggregate: aggregateNonce, publicKeyAggregate: aggregateKey )

// Verify an individual partial signature against the declared session let isValid = try! aggregateKey.isValidSignature( firstPartialSignature, publicKey: firstPrivateKey.publicKey, nonce: firstNonce.pubnonce, for: messageHash, publicNonceAggregate: aggregateNonce )

print("Is valid MuSig signature: \(isValid)")

→ Full API: docs.21.dev/documentation/p256k/musig2multisignatures

Security

For information on reporting security vulnerabilities, see SECURITY.md.

Contributing

Contributions are welcome. Please read the 21-DOT-DEV contributing guidelines for general workflow, and AGENTS.md for project-specific code style (SwiftFormat, SwiftLint, Lefthook) and AI-assisted development guidance.

License

This project is released under the MIT License. See LICENSE for details.

Chat with me