swift-secp256k1

0.23.0

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

What's New

Docs & Benchmarks

2026-03-30T03:25:37Z

What's Changed

  • docs: update README with improved structure and clarity by @csjones in #1037
  • docs: add AGENTS.md files for AI-assisted development guidance by @csjones in #1038
  • Fix typo in AGENTS.md regarding ecosystems by @csjones in #1039
  • Reduce throws propagation and reorganize Shared sources by @csjones in #1034
  • Mark SPM build stubs as vendored in .gitattributes by @csjones in #1043
  • ci: update Docker registry path from finestructure to SwiftPackageIndex by @csjones in #1056
  • refactor: enable Swift upcoming features MemberImportVisibility and InternalImportsByDefault by @csjones in #1057
  • docs: add DocC documentation catalog with Getting Started guide and API reference by @csjones in #1060
  • docs: update CHANGELOG.md with unreleased changes for v0.23.0 by @csjones in #1061
  • feat: add UInt256 trait and benchmarks with modular arithmetic support by @csjones in #1062
  • ci: add include-hidden-files flag to benchmark baseline artifact upload by @csjones in #1063
  • chore: prepare 0.23.0 release by @csjones in #1064
  • ci: update checkout action to v6 and add dynamic main baseline run lookup by @csjones in #1065

Full Changelog: 0.22.0...0.23.0

Latest Release MIT License Swift Versions Platforms

๐Ÿ” swift-secp256k1

Swift cryptography library for Bitcoin and Nostr. ECDSA, Schnorr Signatures, Elliptic Curve Diffie-Hellman, and zero-knowledge proofs. Uses Swift's C interoperability with libsecp256k1.

๐ŸŒ Project page ยท ๐Ÿ“š Documentation

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

This package uses Swift Package Manager. To add it to your project:

Using Xcode

  1. Go to File > Add Packages...
  2. Enter the package URL: https://github.com/21-DOT-DEV/swift-secp256k1
  3. Select the desired version

Using Package.swift (Recommended)

Add the following to your Package.swift file:

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

Warning

This package is pre-1.0 (SemVer major version zero). The public API should not be considered stable and may change with any release. Pin a version using exact: to avoid unexpected breaking changes.

Then, include P256K as a dependency in your target:

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

Using CocoaPods (version history)

Add the following to your Podfile:

pod 'swift-secp256k1', '0.23.0'

Note

Swift Package Manager is the recommended way to add this package. CocoaPods support may be deprecated in a future release.

Try it out

Use Arena to try the package in a playground:

arena 21-DOT-DEV/swift-secp256k1

Package Traits

This package uses SE-0450 Package Traits (Swift 6.1+) to enable fine-grained module selection. By default, the following traits are enabled: ecdh, musig, recovery, and schnorrsig.

To use only specific modules, specify traits in your dependency:

.package(
    url: "https://github.com/21-DOT-DEV/swift-secp256k1",
    from: "0.23.0",
    traits: ["schnorrsig"]
),

To enable all ZKP modules, use the zkp bundle trait:

.package(
    url: "https://github.com/21-DOT-DEV/swift-secp256k1",
    from: "0.23.0",
    traits: ["zkp"]
),

To use the UInt256 and Int256 fixed-width integer types, enable the uint256 trait:

.package(
    url: "https://github.com/21-DOT-DEV/swift-secp256k1",
    from: "0.23.0",
    traits: ["uint256"]
),

Note

The uint256 trait is opt-in and not enabled by default. The UInt256 and Int256 types require macOS 15, iOS 18, macCatalyst 18, watchOS 11, tvOS 18, or visionOS 2 and later.

Available traits: ecdh, ellswift, musig, recovery, schnorrsig, uint256, bppp, ecdsaAdaptor, ecdsaS2C, generator, rangeproof, schnorrsigHalfagg, surjectionproof, whitelist, zkp.

Note

Xcode does not currently resolve SwiftPM package trait conditions for Swift settings. As a workaround, all optional modules are compiled when building in Xcode. Package traits are fully supported when building with swift build from the command line.

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

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())

Schnorr

// Strict BIP340 mode is disabled by default for Schnorr signatures with variable length messages
let privateKey = try! P256K.Schnorr.PrivateKey()

// Extra params for custom signing
var auxRand = try! "C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906".bytes
var messageDigest = try! "7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C".bytes

// API allows for signing variable length messages
let signature = try! privateKey.signature(message: &messageDigest, auxiliaryRand: &auxRand)

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)

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)

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)

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 = P256K.Recovery.PublicKey(messageData, signature: recoverySignature)

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

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)

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)

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 full signature
let aggregateSignature = try! P256K.MuSig.aggregateSignatures([firstPartialSignature, secondPartialSignature])

// Verify the aggregate signature
let isValid = aggregateKey.isValidSignature(
    firstPartialSignature,
    publicKey: firstPrivateKey.publicKey,
    nonce: firstNonce.pubnonce,
    for: messageHash
)

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

Security

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

Contributing

Contributions are welcome. Please read CONTRIBUTING.md for guidelines on how to get started. For AI-assisted development guidance, see AGENTS.md.

License

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

Description

  • Swift Tools 6.1.0
View More Packages from this Author

Dependencies

Last updated: Sun Apr 05 2026 03:43:37 GMT-0900 (Hawaii-Aleutian Daylight Time)