Skip to content
/ K1 Public

Swift wrapper around libsecp256k1 with API's like CryptoKit.

License

Notifications You must be signed in to change notification settings

Sajjon/K1

K1 🏔

Safer than K2

K1 is Swift wrapper around libsecp256k1 (bitcoin-core/secp256k1), offering ECDSA, Schnorr (BIP340) and ECDH features.

Documentation

Read full documentation here on SwiftPackageIndex.

Quick overview

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 scheme
  • K1.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 }
}

ECDSA (Elliptic Curve Digital Signature Algorithm)

There exists two set of ECDSA key pairs:

  • A key pair for signatures from which you can recover the public key, specifically: K1.ECDSAWithKeyRecovery.PrivateKey and K1.ECDSAWithKeyRecovery.PublicKey
  • A key pair for signatures from which you can not recover the public key, specifically: 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.

NonRecoverable

Sign

let alice = K1.ECDSA.PrivateKey()
Hashed (Data)
let hashedMessage: Data = // from somewhere
let signature = try alice.signature(for: hashedMessage)
Digest
let message: Data = // from somewhere
let digest = SHA256.hash(data: message)
let signature = try alice.signature(for: digest)
Hash and Sign

The forUnhashed will SHA256 hash the message and then sign it.

let message: Data = // from somewhere
let signature = try alice.signature(forUnhashed: message)

Validate

Hashed (Data)
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
Digest
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
Hash and Validate
let message: Data = // from somewhere
let signature: K1.ECDSA.Signature // from above

assert(
    publicKey.isValidSignature(signature, unhashed: message)
) // PASS

Recoverable

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

Schnorr Signature Scheme

Sign

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.

Validate

let publicKey: K1.Schnorr.PublicKey = alice.publicKey
assert(publicKey.isValidSignature(signature, unhashed: message)) // PASS

Or alternatively isValidSignature:digest or isValidSignature:hashed.

Schnorr Scheme

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

ECDH

This library vendors three different EC Diffie-Hellman (ECDH) key exchange functions:

  1. ASN1 x9.63 - No hash, return only the X coordinate of the point - sharedSecretFromKeyAgreement:with -> SharedSecret
  2. libsecp256k1 - SHA-256 hash the compressed point - ecdh:with -> SharedSecret
  3. Custom - No hash, return point uncompressed - ecdhPoint -> Data
let alice = try K1.KeyAgreement.PrivateKey()
let bob = try K1.KeyAgreement.PrivateKey()

ASN1 x9.63 ECDH

Returning 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 ECDH

Using 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
}

Custom ECDH

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

Acknowledgements

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.

Development

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.

Alternatives