Reputation: 876
I'm facing an issue with 0.01% of user base having problems generating signature. Basically, they are able to generate key pair but the resulting signature is invalid when validated with the public key.
The signature is being created with the following code.
@objc public func signData(
_ text: String,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
guard let privateKey = self.getPrivateKey() else {
reject(nil, "error fetching private key", nil)
return
}
let data = text.data(using: .utf8)!
var error: Unmanaged<CFError>?
let signature = SecKeyCreateSignature(privateKey, .ecdsaSignatureMessageX962SHA256, data as CFData, &error) as Data?
if error != nil {
reject(nil, "Error in creating signature", nil)
return
}
resolve(signature!.base64EncodedString())
}
The keypair is generated as follows:
@objc public func generateKeyPair(
_ resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) -> Void {
let access = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.privateKeyUsage,
nil)!
let attributes: NSDictionary = [
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits: 256,
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, // Enable secure enclave
kSecPrivateKeyAttrs: [
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, // Do not share across devices with same appleid
kSecAttrIsPermanent: true,
kSecAttrApplicationTag: privateKeyTag,
kSecAttrAccessControl: access
],
kSecPublicKeyAttrs: [
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, // Do not share across devices with same appleid
kSecAttrIsPermanent: true,
kSecAttrApplicationTag: publicKeyTag,
]
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes, &error) else {
reject(nil, "error creating private key", error!.takeRetainedValue() as Error)
return
}
let publicKey = SecKeyCopyPublicKey(privateKey)
if publicKey == nil {
reject(nil, "error creating public key", nil)
return
}
resolve(self.PEMFormattedPublicKey(key: publicKey!))
}
private func PEMFormattedPublicKey(key: SecKey) -> String? {
guard let publicKeyData = self.dataForKey(key: key) else {
return nil
}
// ans1 headers format. From https://lapo.it/asn1js/#MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEo0WcFrFClvBE8iZ1Gdlj-mTgZoJWMNE3kDKTfRny2iaguwuSYxo-jCnXqzkR-kyFK9CR3D-pypD1sGb1BnfWAA
// Requred for 256 EC keys
let ecHeader: [UInt8] = [
0x30, 0x59, // Sequence
0x30, 0x13, // Algorithm identifier
0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, // (ANSI X9.62 public key type)
0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, // (ANSI X9.62 named elliptic curve)
0x07, 0x03, 0x42, 0x00 // bit headers
]
var asn1 = Data()
asn1.append(Data(ecHeader))
asn1.append(publicKeyData as Data)
let encoded = asn1.base64EncodedString(options: .lineLength64Characters)
let pemString = "-----BEGIN PUBLIC KEY-----\r\n\(encoded)\r\n-----END PUBLIC KEY-----\r\n"
return pemString
}
And in the backend service, the check is made with the following code:
const publicKey = Buffer.from(publicKeyB64, 'base64').toString('utf-8');
const key = {
key: publicKey,
};
const verifier = createVerify('SHA256');
verifier.update(challenge);
return verifier.verify(key, digest, 'base64');
This is strange since is only happening to a very few users. iOS versions ranging from 17 to 18.1.
Sample of corrupted digest
challenge: ac13288a-3ef7-4eb8-9198-5cb7f3a18665
signature: MEUCIEQMq6unQLES+vBMhalDG0ZxTX9TNWukb63Qqd7W4ReRAiEAyqTwMMQC/HkU4AbLmwbBTm6DBFDpA8yo+3Cigg0H0VA=
publicKeyB64: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRXJENjNrSXdRaC83bVNia1R1QllUUEJtSGdOVmwNCnJueE4rNGVER2pHSDNRZmdVaGhKNlZYTjVSWTZhVW9iRjd3MjdFQXMwZ0JHaTdjOXdPdndQbmhxNEE9PQ0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo=
Upvotes: 0
Views: 66