Implementing SECP256R1 Signature Matching with Rust's FastCrypto in Swift

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!

Accepted Reply

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.

Replies

It’s hard to be 100% sure what’s going on here because your snippet isn’t self contained (how’s Signature defined; what’s the value of validSecp256r1SecretKey?) but, assuming that:

26d84720652d8bc4ddd1986434a10b3b7b69f0e35a17c6a5987e6d1cba69652f4384a342487642df5e44592d304bea0ceb0fae2e347fa3cec5ce1a8144cfbbb2

is meant to be the signature value then it looks like your code is expecting to get a raw signature whereas .ecdsaSignatureMessageX962SHA256 returns an X9.62 ASN.1 signature.

Consider this code:

import Foundation
import CryptoKit

func main() throws {
    let pk = P256.Signing.PrivateKey()
    let message = Data("Hello Cruel World!".utf8)
    let signature = try pk.signature(for: message)
    print((signature.rawRepresentation as NSData).debugDescription)
    print((signature.derRepresentation as NSData).debugDescription)
}

try! main()

Note I’m using Apple CryptoKit here because it’s much nicer, but the same logic applies to Security framework.

When I ran it here, it printed:

<743ae104 ec13aea4 6528ac73 8a8e0def d1256b71 3d1ad658 ef090f7c e316987d e7315995 11aa5752 f01decd2 d9d6025d bedbe464 9742118d 0730daf6 ae11a557>
<30450220 743ae104 ec13aea4 6528ac73 8a8e0def d1256b71 3d1ad658 ef090f7c e316987d 022100e7 31599511 aa5752f0 1decd2d9 d6025dbe dbe46497 42118d07 30daf6ae 11a557>

Now dump the DER form:

% dumpasn1 -a -p signature.der
SEQUENCE {
  INTEGER
    74 3A E1 04 EC 13 AE A4 65 28 AC 73 8A 8E 0D EF
    D1 25 6B 71 3D 1A D6 58 EF 09 0F 7C E3 16 98 7D
  INTEGER
    00 E7 31 59 95 11 AA 57 52 F0 1D EC D2 D9 D6 02
    5D BE DB E4 64 97 42 11 8D 07 30 DA F6 AE 11 A5
    57
  }

As you can see, it’s the same raw bytes, just one is packaged in a DER structure.

Assuming my theory is correct, you have two options:

  • Continue with Security framework and strip that DER structure manually.

  • Switch to Apple CryptoKit, and use rawRepresentation.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi @eskimo,

Thank you for your prompt reply. To clarify, at the minimum from my understand so feel free to correct any misinformation if presented, the Rust FastCrypto library use PKCS 1.*, while P256 (and it seems like CryptoKit as well) uses PSS signatures.

PKCS 1.* signatures are deterministic, while PSS are non-deterministic.

Linked here is what flagged to me that CryptoKit utilized PSS, specifically quoted:

The signing algorithm employs randomization to generate a different signature on every call, even for the same data and key.

I'm also sharing an updated code example without external dependencies.

The test case has the expected signature that is derived from Rust's FastCrypto signature function for P256:

import Foundation
import CryptoKit

func main() throws {
    let pk = P256.Signing.PrivateKey()
    let message = Data("Hello, world!".utf8)
    let signature = try pk.signature(for: message)

    let expectedSignature = Data([UInt8]([
        38, 216,  71,  32, 101,  45, 139, 196, 221, 209, 152,
       100,  52, 161,  11,  59, 123, 105, 240, 227,  90,  23,
       198, 165, 152, 126, 109,  28, 186, 105, 101,  47,  67,
       132, 163,  66,  72, 118,  66, 223,  94,  68,  89,  45,
        48,  75, 234,  12, 235,  15, 174,  46,  52, 127, 163,
       206, 197, 206,  26, 129,  68, 207, 187, 178
     ]))

    if signature.rawRepresentation == expectedSignature {
        print("Signature matches with Rust.")
    } else {
        print("Signature mismatch.\n")
        print("Expected: \([UInt8](expectedSignature))\n")
        print("Got: \([UInt8](signature.rawRepresentation))\n")
    }
}

try! main()

Here are a couple print outs of the code for context as well to demonstrate the issue at hand:

Signature mismatch.

Expected: [38, 216, 71, 32, 101, 45, 139, 196, 221, 209, 152, 100, 52, 161, 11, 59, 123, 105, 240, 227, 90, 23, 198, 165, 152, 126, 109, 28, 186, 105, 101, 47, 67, 132, 163, 66, 72, 118, 66, 223, 94, 68, 89, 45, 48, 75, 234, 12, 235, 15, 174, 46, 52, 127, 163, 206, 197, 206, 26, 129, 68, 207, 187, 178]

Got: [3, 132, 62, 17, 173, 125, 251, 71, 167, 101, 152, 113, 63, 63, 214, 167, 87, 183, 132, 190, 49, 4, 45, 6, 101, 6, 195, 190, 245, 44, 129, 236, 173, 119, 187, 71, 223, 121, 127, 225, 93, 123, 0, 176, 157, 231, 65, 186, 46, 110, 243, 147, 56, 87, 216, 10, 30, 5, 229, 49, 234, 20, 61, 59]
Signature mismatch.

Expected: [38, 216, 71, 32, 101, 45, 139, 196, 221, 209, 152, 100, 52, 161, 11, 59, 123, 105, 240, 227, 90, 23, 198, 165, 152, 126, 109, 28, 186, 105, 101, 47, 67, 132, 163, 66, 72, 118, 66, 223, 94, 68, 89, 45, 48, 75, 234, 12, 235, 15, 174, 46, 52, 127, 163, 206, 197, 206, 26, 129, 68, 207, 187, 178]

Got: [245, 245, 34, 3, 104, 239, 95, 95, 193, 202, 39, 178, 83, 180, 173, 34, 118, 21, 146, 96, 226, 168, 188, 71, 179, 20, 228, 181, 224, 203, 176, 206, 146, 247, 227, 180, 72, 249, 112, 21, 23, 113, 67, 81, 176, 170, 94, 210, 120, 128, 174, 115, 78, 187, 16, 27, 116, 81, 107, 139, 84, 105, 44, 185]

My main question here is what can be done here to match CryptoKit's P256 implementation with that of Rust's FastCrypto P256 implementation. And if not, what other alternatives can be used to have deterministic signatures with either CryptoKit or SecKey.

Kindly, MarcoDotIO

Adding to this as well @eskimo, here's the updated code with the private key used for the test.

import Foundation
import CryptoKit

func main() throws {
    let validSecp256r1SecretKey: [UInt8] = [
        66, 37, 141, 205, 161, 76, 241, 17, 198, 2, 184, 151, 
        27, 140, 200, 67, 233, 30, 70, 202, 144, 81, 81, 192, 
        39, 68, 166, 176, 23, 230, 147, 22
    ]
    let pk = try P256.Signing.PrivateKey(rawRepresentation: validSecp256r1SecretKey)
    let message = Data("Hello, world!".utf8)
    let signature = try pk.signature(for: message)

    let expectedSignature = Data([UInt8]([
        38, 216,  71,  32, 101,  45, 139, 196, 221, 209, 152,
       100,  52, 161,  11,  59, 123, 105, 240, 227,  90,  23,
       198, 165, 152, 126, 109,  28, 186, 105, 101,  47,  67,
       132, 163,  66,  72, 118,  66, 223,  94,  68,  89,  45,
        48,  75, 234,  12, 235,  15, 174,  46,  52, 127, 163,
       206, 197, 206,  26, 129,  68, 207, 187, 178
     ]))

    if signature.rawRepresentation == expectedSignature {
        print("Signature matches with Rust.")
    } else {
        print("Signature mismatch.\n")
        print("Expected: \([UInt8](expectedSignature))\n")
        print("Got: \([UInt8](signature.rawRepresentation))\n")
    }
}

try! main()

The updated code does still have the issue listed above, for the record.

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.