From f3e83f9954a9d2fa7dea9d4e304fc0f0ee51795c Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 8 Aug 2022 02:00:00 +0200 Subject: [PATCH] Validate signatures without bouncycastle --- app/build.gradle | 1 - app/proguard-rules.pro | 6 +- .../android/u2f/U2FSignatureValidation.kt | 37 +++----- app/src/main/res/values/strings.xml | 4 - contrib/publickeymetadata.js | 95 +++++++++++++++++++ 5 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 contrib/publickeymetadata.js diff --git a/app/build.gradle b/app/build.gradle index b99601a..90dc906 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,7 +214,6 @@ dependencies { implementation 'org.apache.commons:commons-text:1.6' implementation 'org.whispersystems:curve25519-java:0.5.0' - implementation 'org.bouncycastle:bcutil-jdk15on:1.70' implementation 'com.google.zxing:core:3.3.3' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2708626..fa114c6 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -57,8 +57,4 @@ -keep class org.whispersystems.curve25519.NativeCurve25519Provider {} -keep class org.whispersystems.curve25519.JavaCurve25519Provider {} -keep class org.whispersystems.curve25519.J2meCurve25519Provider {} --keep class org.whispersystems.curve25519.OpportunisticCurve25519Provider {} - -# bouncycastle --keep class org.bouncycastle.jcajce.provider.** { *; } --keep class org.bouncycastle.jce.provider.** { *; } \ No newline at end of file +-keep class org.whispersystems.curve25519.OpportunisticCurve25519Provider {} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/u2f/U2FSignatureValidation.kt b/app/src/main/java/io/timelimit/android/u2f/U2FSignatureValidation.kt index 7ba8835..0633e9a 100644 --- a/app/src/main/java/io/timelimit/android/u2f/U2FSignatureValidation.kt +++ b/app/src/main/java/io/timelimit/android/u2f/U2FSignatureValidation.kt @@ -16,26 +16,13 @@ package io.timelimit.android.u2f import io.timelimit.android.u2f.protocol.U2FResponse -import org.bouncycastle.asn1.sec.SECNamedCurves -import org.bouncycastle.crypto.CryptoException -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.jce.spec.ECParameterSpec -import org.bouncycastle.jce.spec.ECPublicKeySpec import java.security.KeyFactory -import java.security.Security import java.security.Signature +import java.security.spec.InvalidKeySpecException +import java.security.spec.X509EncodedKeySpec object U2FSignatureValidation { - init { - Security.removeProvider("BC") - Security.addProvider(BouncyCastleProvider()) - } - - private val curve = SECNamedCurves.getByName("secp256r1") - private val ecParamSpec = ECParameterSpec(curve.getCurve(), curve.getG(), curve.getN(), curve.getH()) - - // based on https://github.com/Yubico/java-u2flib-server/blob/dd44d3cdce4eeaeb517f2acd1fd520d5a42ce752/u2flib-server-core/src/main/java/com/yubico/u2f/crypto/BouncyCastleCrypto.java fun validate( applicationId: ByteArray, challenge: ByteArray, @@ -54,22 +41,22 @@ object U2FSignatureValidation { if (publicKey.size != 65 || publicKey[0] != 4.toByte()) return false - val point = curve.getCurve().decodePoint(publicKey) + val verifier = Signature.getInstance("SHA256withECDSA") - val decodedPublicKey = KeyFactory.getInstance("EC", "BC").generatePublic( - ECPublicKeySpec(point, ecParamSpec) + verifier.initVerify( + KeyFactory + .getInstance("EC") + .generatePublic( + X509EncodedKeySpec( + byteArrayOf(48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61, 3, 1, 7, 3, 66, 0) + publicKey + ) + ) ) - val verifier = Signature.getInstance("SHA256withECDSA", "BC") - - verifier.initVerify(decodedPublicKey) - verifier.update(signedData) return verifier.verify(response.signature) - } catch (ex: CryptoException) { - return false - } catch (ex: IllegalArgumentException) { + } catch (ex: InvalidKeySpecException) { return false } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 87eae0b..4a2732e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,10 +107,6 @@ (Apache License 2.0) \nWire (Apache License 2.0) - \nBouncy Castle - (MIT License) - \njava-u2flib-server - (BSD License) Error diagnose diff --git a/contrib/publickeymetadata.js b/contrib/publickeymetadata.js new file mode 100644 index 0000000..a1b5f19 --- /dev/null +++ b/contrib/publickeymetadata.js @@ -0,0 +1,95 @@ +// https://www.rfc-editor.org/rfc/rfc5280.html#appendix-A.1 +// https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/ +// https://www.ietf.org/rfc/rfc5480.txt + +/* +SubjectPublicKeyInfo ::= SEQUENCE { + algorithm AlgorithmIdentifier, + subjectPublicKey BIT STRING } + + +AlgorithmIdentifier ::= SEQUENCE { + algorithm OBJECT IDENTIFIER, + parameters ANY DEFINED BY algorithm OPTIONAL } + -- contains a value of the type + -- registered for use with the + -- algorithm object identifier value + +id-ecPublicKey OBJECT IDENTIFIER ::= { + iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } + +secp256r1 OBJECT IDENTIFIER ::= { + iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) + prime(1) 7 } +*/ + +function encodeValue(type, valueBuffer) { + const length = valueBuffer.length + + if (length >= 128) throw new Error('long encoding not supported') + + const typeBuffer = Buffer.from([type]) + const lengthBuffer = Buffer.from([length]) + + return Buffer.concat([typeBuffer, lengthBuffer, valueBuffer]) +} + +function encodeBitstring(valueBuffer) { + return encodeValue(3, Buffer.concat([Buffer.from([0 /* no unused bits */]), valueBuffer])) +} + +function encodeOid(value) { + const numbers = [ + value[0] * 40 + value[1], + ...value.slice(2) + ] + + const result = [] + + for (let number of numbers) { + const tempResult = [] + while (number) { tempResult.push(128 | (number % 128)); number = Math.floor(number / 128) } + tempResult.reverse() + tempResult[tempResult.length - 1] &= 127 + + for (const item of tempResult) result.push(item) + } + + return encodeValue(6, Buffer.from(result)) +} + +function encodeSequence(itemBuffers) { + return encodeValue(0x30, Buffer.concat(itemBuffers)) +} + +function encodeAlogrithmIdentifier() { + return encodeSequence([ + encodeOid([1, 2, 840, 10045, 2, 1]), + encodeOid([1, 2, 840, 10045, 3, 1, 7]) + ]) +} + +function encodePublicKeyInfo() { + const dummyKey = Buffer.alloc(65) + for (let i = 0; i < dummyKey.length; i++) dummyKey.writeUInt8(255, i) + + return encodeSequence([ + encodeAlogrithmIdentifier(), + encodeBitstring(dummyKey) + ]) +} + +const result = encodePublicKeyInfo() + +// compare result with https://github.com/ashtuchkin/u2f/blob/2e45ea40acd8c3ad6c113cd1b4e0558acc4cda3a/index.js#L21 +if (result.toString('hex') !== '3059301306072a8648ce3d020106082a8648ce3d030107034200' + ('ff'.repeat(65))) throw new Error() + +const prefix = result.slice(0, result.length - 65) + +console.log(prefix.toString('hex')) +console.log(prefix.toString('base64')) + +const prefixNumbers = []; for (let i = 0; i < prefix.length; i++) prefixNumbers.push(prefix.readInt8(i)) +const kotlinByteArray = 'byteArrayOf(' + prefixNumbers.join(', ') + ')' + +console.log(kotlinByteArray)