Carsten
Carsten

Reputation: 1059

Testing CryptoKit's data validation

I want to verify some downloaded data is unmodified. My expectation is that if I modify the original data, the signature would fail. While this is true in some cases (data2) it's surprisingly not working in others (data3). Using hashes/Digest returns the same results.

import CryptoKit

let rootKey = P256.Signing.PrivateKey()
let publicKey = rootKey.publicKey

let data = Data(bytes: [0xA, 0xB, 0xC, 0xD], count: 4)
let digest = SHA256.hash(data: data)
let signature = try rootKey.signature(for: data)
let hashSignature = try rootKey.signature(for: digest)

// now alter the data
let data2 = Data(bytes: [0x0, 0xB, 0xC, 0xD], count: 4)
let data3 = Data(bytes: [0xA, 0xB, 0xC, 0xE], count: 4)

publicKey.isValidSignature(signature, for: data) // true, as expected
publicKey.isValidSignature(signature, for: data2) // false, as expected
publicKey.isValidSignature(signature, for: data3) // why is THIS true/valid?

publicKey.isValidSignature(hashSignature, for: SHA256.hash(data: data)) // true
publicKey.isValidSignature(hashSignature, for: SHA256.hash(data: data2)) // false
publicKey.isValidSignature(hashSignature, for: SHA256.hash(data: data3)) // true

For simplicity CryptoKit is used. This also fails in (my) CommonCrypto/SecKey... implementation.

Upvotes: 3

Views: 540

Answers (2)

Carsten
Carsten

Reputation: 1059

Thanks to Sajjon's answer I was able to figure out what I did wrong. It seems it's not a bug but a simple type casting issue:

let bytes = [UInt8]([0xA, 0xB, 0xC, 0xD])
var foo = Data(bytes: bytes, count: 4)
print(hexString(foo)) // 0a0b0c0d
foo[1] <<= 1
print(hexString(foo)) // 0a160c0d

let bar = Data(bytes: [0xA, 0xB, 0xC, 0xD], count: 32)
print(hexString(bar)) // 0a000000000000000b000000000000000c000000000000000d00000000000000

I defined the bytes in my original Data in place, so they were defaulted to Int a.k.a. Int64. Providing a data length of 4 simply omits everything after the first 4 bytes of data.

Upvotes: 0

Sajjon
Sajjon

Reputation: 9887

OK I found the problem. Your code is 100% correct. The problem is caused by some rather problematic bug in Swift - I think. Smarter Swift experts - please correct and enlighten me!

By the way: I'm using Xcode Version 12.2 beta 3 (12B5035g)

Data inequality bug

func testDataInequality() {
    func hexString(_ data: Data) -> String {
        data.map { String(format: "%02hhx", $0) }.joined()
    }
    
    let data = Data(bytes: [0xA, 0xB, 0xC, 0xD], count: 4)
    let data3 = Data(bytes: [0xA, 0xB, 0xC, 0xE], count: 4)
    XCTAssertEqual(data.count, data3.count)
    XCTAssertEqual(data.count, 4)
    XCTAssertNotEqual(data, data3, "Expected 'data': \(hexString(data)) to NOT equal: 'data3':  \(hexString(data3)), but it did.")
}

This simple unit test fails, but it should not fail. Meaning, data and data3 is clearly not the same, since we see [0xA, 0xB, 0xC, 0xD] != [0xA, 0xB, 0xC, 0xE]. And we assert using XCTAssertNotEqual that they should not equal, but this assertion fails, meaning Swift believe them to be equal.

I printed the contents of data and data3 respectively using the nested method hexString, we get this error message when running the test in Xcode:

XCTAssertNotEqual failed: ("4 bytes") is equal to ("4 bytes") - Expected 'data': 0a000000 to NOT equal: 'data3':  0a000000, but it did.

As we can see both data and data3 incorrectly takes the value: 0a000000, as in [0xa, 0x0, 0x0, 0x0].

Changing to using the deprecated initializer on data:

    @available(swift 4.2)
    @available(swift, deprecated: 5, message: "use `init(_:)` instead")
    public init<S>(bytes elements: S) where S : Sequence, S.Element == UInt8

instead of the "correct" (non-deprecated) one that you are using bytes:count, we get the correct behaviour....

So change to:

let data = Data(bytes: [0xA, 0xB, 0xC, 0xD])
let data3 = Data(bytes: [0xA, 0xB, 0xC, 0xE])

And re-run the tests and see that it works.

Furthermore, you can add these assertions:

XCTAssertEqual(hexString(data), "0a0b0c0d")
XCTAssertEqual(hexString(data3), "0a0b0c0e")

I will make sure to report this as a Swift bug.

Upvotes: 3

Related Questions