Swift wrapper around libsecp256k1 with API's like CryptoKit.
Safer than K2
K1 is Swift wrapper around libsecp256k1 (bitcoin-core/secp256k1), offering ECDSA, Schnorr (BIP340) and ECDH features.
Read full documentation here on SwiftPackageIndex.
The API of K1 maps almost 1:1 with Apple’s CryptoKit, vendoring a set of keypairs, one per feature. E.g. in CryptoKit you have Curve25519.KeyAgreement.PrivateKey
and Curve25519.KeyAgreement.PublicKey
which are seperate for Curve25519.Signing.PrivateKey
and Curve25519.Signing.PublicKey
.
Just like that K1 vendors these key pairs:
K1.KeyAgreement.PrivateKey
/ K1.KeyAgreement.PublicKey
for key agreement (ECDH)K1.Schnorr.PrivateKey
/ K1.Schnorr.PublicKey
for sign / verify methods using Schnorr signature schemeK1.ECDSAWithKeyRecovery.PrivateKey
/ K1.ECDSAWithKeyRecovery.PublicKey
for sign / verify methods using ECDSA (producing/validating signature where public key is recoverable)K1.ECDSA.PrivateKey
/ K1.ECDSA.PublicKey
for sign / verify methods using ECDSA (producing/validating signature where public key is not recoverable)Just like you can convert between e.g. Curve25519.KeyAgreement.PrivateKey
and Curve25519.Signing.PrivateKey
back and forth using any of the initializers and serializer, you can convert between all PrivateKeys and all PublicKeys of all features in K1.
All keys can be serialized using these computed properties:
{
var rawRepresentation: Data { get }
var derRepresentation: Data { get }
var pemRepresentation: String { get }
var x963Representation: Data { get }
}
All keys can be deserialize using these initializer:
{
init(rawRepresentation: some ContiguousBytes) throws
init(derRepresentation: some RandomAccessCollection<UInt8>) throws
init(pemRepresentation: String) throws
init(x963Representation: some ContiguousBytes) throws
}
Furthermore, all PrivateKey’s have these additional APIs:
{
init()
associatedtype PublicKey
var publicKey: PublicKey { get }
}
Furthermore, all PublicKeys’s have these additional APIs:
{
init(compressedRepresentation: some ContiguousBytes) throws
var compressedRepresentation: Data { get }
}
There exists two set of ECDSA key pairs:
K1.ECDSAWithKeyRecovery.PrivateKey
and K1.ECDSAWithKeyRecovery.PublicKey
K1.ECDSA.PrivateKey
and K1.ECDSA.PublicKey
For each private key there exists two different signature:for:options
(one taking hashed data and taking Digest
as argument) methods and one signature:forUnhashed:options
.
The option
is a K1.ECDSA.SigningOptions
struct, which by default specifies RFC6979
deterministic signing, as per Bitcoin standard, however, you can change to use secure random nonce instead.
let alice = K1.ECDSA.PrivateKey()
let hashedMessage: Data = // from somewhere
let signature = try alice.signature(for: hashedMessage)
let message: Data = // from somewhere
let digest = SHA256.hash(data: message)
let signature = try alice.signature(for: digest)
The forUnhashed
will SHA256
hash the message and then sign it.
let message: Data = // from somewhere
let signature = try alice.signature(forUnhashed: message)
let hashedMessage: Data = // from somewhere
let publicKey: K1.ECDSA.PublicKey = alice.publcKey
let signature: K1.ECDSA.Signature // from above
assert(
publicKey.isValidSignature(signature, hashed: hashedMessage)
) // PASS
let message: Data = // from somewhere
let digest = SHA256.hash(data: message)
let signature: K1.ECDSA.Signature // from above
assert(
publicKey.isValidSignature(signature, digest: digest)
) // PASS
let message: Data = // from somewhere
let signature: K1.ECDSA.Signature // from above
assert(
publicKey.isValidSignature(signature, unhashed: message)
) // PASS
All signing and validation APIs are identical to the NonRecoverable
namespace.
let alice = K1.ECDSA.PrivateKey()
let message: Data = // from somewhere
let digest = SHA256.hash(data: message)
let signature: K1.ECDSAWithKeyRecovery.Signature = try alice.signature(for: digest)
let publicKey: K1.ECDSAWithKeyRecovery.PublicKey = alice.publicKey
assert(
publicKey.isValidSignature(signature, digest: digest)
) // PASS
let alice = K1.Schnorr.PrivateKey()
let signature = try alice.signature(forUnhashed: message)
There exists other sign variants, signature:for:options
(hashed data) and signature:for:options
(Digest
) if you already have a hashed message. All three variants takes a K1.Schnorr.SigningOptions
struct where you can pass auxiliaryRandomData
to be signed.
let publicKey: K1.Schnorr.PublicKey = alice.publicKey
assert(publicKey.isValidSignature(signature, unhashed: message)) // PASS
Or alternatively isValidSignature:digest
or isValidSignature:hashed
.
The Schnorr signature implementation is BIP340, since we use libsecp256k1 which only provides the BIP340 Schnorr scheme.
It is worth noting that some Schnorr implementations are incompatible with BIP340 and thus this library, e.g. Zilliqa’s (kudelski report, libsecp256k1 proposal, Twitter thread).
This library vendors three different EC Diffie-Hellman (ECDH) key exchange functions:
ASN1 x9.63
- No hash, return only the X
coordinate of the point - sharedSecretFromKeyAgreement:with -> SharedSecret
libsecp256k1
- SHA-256 hash the compressed point - ecdh:with -> SharedSecret
ecdhPoint -> Data
let alice = try K1.KeyAgreement.PrivateKey()
let bob = try K1.KeyAgreement.PrivateKey()
ASN1 x9.63
ECDHReturning only the X
coordinate of the point, following ANSI X9.63 standards, embedded in a CryptoKit.SharedSecret
, which is useful since you can use CryptoKit
key derivation functions on this SharedSecret, e.g. x963DerivedSymmetricKey
or hkdfDerivedSymmetricKey
.
You can retrieve the X
coordinate as raw data using withUnsafeBytes
if you need to.
let ab: CryptoKit.SharedSecret = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey)
let ba: CryptoKit.SharedSecret = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey)
assert(ab == ba) // pass
ab.withUnsafeBytes {
assert(Data($0).count == 32) // pass
}
libsecp256k1
ECDHUsing libsecp256k1
default behaviour, returning a SHA-256 hash of the compressed point, embedded in a CryptoKit.SharedSecret
, which is useful since you can use CryptoKit
key derivation functions.
let ab: CryptoKit.SharedSecret = try alice.ecdh(with: bob.publicKey)
let ba: CryptoKit.SharedSecret = try bob.ecdh(with: alice.publicKey)
assert(ab == ba) // pass
ab.withUnsafeBytes {
assert(Data($0).count == 32) // pass
}
Returns an entire uncompressed EC point, without hashing it. Might be useful if you wanna construct your own cryptographic functions, e.g. some custom ECIES.
let ab: Data = try alice.ecdhPoint(with: bob.publicKey)
let ba: Data = try bob.ecdhPoint(with: alice.publicKey)
assert(ab == ba) // pass
assert(ab.count == 65) // pass
K1
is a Swift wrapper around libsecp256k1, so this library would not exist without the Bitcoin Core developers. Massive thank you for a wonderful library! I’ve included it as a submodule, without any changes to the code, i.e. with copyright headers in files intact.
K1
uses some code from swift-crypto
, which has been copied over with relevant copyright header. Since swift-crypto
is licensed under Apache, so is this library.
Stand in root and run
./scripts/build.sh
To clone the dependency libsecp256k1, using commit 427bc3cdcfbc74778070494daab1ae5108c71368 (semver 0.3.0)
gyb
Some of the files in this project are autogenerated (metaprogramming) using the Swift Utils tools called gyb (“generate your boilerplate”). gyb
is included in ./scripts/gyb
.
gyb
will generate some Foobar.swift
Swift file from some Foobar.swift.gyb
template file. You should not edit Foobar.swift
directly, since all manual edits in that generated file will be overwritten the next time gyb
is run.
You run gyb
for a single file like so:
./scripts/gyb --line-directive "" Sources/Foobar.swift.gyb -o Sources/Foobar.swift
More conveniently you can run the bash script ./scripts/generate_boilerplate_files_with_gyb.sh
to generate all Swift files from their corresponding gyb template.
If you add a new .gyb
file, you should append a // MARK: - Generated file, do NOT edit
warning inside it, e.g.
// MARK: - Generated file, do NOT edit
// any edits of this file WILL be overwritten and thus discarded
// see section `gyb` in `README` for details.
libsecp256k1
, ⚠️ possibly unsafe, ✅ Schnorr support)libsecp256k1
, ❌ No Schnorr)libsecp256k1
, ❌ No Schnorr)