Validate signatures without bouncycastle

This commit is contained in:
Jonas Lochmann 2022-08-08 02:00:00 +02:00
parent 9a06227ced
commit f3e83f9954
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
5 changed files with 108 additions and 35 deletions

View file

@ -214,7 +214,6 @@ dependencies {
implementation 'org.apache.commons:commons-text:1.6' implementation 'org.apache.commons:commons-text:1.6'
implementation 'org.whispersystems:curve25519-java:0.5.0' implementation 'org.whispersystems:curve25519-java:0.5.0'
implementation 'org.bouncycastle:bcutil-jdk15on:1.70'
implementation 'com.google.zxing:core:3.3.3' implementation 'com.google.zxing:core:3.3.3'

View file

@ -57,8 +57,4 @@
-keep class org.whispersystems.curve25519.NativeCurve25519Provider {} -keep class org.whispersystems.curve25519.NativeCurve25519Provider {}
-keep class org.whispersystems.curve25519.JavaCurve25519Provider {} -keep class org.whispersystems.curve25519.JavaCurve25519Provider {}
-keep class org.whispersystems.curve25519.J2meCurve25519Provider {} -keep class org.whispersystems.curve25519.J2meCurve25519Provider {}
-keep class org.whispersystems.curve25519.OpportunisticCurve25519Provider {} -keep class org.whispersystems.curve25519.OpportunisticCurve25519Provider {}
# bouncycastle
-keep class org.bouncycastle.jcajce.provider.** { *; }
-keep class org.bouncycastle.jce.provider.** { *; }

View file

@ -16,26 +16,13 @@
package io.timelimit.android.u2f package io.timelimit.android.u2f
import io.timelimit.android.u2f.protocol.U2FResponse 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.KeyFactory
import java.security.Security
import java.security.Signature import java.security.Signature
import java.security.spec.InvalidKeySpecException
import java.security.spec.X509EncodedKeySpec
object U2FSignatureValidation { 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( fun validate(
applicationId: ByteArray, applicationId: ByteArray,
challenge: ByteArray, challenge: ByteArray,
@ -54,22 +41,22 @@ object U2FSignatureValidation {
if (publicKey.size != 65 || publicKey[0] != 4.toByte()) return false 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( verifier.initVerify(
ECPublicKeySpec(point, ecParamSpec) 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) verifier.update(signedData)
return verifier.verify(response.signature) return verifier.verify(response.signature)
} catch (ex: CryptoException) { } catch (ex: InvalidKeySpecException) {
return false
} catch (ex: IllegalArgumentException) {
return false return false
} }
} }

View file

@ -107,10 +107,6 @@
(<a href="https://github.com/zxing/zxing/blob/master/LICENSE">Apache License 2.0</a>) (<a href="https://github.com/zxing/zxing/blob/master/LICENSE">Apache License 2.0</a>)
\n<a href="https://github.com/square/wire/blob/master/README.md">Wire</a> \n<a href="https://github.com/square/wire/blob/master/README.md">Wire</a>
(<a href="https://github.com/square/wire/blob/master/LICENSE.txt">Apache License 2.0</a>) (<a href="https://github.com/square/wire/blob/master/LICENSE.txt">Apache License 2.0</a>)
\n<a href="https://www.bouncycastle.org/">Bouncy Castle</a>
(<a href="https://www.bouncycastle.org/licence.html">MIT License</a>)
\n<a href="https://github.com/Yubico/java-u2flib-server">java-u2flib-server</a>
(<a href="https://github.com/Yubico/java-u2flib-server/blob/master/COPYING">BSD License</a>)
</string> </string>
<string name="about_diagnose_title">Error diagnose</string> <string name="about_diagnose_title">Error diagnose</string>

View file

@ -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)