Android & iOS P-256(secp256r1/prime256v1) public key compress and reformat signature

今天在做 Flutter 的 API 接口鉴权的变更,采用 Android KeyStore 和 iOS 的 Secure Enclave 提供的安全能力使用 P-256 来对 API 请求进行签名,服务器端再进行验证。

但是发现不论是 iOS 还是安卓都没有提供一个便捷的方式从 iOS 的SecKeyCopyExternalRepresentation(SecKeyCopyPublicKey) 和 Android 的 KeyPair 中得到 33-bytes 的 compressed public key。

压缩公钥(Compressed Public Key)是一种公钥编码方式,可以将 ECC(椭圆曲线密码学)公钥从 64 个字节压缩为 33 个字节。这种编码方式由一个字节的标识符和32个字节的公钥坐标的一部分(y坐标)组成,从而实现了公钥的压缩。在使用压缩公钥时,可以减少传输的数据量和存储空间,同时保持相同的安全性和加密效果。压缩公钥广泛应用于比特币、以太坊等区块链领域中。

compressed_public_key = y is even?0x02:0x03 + x 

Android

private fun secp256r1JKeyPair(
    packageManager: PackageManager,
    alias: String,
    throwIfNotExists: Boolean = false,
): KeyPair {
    val ks: KeyStore = KeyStore.getInstance(storeProvider).apply { load(null) }
    val keyPair: KeyPair = if (ks.containsAlias(alias)) {
        val entry = ks.getEntry(alias, null)
        if (entry !is KeyStore.PrivateKeyEntry) {
            throw TypeCastException()
        }
        KeyPair(entry.certificate.publicKey, entry.privateKey)
    } else if (throwIfNotExists) {
        throw KeyStoreException("No key was found with the alias $alias.")
    } else {
        val kpg: KeyPairGenerator =
            KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, storeProvider)
        var properties =
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
        val parameterSpec = KeyGenParameterSpec.Builder(alias, properties).apply {
            setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
            setDigests(KeyProperties.DIGEST_SHA256)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && hasStrongBox(packageManager)) {
                setIsStrongBoxBacked(true)
            }
        }.build()
        kpg.initialize(parameterSpec)
        kpg.generateKeyPair()
    }
    return keyPair
}

@OptIn(ExperimentalUnsignedTypes::class)
fun bnUByteArrayToUByteArray(bnUByteArray: UByteArray, expectLength: Int): UByteArray {
    if (bnUByteArray.size == expectLength + 1) {
        return bnUByteArray.sliceArray(1..expectLength)
    }
    if (bnUByteArray.size < expectLength) {
        return UByteArray(expectLength - bnUByteArray.size) { UByte.MIN_VALUE } + bnUByteArray
    }
    return bnUByteArray
}

@OptIn(ExperimentalUnsignedTypes::class)
fun secp256r1PublicKey(
    packageManager: PackageManager,
    alias: String
): UByteArray {
    val kp = secp256r1JKeyPair(packageManager, alias)
    val publicKey = kp.public as ECPublicKey;
    val point = publicKey.w
    val x: BigInteger = point.affineX
    val y: BigInteger = point.affineY
    val xBytes: UByteArray = bnUByteArrayToUByteArray(x.toByteArray().toUByteArray(), 32)

    val yFLag = UByteArray(1)
    yFLag[0] = (if (y.testBit(0)) 0x03 else 0x02).toUByte()
    return yFLag + xBytes
}

@OptIn(ExperimentalUnsignedTypes::class)
fun secp256r1Sign(
    packageManager: PackageManager,
    alias: String,
    payload: ByteArray
): UByteArray {
    val privateKey =
        secp256r1JKeyPair(packageManager, alias).private
    val signature = Signature.getInstance(signatureAlgorithm).run {
        initSign(privateKey)
        update(payload)
        sign()
    }
    val seq = DERSequence.fromByteArray(signature) as DLSequence
    val r = bnUByteArrayToUByteArray(
        (seq.getObjectAt(0) as ASN1Integer).value.toByteArray().toUByteArray(), 32
    )
    val s = bnUByteArrayToUByteArray(
        (seq.getObjectAt(1) as ASN1Integer).value.toByteArray().toUByteArray(), 32
    )
    return r + s
}

@OptIn(ExperimentalUnsignedTypes::class)
private fun byteArrayToHexString(byteArray: UByteArray): String {
    return byteArray.joinToString(separator = "") { it.toString(16).padStart(2, '0') }
}

iOS

static func secp256r1Key(name: String, requiresBiometry: Bool = false) throws -> SecKey {
    let flags: SecAccessControlCreateFlags = requiresBiometry ? [.privateKeyUsage, .userPresence] : .privateKeyUsage
    let access =
    SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                    kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                    flags,
                                    nil)!
    let tag = name.data(using: .utf8)!
    let attributes: [String: Any] = [
        kSecAttrKeyType as String           : kSecAttrKeyTypeEC,
        kSecAttrKeySizeInBits as String     : 256,
        kSecAttrTokenID as String           : kSecAttrTokenIDSecureEnclave,
        kSecPrivateKeyAttrs as String : [
            kSecAttrIsPermanent as String       : true,
            kSecAttrApplicationTag as String    : tag,
            kSecAttrAccessControl as String     : access
        ] as [String : Any]
    ]
  
    var error: Unmanaged<CFError>?
    guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
        throw error!.takeRetainedValue()
    }
  
    return privateKey
}

static func getCompressedPublicKey(key: SecKey) throws -> Data {
    guard let publicKeyData = SecKeyCopyExternalRepresentation(SecKeyCopyPublicKey(key)!, nil) as? Data else {
        throw NSError()
    }
    let x = publicKeyData.dropFirst().prefix(32)
    let y = publicKeyData.subdata(in: Range(33...64))
    return Data([0x02 | (y.last! & 0x01)]) + x
}

static func secp256r1Sign(name: String, payload: Data) -> Data {
    let key = secp256r1Key(name: name)
    var error: Unmanaged<CFError>?
    let asn1signature = SecKeyCreateSignature(key!, .ecdsaSignatureMessageX962SHA256, payload as CFData, &error)! as Data
    let signature = try! ECSignature(asn1: asn1signature)
    return signature.r + signature.s
}

Comments