mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Validate signatures without bouncycastle
This commit is contained in:
parent
9a06227ced
commit
f3e83f9954
5 changed files with 108 additions and 35 deletions
|
@ -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'
|
||||||
|
|
||||||
|
|
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
|
@ -58,7 +58,3 @@
|
||||||
-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.** { *; }
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
95
contrib/publickeymetadata.js
Normal file
95
contrib/publickeymetadata.js
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue