Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 14 additions & 62 deletions packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -706,77 +706,29 @@ public final class KeychainSigner: Signer, @unchecked Sendable {
return .success(signature)
}

/// v1 sign primitive. Wraps the raw 32-byte ECDSA scalar in a
/// throwaway FFI signer just long enough to produce a signature;
/// the keychain bytes are zeroed from the local copy as soon as
/// the FFI call returns.
/// v1 sign primitive. Delegates to `RawKeySigner.sign` (the shared
/// create-signer → sign → destroy round-trip; the key copy is zeroed
/// there), mapping its typed errors onto this signer's error space.
///
/// TODO(KeychainSigner v2): replace this whole function with a
/// native-Swift `secp256k1` invocation once we add the
/// TODO(KeychainSigner v2): replace `RawKeySigner`'s FFI round-trip
/// with a native-Swift `secp256k1` invocation once we add the
/// `swift-secp256k1` SPM dep.
fileprivate func ffiSign(
privateKey: Data,
data: Data
) -> Result<Data, Error> {
// Defensive copy into a mutable buffer we can zero on exit.
var keyCopy = [UInt8](privateKey)
defer {
// Best-effort scrub. Swift doesn't guarantee this won't be
// optimised away, but an explicit `withContiguousMutableStorageIfAvailable`
// pattern does keep the touch in the IR.
keyCopy.withUnsafeMutableBufferPointer { buf in
if let base = buf.baseAddress {
memset_s(UnsafeMutableRawPointer(base), buf.count, 0, buf.count)
}
}
}

let signerResult = keyCopy.withUnsafeBufferPointer { keyBuf -> DashSDKResult in
dash_sdk_signer_create_from_private_key(
keyBuf.baseAddress!,
UInt(keyBuf.count),
self.network.ffiValue
)
}

if let errPtr = signerResult.error {
let message = errPtr.pointee.message.map { String(cString: $0) } ?? "unknown"
dash_sdk_error_free(errPtr)
do {
return .success(
try RawKeySigner.sign(data: data, privateKey: privateKey, network: self.network))
} catch KeyManagerError.signerCreationFailed(let message) {
return .failure(.ffiSignerCreationFailed(message: message))
}
guard let rawSigner = signerResult.data else {
return .failure(.ffiSignerCreationFailed(message: "null handle"))
}
let signerHandle = OpaquePointer(rawSigner)
defer { dash_sdk_signer_destroy(signerHandle) }

let signResult = data.withUnsafeBytes { dataBuf -> DashSDKResult in
dash_sdk_signer_sign(
signerHandle,
dataBuf.bindMemory(to: UInt8.self).baseAddress,
UInt(dataBuf.count)
)
}

if let errPtr = signResult.error {
let message = errPtr.pointee.message.map { String(cString: $0) } ?? "unknown"
dash_sdk_error_free(errPtr)
} catch KeyManagerError.invalidKeyFormat(let message) {
return .failure(.ffiSignerCreationFailed(message: "invalid key format: \(message)"))
} catch KeyManagerError.signingFailed(let message) {
return .failure(.ffiSignFailed(message: message))
} catch {
return .failure(.ffiSignFailed(message: String(describing: error)))
}
guard let sigPtr = signResult.data else {
return .failure(.ffiSignFailed(message: "null signature"))
}

let sigStruct = sigPtr.assumingMemoryBound(to: DashSDKSignature.self)
defer { dash_sdk_signature_free(sigStruct) }

let sigBytes: Data
if let bytes = sigStruct.pointee.signature {
sigBytes = Data(bytes: bytes, count: Int(sigStruct.pointee.signature_len))
} else {
sigBytes = Data()
}
return .success(sigBytes)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// MARK: - Signer protocol conformance (legacy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum KeyManagerError: LocalizedError, Sendable {
case privateKeyNotFound(KeyID)
case invalidKeyFormat(String)
case signerCreationFailed(String)
case signingFailed(String)
Comment thread
QuantumExplorer marked this conversation as resolved.
case keychainError(String)
case noSuitableKey(String)

Expand All @@ -22,6 +23,8 @@ public enum KeyManagerError: LocalizedError, Sendable {
return "Invalid key format: \(message)"
case .signerCreationFailed(let message):
return "Failed to create signer: \(message)"
case .signingFailed(let message):
return "Failed to sign: \(message)"
case .keychainError(let message):
return "Keychain error: \(message)"
case .noSuitableKey(let message):
Expand All @@ -30,6 +33,92 @@ public enum KeyManagerError: LocalizedError, Sendable {
}
}

// MARK: - Raw-key one-shot signing

/// One-shot ECDSA signing with a raw 32-byte private key.
///
/// The `dash_sdk_signer_create_from_private_key` → `dash_sdk_signer_sign`
/// → `dash_sdk_signer_destroy` FFI round-trip, extracted from
/// `KeychainSigner` so callers that already hold key material (e.g. a
/// derivation-path lookup) can sign without constructing a
/// `KeychainSigner`/`KeyManager` stack. `KeychainSigner.ffiSign`
/// delegates here — keep the two in sync by keeping this the only
/// implementation.
///
/// The FFI double-SHA256es `data` before signing
/// (`dashcore::signer::sign`) and returns the 65-byte compact
/// recoverable signature — recovery header byte first, compressed-key
/// range 31–34 — i.e. the Dash Core `signmessage` wire format.
public enum RawKeySigner {
/// - Parameters:
/// - data: Raw bytes to sign. Hashed (SHA256d) inside the FFI —
/// pass the full message, never a pre-computed digest.
/// - privateKey: 32-byte ECDSA scalar. A local copy is zeroed
/// before returning.
/// - network: Affects WIF/address metadata inside the signer only,
/// not the signature bytes.
public static func sign(data: Data, privateKey: Data, network: Network) throws -> Data {
guard privateKey.count == 32 else {
throw KeyManagerError.invalidKeyFormat(
"Private key must be 32 bytes, got \(privateKey.count)")
}

// Defensive copy into a mutable buffer we can zero on exit.
var keyCopy = [UInt8](privateKey)
defer {
keyCopy.withUnsafeMutableBufferPointer { buf in
if let base = buf.baseAddress {
memset_s(UnsafeMutableRawPointer(base), buf.count, 0, buf.count)
}
}
}

let signerResult = keyCopy.withUnsafeBufferPointer { keyBuf -> DashSDKResult in
dash_sdk_signer_create_from_private_key(
keyBuf.baseAddress!,
UInt(keyBuf.count),
network.ffiValue
)
}

if let errPtr = signerResult.error {
let message = errPtr.pointee.message.map { String(cString: $0) } ?? "unknown"
dash_sdk_error_free(errPtr)
throw KeyManagerError.signerCreationFailed(message)
}
guard let rawSigner = signerResult.data else {
throw KeyManagerError.signerCreationFailed("null handle")
}
let signerHandle = OpaquePointer(rawSigner)
defer { dash_sdk_signer_destroy(signerHandle) }

let signResult = data.withUnsafeBytes { dataBuf -> DashSDKResult in
dash_sdk_signer_sign(
signerHandle,
dataBuf.bindMemory(to: UInt8.self).baseAddress,
UInt(dataBuf.count)
)
}

if let errPtr = signResult.error {
let message = errPtr.pointee.message.map { String(cString: $0) } ?? "unknown"
dash_sdk_error_free(errPtr)
throw KeyManagerError.signingFailed(message)
}
guard let sigPtr = signResult.data else {
throw KeyManagerError.signingFailed("null signature")
}

let sigStruct = sigPtr.assumingMemoryBound(to: DashSDKSignature.self)
defer { dash_sdk_signature_free(sigStruct) }

guard let bytes = sigStruct.pointee.signature else {
throw KeyManagerError.signingFailed("empty signature")
}
return Data(bytes: bytes, count: Int(sigStruct.pointee.signature_len))
}
}

// MARK: - Key Manager

/// Centralized key management for Dash Platform identities.
Expand Down
Loading