Skip to content

Commit 98adfd8

Browse files
authored
Recovery Module (#211)
* WIP Recovery Module * Adding tests and updating documentation
1 parent d1e2821 commit 98adfd8

File tree

8 files changed

+313
-6
lines changed

8 files changed

+313
-6
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,13 @@ let package = Package(
148148
sources: [
149149
"Asymmetric.swift",
150150
"DH.swift",
151-
"Digests.swift",
151+
"Hash32BytesDigest.swift",
152152
"ECDH.swift",
153153
"ECDSA.swift",
154154
"EdDSA.swift",
155155
"Errors.swift",
156156
"PrettyBytes.swift",
157+
"Recovery.swift",
157158
"SafeCompare.swift",
158159
"Schnorr.swift",
159160
"secp256k1.swift",

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ let privateKey = try! secp256k1.Signing.PrivateKey(rawRepresentation: privateByt
5959

6060
// Adding a tweak to the private key and public key
6161
let tweak = try! "5f0da318c6e02f653a789950e55756ade9f194e1ec228d7f368de1bd821322b6".bytes
62-
let tweakedPrivateKey = try! privateKey.tweak(tweak)
63-
let tweakedPublicKeyKey = try! privateKey.publicKey.tweak(tweak)
62+
let tweakedPrivateKey = try! privateKey.add(tweak)
63+
let tweakedPublicKeyKey = try! privateKey.publicKey.add(tweak)
6464
```
6565

6666
## Elliptic Curve Diffie Hellman
@@ -95,6 +95,22 @@ let xonlyTweak2 = try! sharedSecretSign2.publicKey.xonly.add(privateSign1.public
9595
let privateTweak1 = try! sharedSecretSign1.add(xonly: privateSign1.publicKey.xonly.bytes)
9696
```
9797

98+
## Recovery
99+
100+
```swift
101+
let privateKey = try! secp256k1.Signing.PrivateKey()
102+
let messageData = "We're all Satoshi.".data(using: .utf8)!
103+
104+
// Create a recoverable ECDSA signature
105+
let recoverySignature = try! privateKey.ecdsa.recoverableSignature(for: messageData)
106+
107+
// Recover an ECDSA public key from a signature
108+
let publicKey = try! secp256k1.Recovery.PublicKey(messageData, signature: recoverySignature)
109+
110+
// Convert a recoverable signature into a normal signature
111+
let signature = try! recoverySignature.normalize
112+
```
113+
98114

99115
# Getting Started
100116

Sources/implementation/ECDSA.swift

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public extension secp256k1.Signing {
5151

5252
/// Initializes ECDSASignature from the raw representation.
5353
/// - Parameters:
54-
/// - rawRepresentation: A raw representation of the key as a collection of contiguous bytes.
54+
/// - dataRepresentation: A data representation of the key as a collection of contiguous bytes.
5555
/// - Throws: If there is a failure with the dataRepresentation count
5656
internal init(_ dataRepresentation: Data) throws {
5757
guard dataRepresentation.count == 4 * secp256k1.CurveDetails.coordinateByteCount else {
@@ -146,6 +146,28 @@ public extension secp256k1.Signing {
146146
}
147147

148148
extension secp256k1.Signing.ECDSASigner: DigestSigner, Signer {
149+
/// Generates a recoverable ECDSA signature.
150+
///
151+
/// - Parameter digest: The digest to sign.
152+
/// - Returns: The recoverable ECDSA Signature.
153+
/// - Throws: If there is a failure producing the signature
154+
public func recoverableSignature<D: Digest>(for digest: D) throws -> secp256k1.Recovery.ECDSASignature {
155+
var signature = secp256k1_ecdsa_recoverable_signature()
156+
157+
guard secp256k1_ecdsa_sign_recoverable(
158+
secp256k1.Context.raw,
159+
&signature,
160+
Array(digest),
161+
Array(signingKey.rawRepresentation),
162+
nil,
163+
nil
164+
).boolValue else {
165+
throw secp256k1Error.underlyingCryptoError
166+
}
167+
168+
return try secp256k1.Recovery.ECDSASignature(signature.dataValue)
169+
}
170+
149171
/// Generates an ECDSA signature over the secp256k1 elliptic curve.
150172
///
151173
/// - Parameter digest: The digest to sign.
@@ -154,12 +176,29 @@ extension secp256k1.Signing.ECDSASigner: DigestSigner, Signer {
154176
public func signature<D: Digest>(for digest: D) throws -> secp256k1.Signing.ECDSASignature {
155177
var signature = secp256k1_ecdsa_signature()
156178

157-
guard secp256k1_ecdsa_sign(secp256k1.Context.raw, &signature, Array(digest), Array(signingKey.rawRepresentation), nil, nil).boolValue else {
179+
guard secp256k1_ecdsa_sign(
180+
secp256k1.Context.raw,
181+
&signature,
182+
Array(digest),
183+
Array(signingKey.rawRepresentation),
184+
nil,
185+
nil
186+
).boolValue else {
158187
throw secp256k1Error.underlyingCryptoError
159188
}
160189

161190
return try secp256k1.Signing.ECDSASignature(signature.dataValue)
162191
}
192+
193+
/// Generates an ECDSA signature over the secp256k1 elliptic curve.
194+
/// SHA256 is used as the hash function.
195+
///
196+
/// - Parameter data: The data to sign.
197+
/// - Returns: The ECDSA Signature.
198+
/// - Throws: If there is a failure producing the signature.
199+
public func recoverableSignature<D: DataProtocol>(for data: D) throws -> secp256k1.Recovery.ECDSASignature {
200+
try recoverableSignature(for: SHA256.hash(data: data))
201+
}
163202

164203
/// Generates an ECDSA signature over the secp256k1 elliptic curve.
165204
/// SHA256 is used as the hash function.

Sources/implementation/Digests.swift renamed to Sources/implementation/Hash32BytesDigest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Digests.swift
2+
// Hash32BytesDigest.swift
33
// GigaBitcoin/secp256k1.swift
44
//
55
// Modifications Copyright (c) 2021 GigaBitcoin LLC
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//
2+
// Recovery.swift
3+
// GigaBitcoin/secp256k1.swift
4+
//
5+
// Copyright (c) 2022 GigaBitcoin LLC
6+
// Distributed under the MIT software license
7+
//
8+
// See the accompanying file LICENSE for information
9+
//
10+
11+
import Foundation
12+
import secp256k1_bindings
13+
14+
// MARK: - secp256k1 + Recovery
15+
16+
public extension secp256k1 {
17+
enum Recovery {
18+
public struct PublicKey {
19+
let baseKey: PublicKeyImplementation
20+
21+
/// Initializes a secp256k1 public key using a data message and a recovery signature.
22+
/// - Parameters:
23+
/// - data: The data to be hash and assumed signed.
24+
/// - signature: A raw representation of the initialized signature that supports pubkey recovery.
25+
/// - format: the format of the public key object
26+
public init<D: DataProtocol>(
27+
_ data: D,
28+
signature: secp256k1.Recovery.ECDSASignature,
29+
format: secp256k1.Format = .compressed
30+
) throws {
31+
self.baseKey = try PublicKeyImplementation(
32+
SHA256.hash(data: data),
33+
signature: signature,
34+
format: format
35+
)
36+
}
37+
38+
/// Initializes a secp256k1 public key using a hash digest and a recovery signature.
39+
/// - Parameters:
40+
/// - digest: The hash digest assumed to be signed.
41+
/// - signature: A raw representation of the initialized signature that supports pubkey recovery.
42+
/// - format: the format of the public key object
43+
public init<D: Digest>(
44+
_ digest: D,
45+
signature: secp256k1.Recovery.ECDSASignature,
46+
format: secp256k1.Format = .compressed
47+
) throws {
48+
self.baseKey = try PublicKeyImplementation(digest, signature: signature, format: format)
49+
}
50+
51+
/// Initializes a secp256k1 public key for recovery.
52+
/// - Parameter baseKey: generated secp256k1 public key.
53+
init(baseKey: PublicKeyImplementation) {
54+
self.baseKey = baseKey
55+
}
56+
57+
/// A data representation of the public key
58+
public var rawRepresentation: Data { baseKey.rawRepresentation }
59+
60+
/// Implementation public key object
61+
var bytes: [UInt8] { baseKey.bytes }
62+
}
63+
}
64+
}
65+
66+
/// An ECDSA (Elliptic Curve Digital Signature Algorithm) Recovery Signature
67+
public extension secp256k1.Recovery {
68+
69+
/// Recovery Signature
70+
struct ECDSACompactSignature {
71+
let signature: Data
72+
let recoveryId: Int32
73+
}
74+
75+
struct ECDSASignature: ContiguousBytes, RawSignature {
76+
/// Returns the raw signature.
77+
public var rawRepresentation: Data
78+
79+
/// Initializes ECDSASignature from the raw representation.
80+
/// - Parameters:
81+
/// - rawRepresentation: A raw representation of the key as a collection of contiguous bytes.
82+
/// - Throws: If there is a failure with the dataRepresentation count
83+
public init<D: DataProtocol>(rawRepresentation: D) throws {
84+
guard rawRepresentation.count == 4 * secp256k1.CurveDetails.coordinateByteCount + 1 else {
85+
throw secp256k1Error.incorrectParameterSize
86+
}
87+
88+
self.rawRepresentation = Data(rawRepresentation)
89+
}
90+
91+
/// Initializes ECDSASignature from the raw representation.
92+
/// - Parameters:
93+
/// - rawRepresentation: A raw representation of the key as a collection of contiguous bytes.
94+
/// - Throws: If there is a failure with the dataRepresentation count
95+
internal init(_ dataRepresentation: Data) throws {
96+
guard dataRepresentation.count == 4 * secp256k1.CurveDetails.coordinateByteCount + 1 else {
97+
throw secp256k1Error.incorrectParameterSize
98+
}
99+
100+
self.rawRepresentation = dataRepresentation
101+
}
102+
103+
/// Initializes ECDSASignature from the Compact representation.
104+
/// - Parameter compactRepresentation: A Compact representation of the key as a collection of contiguous bytes.
105+
/// - Throws: If there is a failure with parsing the derRepresentation
106+
public init<D: DataProtocol>(compactRepresentation: D, recoveryId: Int32) throws {
107+
var recoverableSignature = secp256k1_ecdsa_recoverable_signature()
108+
109+
guard secp256k1_ecdsa_recoverable_signature_parse_compact(
110+
secp256k1.Context.raw,
111+
&recoverableSignature,
112+
Array(compactRepresentation),
113+
recoveryId
114+
).boolValue else {
115+
throw secp256k1Error.underlyingCryptoError
116+
}
117+
118+
self.rawRepresentation = recoverableSignature.dataValue
119+
}
120+
121+
/// Invokes the given closure with a buffer pointer covering the raw bytes of the digest.
122+
/// - Parameter body: A closure that takes a raw buffer pointer to the bytes of the digest and returns the digest.
123+
/// - Throws: If there is a failure with underlying `withUnsafeBytes`
124+
/// - Returns: The signature as returned from the body closure.
125+
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
126+
try rawRepresentation.withUnsafeBytes(body)
127+
}
128+
129+
/// Serialize an ECDSA signature in compact (64 byte) format.
130+
/// - Throws: If there is a failure parsing signature
131+
/// - Returns: a 64-byte data representation of the compact serialization
132+
public var compactRepresentation: ECDSACompactSignature {
133+
get throws {
134+
let compactSignatureLength = 64
135+
var recoveryId = Int32()
136+
var recoverableSignature = secp256k1_ecdsa_recoverable_signature()
137+
var compactSignature = [UInt8](repeating: 0, count: compactSignatureLength)
138+
139+
rawRepresentation.copyToUnsafeMutableBytes(of: &recoverableSignature.data)
140+
141+
guard secp256k1_ecdsa_recoverable_signature_serialize_compact(
142+
secp256k1.Context.raw,
143+
&compactSignature,
144+
&recoveryId,
145+
&recoverableSignature
146+
).boolValue else {
147+
throw secp256k1Error.underlyingCryptoError
148+
}
149+
150+
return secp256k1.Recovery.ECDSACompactSignature(
151+
signature: Data(bytes: &compactSignature, count: compactSignatureLength),
152+
recoveryId: recoveryId
153+
)
154+
}
155+
}
156+
157+
/// Convert a recoverable signature into a normal signature.
158+
public var normalize: secp256k1.Signing.ECDSASignature {
159+
get throws {
160+
var normalizedSignature = secp256k1_ecdsa_signature()
161+
var recoverableSignature = secp256k1_ecdsa_recoverable_signature()
162+
163+
rawRepresentation.copyToUnsafeMutableBytes(of: &recoverableSignature.data)
164+
165+
guard secp256k1_ecdsa_recoverable_signature_convert(
166+
secp256k1.Context.raw,
167+
&normalizedSignature,
168+
&recoverableSignature
169+
).boolValue else {
170+
throw secp256k1Error.underlyingCryptoError
171+
}
172+
173+
return try secp256k1.Signing.ECDSASignature(normalizedSignature.dataValue)
174+
}
175+
}
176+
}
177+
}

Sources/implementation/Utility.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ public extension secp256k1_ecdsa_signature {
4242
}
4343
}
4444

45+
public extension secp256k1_ecdsa_recoverable_signature {
46+
var dataValue: Data {
47+
var mutableSig = self
48+
return Data(bytes: &mutableSig.data, count: MemoryLayout.size(ofValue: data))
49+
}
50+
}
51+
4552
public extension String {
4653
/// Public initializer backed by the `BytesUtil.swift` DataProtocol extension property `hexString`
4754
/// - Parameter bytes: byte array to initialize

Sources/implementation/secp256k1.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,32 @@ extension secp256k1 {
182182
self._xonlyBytes = xonly
183183
self._keyParity = keyParity
184184
}
185+
186+
/// Backing initialization that sets the public key from a digest and recoverable signature.
187+
/// - Parameters:
188+
/// - digest: The digest that was signed.
189+
/// - signature: The signature to recover the public key from
190+
/// - format: the format of the public key object
191+
/// - Throws: An error is thrown when a public key is not recoverable from the signature.
192+
@usableFromInline init<D: Digest>(_ digest: D, signature: secp256k1.Recovery.ECDSASignature, format: secp256k1.Format) throws {
193+
var keyParity = Int32()
194+
var pubKeyLen = format.length
195+
var pubKey = secp256k1_pubkey()
196+
var pubBytes = [UInt8](repeating: 0, count: pubKeyLen)
197+
var recoverySignature = secp256k1_ecdsa_recoverable_signature()
198+
199+
signature.rawRepresentation.copyToUnsafeMutableBytes(of: &recoverySignature.data)
200+
201+
guard secp256k1_ecdsa_recover(secp256k1.Context.raw, &pubKey, &recoverySignature, Array(digest)).boolValue,
202+
secp256k1_ec_pubkey_serialize(secp256k1.Context.raw, &pubBytes, &pubKeyLen, &pubKey, format.rawValue).boolValue else {
203+
throw secp256k1Error.underlyingCryptoError
204+
}
205+
206+
self._xonlyBytes = try XonlyKeyImplementation.generate(bytes: pubBytes, keyParity: &keyParity, format: format)
207+
self._keyParity = keyParity
208+
self.format = format
209+
self.bytes = pubBytes
210+
}
185211

186212
/// Generates a secp256k1 public key from bytes representation.
187213
/// - Parameter privateBytes: a private key object in bytes form

0 commit comments

Comments
 (0)