Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
secp256k1 key generation, signing, and verification (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
amika-sq authored Dec 19, 2023
1 parent 45e8928 commit 78d3e4f
Show file tree
Hide file tree
Showing 10 changed files with 874 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ let package = Package(
name: "tbDEXTests",
dependencies: ["tbDEX"],
resources: [
.copy("TestVectors/ed25519")
.copy("TestVectors/ed25519"),
.copy("TestVectors/secp256k1")
]
),
]
Expand Down
268 changes: 268 additions & 0 deletions Sources/tbDEX/crypto/Secp256k1.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import Foundation
import secp256k1

public enum Secp256k1 {

// MARK: - Constants

/// Uncompressed key leading byte that indicates both the X and Y coordinates are available directly within the key.
static let uncompressedKeyID: UInt8 = 0x04

/// Compressed key leading byte that indicates the Y coordinate is even.
static let compressedKeyEvenYID: UInt8 = 0x02

/// Compressed key leading byte that indicates the Y coordinate is odd.
static let compressedKeyOddYID: UInt8 = 0x03

/// Size of an uncompressed public key, in bytes.
///
/// An uncompressed key is represented with a leading 0x04 bytes,
/// followed by 32 bytes for the x-coordinate and 32 bytes for the y-coordinate.
static let uncompressedKeySize: Int = 65

/// Size of a compressed public key, in bytes.
///
/// A compressed key is represented with a leading 0x02 or 0x03 byte,
/// followed by 32 bytes for the x-coordinate.
static let compressedKeySize: Int = 33

/// Size of a private key, in bytes.
static let privateKeySize: Int = 32

// MARK: - Public Functions

/// Generates an Secp256k1 private key in JSON Web Key (JWK) format.
public static func generatePrivateKey() throws -> Jwk {
return try generatePrivateJwk(
privateKey:secp256k1.Signing.PrivateKey()
)
}

/// Derives the public key in JSON Web Key (JWK) format from a given Secp256k1 private key in JWK format.
public static func computePublicKey(privateKey: Jwk) throws -> Jwk {
guard let d = privateKey.d else {
throw Secpsecp256k1Error.invalidPrivateJwk
}

let privateKeyData = try d.decodeBase64Url()
let privateKey = try secp256k1.Signing.PrivateKey(dataRepresentation: privateKeyData)

return try generatePublicJwk(publicKey: privateKey.publicKey)
}

/// Converts raw Secp256k1 private key in bytes to its corresponding JSON Web Key (JWK) format.
public static func bytesToPrivateKey(_ bytes: Data) throws -> Jwk {
let privateKey = try secp256k1.Signing.PrivateKey(dataRepresentation: bytes)
return try generatePrivateJwk(privateKey: privateKey)
}


/// Converts a raw Secp256k1 public key in bytes to its corresponding JSON Web Key (JWK) format.
public static func bytesToPublicKey(_ bytes: Data) throws -> Jwk {
let publicKey = try secp256k1.Signing.PublicKey(
dataRepresentation: bytes,
format: bytes.isCompressed() ? .compressed : .uncompressed
)

return try generatePublicJwk(publicKey: publicKey)
}

/// Converts a Secp256k1 private key from JSON Web Key (JWK) format to a raw bytes.
public static func privateKeyToBytes(_ privateKey: Jwk) throws -> Data {
guard let d = privateKey.d else {
throw Secpsecp256k1Error.invalidPrivateJwk
}

return try d.decodeBase64Url()
}

/// Converts a Secp256k1 public key from JSON Web Key (JWK) format to a raw bytes.
public static func publicKeyToBytes(_ publicKey: Jwk) throws -> Data {
guard let x = publicKey.x,
let y = publicKey.y
else {
throw Secpsecp256k1Error.invalidPublicJwk
}

var data = Data()
data.append(Self.uncompressedKeyID)
data.append(contentsOf: try x.decodeBase64Url())
data.append(contentsOf: try y.decodeBase64Url())

guard data.count == Self.uncompressedKeySize else {
throw Secpsecp256k1Error.internalError(reason: "Public Key incorrect size: \(data.count)")
}

return data
}

/// Converts a Secp256k1 raw public key to its compressed form.
public static func compressPublicKey(publicKeyBytes: Data) throws -> Data {
guard publicKeyBytes.count == Self.uncompressedKeySize,
publicKeyBytes.first == Self.uncompressedKeyID
else {
throw Secpsecp256k1Error.internalError(reason: "Public key must be 65 bytes long an start with 0x04")
}

let xBytes = publicKeyBytes[1...32]
let yBytes = publicKeyBytes[33...64]

let prefix = if yBytes.last! % 2 == 0 {
Self.compressedKeyEvenYID
} else {
Self.compressedKeyOddYID
}

var data = Data()
data.append(prefix)
data.append(contentsOf: xBytes)
return data
}

/// Converts a Secp256k1 raw public key to its uncompressed form.
public static func decompressPublicKey(publicKeyBytes: Data) throws -> Data {
let format: secp256k1.Format = publicKeyBytes.count == Self.compressedKeySize ? .compressed : .uncompressed
let publicKey = try secp256k1.Signing.PublicKey(dataRepresentation: publicKeyBytes, format: format)
return publicKey.uncompressedBytes()
}

/// Generates an RFC6979-compliant ECDSA signature of given data using a Secp256k1 private key in JSON Web Key
/// (JWK) format.
public static func sign<D>(privateKey: Jwk, payload: D) throws -> Data where D: DataProtocol {
guard let d = privateKey.d else {
throw Secpsecp256k1Error.invalidPrivateJwk
}

let privateKeyData = try d.decodeBase64Url()
let privateKey = try secp256k1.Signing.PrivateKey(
dataRepresentation: privateKeyData,
format: privateKeyData.isCompressed() ? .compressed : .uncompressed
)
return try privateKey.signature(for: payload).dataRepresentation
}

/// Verifies an RFC6979-compliant ECDSA signature against given data and a Secp256k1 public key in JSON Web Key
/// (JWK) format.
public static func verify<S,D>(publicKey: Jwk, signature: S, signedPayload: D) throws -> Bool where S: DataProtocol, D: DataProtocol {
let publicKeyBytes = try publicKeyToBytes(publicKey)
let publicKey = try secp256k1.Signing.PublicKey(dataRepresentation: publicKeyBytes, format: .uncompressed)

let ecdsaSignature = try secp256k1.Signing.ECDSASignature(dataRepresentation: signature)
return publicKey.isValidSignature(ecdsaSignature, for: signedPayload)
}

// MARK: - Internal Functions

/// Computes the elliptic curve points (x and y coordinates) for a given a raw Secp256k1 key
static func getCurvePoints(keyBytes: Data) throws -> (Data, Data) {
var keyBytes = keyBytes

// If provided key bytes represent a private key, first compute the public key
if keyBytes.count == Self.privateKeySize {
let privateKey = try secp256k1.Signing.PrivateKey(dataRepresentation: keyBytes)
let publicKey = privateKey.publicKey
keyBytes = publicKey.dataRepresentation
}

let uncompresssedBytes = try Self.decompressPublicKey(publicKeyBytes: keyBytes)
let x = uncompresssedBytes[1...32]
let y = uncompresssedBytes[33...64]

return (x, y)
}

/// Validates a given raw Secp256k1 private key to ensure its compliance with the secp256k1 curve standards.
static func validatePrivateKey(privateKeyBytes: Data) -> Bool {
do {
let _ = try secp256k1.Signing.PrivateKey(dataRepresentation: privateKeyBytes)
return true
} catch {
return false
}
}

/// Validates a given raw Secp256k1 public key to confirm its mathematical correctness on the secp256k1 curve.
static func validatePublicKey(publicKeyBytes: Data) -> Bool {
do {
let format: secp256k1.Format = publicKeyBytes.count == Self.compressedKeySize ? .compressed : .uncompressed
let _ = try secp256k1.Signing.PublicKey(dataRepresentation: publicKeyBytes, format: format)
return true
} catch {
return false
}
}

// MARK: - Private Functions

private static func generatePrivateJwk(privateKey: secp256k1.Signing.PrivateKey) throws -> Jwk {
let (x, y) = try getCurvePoints(keyBytes: privateKey.dataRepresentation)

var jwk = Jwk(
keyType: .elliptic,
curve: .secp256k1,
d: privateKey.dataRepresentation.base64UrlEncodedString(),
x: x.base64UrlEncodedString(),
y: y.base64UrlEncodedString()
)

jwk.keyIdentifier = try jwk.thumbprint()

return jwk
}

private static func generatePublicJwk(publicKey: secp256k1.Signing.PublicKey) throws -> Jwk {
let (x, y) = try getCurvePoints(keyBytes: publicKey.dataRepresentation)

var jwk = Jwk(
keyType: .elliptic,
curve: .secp256k1,
x: x.base64UrlEncodedString(),
y: y.base64UrlEncodedString()
)

jwk.keyIdentifier = try jwk.thumbprint()

return jwk
}
}

public enum Secpsecp256k1Error: Error {
/// The private Jwk provide did not have the appropriate parameters set on it
case invalidPrivateJwk
/// The public Jwk provide did not have the appropriate parameters set on it
case invalidPublicJwk
/// Something internally went wrong, check `reason` for more information about the exact error
case internalError(reason: String)
}

// MARK: - Helper extensions

private extension Data {
func isCompressed() -> Bool {
return self.count == Secp256k1.compressedKeySize
}
}

private extension secp256k1.Signing.PublicKey {

/// Get the uncompressed bytes for a given public key.
///
/// With a compressed public key, there's no direct access to the y-coordinate for use within
/// a Jwk. To avoid doing manual computations along the curve to compute the y-coordinate, this
/// function offloads the work to the `secp256k1` library to compute it for us.
func uncompressedBytes() -> Data {
switch self.format {
case .uncompressed:
return self.dataRepresentation
case .compressed:
let targetFormat = secp256k1.Format.uncompressed
var keyLength = targetFormat.length
var key = self.rawRepresentation

let context = secp256k1.Context.rawRepresentation
var bytes = [UInt8](repeating: 0, count: keyLength)
secp256k1_ec_pubkey_serialize(context, &bytes, &keyLength, &key, targetFormat.rawValue)
return Data(bytes)
}
}
}
61 changes: 61 additions & 0 deletions Tests/tbDEXTests/TestVectors/secp256k1/bytes-to-private-key.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"description" : "Secp256k1 bytesToPrivateKey test vectors",
"vectors" : [
{
"description" : "converts noble ecdsa vector 1 to the expected private key",
"input" : {
"privateKeyBytes": "0000000000000000000000000000000000000000000000000000000000000001"
},
"output": {
"crv" : "secp256k1",
"d" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE",
"kid" : "2JF8vg9etJzjFwZwmkvhBLLZ0bfMVVOPivYR5lFtcec",
"kty" : "EC",
"x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
"y": "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg"
}
},
{
"description" : "converts noble ecdsa vector 2 to the expected private key",
"input" : {
"privateKeyBytes": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140"
},
"output": {
"crv" : "secp256k1",
"d" : "_____________________rqu3OavSKA7v9JejNA2QUA",
"kid" : "A5kvmZN8g_rnvmmIfgTaV8S8KUnA6plB3cCmCMUZPyQ",
"kty" : "EC",
"x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
"y": "t8UliNlcO5qiWwQD8e73VwLoS7dZeqvmY7gvbwTvJ3c"
}
},
{
"description" : "converts noble ecdsa vector 3 to the expected private key",
"input" : {
"privateKeyBytes": "00000000000000000000000000007246174ab1e92e9149c6e446fe194d072637"
},
"output": {
"crv" : "secp256k1",
"d" : "AAAAAAAAAAAAAAAAAAByRhdKsekukUnG5Eb-GU0HJjc",
"kid" : "IJxfsScP6p4G8L9Yf8mYI8WIS87ulINKi0oUbS670KQ",
"kty" : "EC",
"x" : "tILZZWpOmaFc_atei0h8sHIG3xy4OvsHbG97qJCkNoE",
"y": "g-vgQCnVwDMAxAHbZapqnb1Hm05gvdGQoZzltVMgE9I"
}
},
{
"description" : "converts private key bytes to the expected private key JWK",
"input" : {
"privateKeyBytes": "bb42227e72b0f2607c7810a814b6796da369e9dc22b85b739e0eb924b770cd51"
},
"output": {
"crv" : "secp256k1",
"d" : "u0IifnKw8mB8eBCoFLZ5baNp6dwiuFtzng65JLdwzVE",
"kid" : "ikH9Jgh0U90gMJQ1txhlaST6VOdP_ygPJNN0JPaAwTI",
"kty" : "EC",
"x" : "9zVCTdMxpNyy3W1l0VfLdkpQyFdkXvDA0Jpx3TTn1og",
"y": "lNsXBQzhcrrGrf9XZYri_LLN1Bye4PdfTaoHbC6PIGI"
}
}
]
}
57 changes: 57 additions & 0 deletions Tests/tbDEXTests/TestVectors/secp256k1/bytes-to-public-key.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"description" : "Secp256k1 bytesToPublicKey test vectors",
"vectors" : [
{
"description" : "converts wycheproof vector 1 to the expected public key",
"input" : {
"publicKeyBytes": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"
},
"output": {
"crv" : "secp256k1",
"kid" : "2JF8vg9etJzjFwZwmkvhBLLZ0bfMVVOPivYR5lFtcec",
"kty" : "EC",
"x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
"y": "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg"
}
},
{
"description" : "converts wycheproof vector 2 to the expected public key",
"input" : {
"publicKeyBytes": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798b7c52588d95c3b9aa25b0403f1eef75702e84bb7597aabe663b82f6f04ef2777"
},
"output": {
"crv" : "secp256k1",
"kid" : "A5kvmZN8g_rnvmmIfgTaV8S8KUnA6plB3cCmCMUZPyQ",
"kty" : "EC",
"x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
"y": "t8UliNlcO5qiWwQD8e73VwLoS7dZeqvmY7gvbwTvJ3c"
}
},
{
"description" : "converts wycheproof vector 3 to the expected public key",
"input" : {
"publicKeyBytes": "04b482d9656a4e99a15cfdab5e8b487cb07206df1cb83afb076c6f7ba890a4368183ebe04029d5c03300c401db65aa6a9dbd479b4e60bdd190a19ce5b5532013d2"
},
"output": {
"crv" : "secp256k1",
"kid" : "IJxfsScP6p4G8L9Yf8mYI8WIS87ulINKi0oUbS670KQ",
"kty" : "EC",
"x" : "tILZZWpOmaFc_atei0h8sHIG3xy4OvsHbG97qJCkNoE",
"y": "g-vgQCnVwDMAxAHbZapqnb1Hm05gvdGQoZzltVMgE9I"
}
},
{
"description" : "converts public key bytes to the expected public key JWK",
"input" : {
"publicKeyBytes": "04f735424dd331a4dcb2dd6d65d157cb764a50c857645ef0c0d09a71dd34e7d68894db17050ce172bac6adff57658ae2fcb2cdd41c9ee0f75f4daa076c2e8f2062"
},
"output": {
"crv" : "secp256k1",
"kid" : "ikH9Jgh0U90gMJQ1txhlaST6VOdP_ygPJNN0JPaAwTI",
"kty" : "EC",
"x" : "9zVCTdMxpNyy3W1l0VfLdkpQyFdkXvDA0Jpx3TTn1og",
"y": "lNsXBQzhcrrGrf9XZYri_LLN1Bye4PdfTaoHbC6PIGI"
}
}
]
}
Loading

0 comments on commit 78d3e4f

Please sign in to comment.