Reputation: 1059
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
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
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)
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