🔐 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]
secp256k1is not Apple'sP256curve. CryptoKit'sP256is NISTsecp256r1(theris for random); this library'sP256Kissecp256k1(thekis 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.