Hello fellow developers,
I'm currently working on an SDK involving the SECP256R1 standard and facing an interesting issue. My goal is to ensure the Swift implementation of SECP256R1 signatures matches that of Rust's FastCrypto implementation.
The Issue:
When running tests to compare signatures generated by Swift and Rust implementations, the signatures do not match. Despite this mismatch, verification tests still succeed. I've tried using both the P256 class from CryptoKit and SecKey from the Security SDK. The Swift code is being written in Xcode 15 Beta 8, Swift 5.9. Code Snippet:
struct SECP256R1PrivateKey {
/// Commented is P256, uncommented is SecKey
// public init(key: Data) throws {
// if let privateKey = try? P256.Signing.PrivateKey(rawRepresentation: key) {
// self.key = privateKey
// } else {
// throw AccountError.invalidData
// }
// }
public init(key: Data) throws {
if let privateKeyP256 = try? P256.Signing.PrivateKey(rawRepresentation: key) {
let attributes: [String: Any] = [
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
kSecAttrKeyType as String: kSecAttrKeyTypeECDSA,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecAttrKeySizeInBits as String: 256
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateWithData(privateKeyP256.rawRepresentation as CFData, attributes as CFDictionary, &error) else {
throw error?.takeRetainedValue() as Error? ?? NSError(domain: NSOSStatusErrorDomain, code: Int(errSecParam), userInfo: nil)
}
self.key = privateKey
} else {
throw AccountError.invalidData
}
}
// public func sign(data: Data) throws -> Signature {
// let signature = try self.key.signature(for: data)
// return Signature(
// signature: signature.rawRepresentation,
// publickey: try self.publicKey().key.compressedRepresentation,
// signatureScheme: .SECP256R1
// )
// }
public func sign(data: Data) throws -> Signature {
let dataHash = Data(data.sha256)
var error: Unmanaged<CFError>?
guard let signature = SecKeyCreateSignature(self.key, .ecdsaSignatureMessageX962SHA256, dataHash as NSData, &error) as Data? else {
throw error!.takeRetainedValue() as Error
}
guard let publicKey = SecKeyCopyExternalRepresentation(try self.publicKey().key, &error) as Data? else {
throw AccountError.invalidData
}
return Signature(
signature: signature,
publickey: publicKey,
signatureScheme: .SECP256R1
)
}
}
func testThatTheRustImplementationForSignaturesIsTheSame() throws {
let account = try Account(privateKey: Data(self.validSecp256r1SecretKey), accountType: .secp256r1)
guard let signData = "Hello, world!".data(using: .utf8) else { XCTFail("Unable to encode message"); return; }
let signature = try account.sign(signData)
XCTAssertEqual(
try signature.hex(),
"26d84720652d8bc4ddd1986434a10b3b7b69f0e35a17c6a5987e6d1cba69652f4384a342487642df5e44592d304bea0ceb0fae2e347fa3cec5ce1a8144cfbbb2"
)
}
The Core Question:
How do I implement the R1 signature in Swift so that it matches the signature generated by Rust's FastCrypto?
Any insights, suggestions, or sample code snippets that could guide me in the right direction would be immensely appreciated!
Thank you in advance!
Alright, I solved the issue regarding signature verification. For context, the end point I used utilizes P256 methods that conform with the RFC6979 protocol:
https://www.rfc-editor.org/rfc/rfc6979.txt
And, it turns out, that the signatures returned from CryptoKit's P256 class do in fact conform to this protocol, with one catch: the signature needs to be normalized before you send it off to the end point. I written up some code here that will help out with normalizing these signatures so that s, in context with r || s for P256 elliptic curves, is in the lower half of the curve; and normalized.
Note: The developer is required to install the BigInt package published by attaswift, as the signature values are too high to store in a normal UInt64 variable:
extension Data {
func hexEncodedString() -> String {
return map { String(format: "%02hhx", $0) }.joined()
}
}
func normalizeSignature(_ signatureHex: String) -> String? {
let curveOrder = BigInt("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", radix: 16)!
// Assuming the signature is evenly split between r and s
let rHex = String(signatureHex.prefix(signatureHex.count / 2))
let sHex = String(signatureHex.suffix(signatureHex.count / 2))
// Convert hex to BigInt
var s = BigInt(sHex, radix: 16)!
// Normalize s if necessary
let halfCurveOrder = curveOrder / 2
if s > halfCurveOrder {
s = curveOrder - s
}
// Convert back to hex, ensuring it's zero-padded to the correct length
let normalizedSHex = String(s, radix: 16).leftPad(toLength: sHex.count, withPad: "0")
return rHex + normalizedSHex
}
extension String {
func leftPad(toLength: Int, withPad: String) -> String {
guard toLength > self.count else { return self }
let padding = String(repeating: withPad, count: toLength - self.count)
return padding + self
}
}
While the signature is still not matching that of Rust's FastCrypto, the signature will pass verification if passed into FastCrypto's verification function:
% target/debug/sigs-cli verify --msg 48656c6c6f2c20776f726c6421 --public-key 0227322b3a891a0a280d6bc1fb2cbb23d28f54906fd6407f5f741f6def5762609a --signature 4672703055add7ea9f75a80798f4fb65f9dfab2db5a661a002ead74cde53f4bc50e54e4ed2d88c95211b70903562daa3e4088452b32fce659d911665aac0ed2e --scheme secp256r1
Verify result: Ok(())
In short, in order for the developer to be able to use a CryptoKit P256 signature with Rust's FastCrypto library, they must normalize s first before passing the signature into the Rust application.
I will mark this post as the solution.