This repository has been archived by the owner on Dec 12, 2024. It is now read-only.
generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
secp256k1 key generation, signing, and verification (#3)
- Loading branch information
Showing
10 changed files
with
874 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
61
Tests/tbDEXTests/TestVectors/secp256k1/bytes-to-private-key.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
57
Tests/tbDEXTests/TestVectors/secp256k1/bytes-to-public-key.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
] | ||
} |
Oops, something went wrong.