ProtocolTest.kt
⎇
Raw
1package de.upapp
2
3import org.bouncycastle.asn1.x500.X500Name
4import org.bouncycastle.asn1.x509.BasicConstraints
5import org.bouncycastle.asn1.x509.Extension
6import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
7import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
8import org.bouncycastle.crypto.util.PrivateKeyFactory
9import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
10import org.bouncycastle.tls.Certificate
11import org.bouncycastle.tls.CipherSuite
12import org.bouncycastle.tls.DefaultTlsServer
13import org.bouncycastle.tls.HashAlgorithm
14import org.bouncycastle.tls.SignatureAlgorithm
15import org.bouncycastle.tls.SignatureAndHashAlgorithm
16import org.bouncycastle.tls.TlsContext
17import org.bouncycastle.tls.crypto.TlsCryptoParameters
18import org.bouncycastle.tls.crypto.impl.bc.BcDefaultTlsCredentialedSigner
19import org.bouncycastle.tls.PSKTlsServer
20import org.bouncycastle.tls.TlsPSKIdentityManager
21import org.bouncycastle.tls.TlsServerProtocol
22import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto
23import org.junit.Assert.assertArrayEquals
24import org.junit.Assert.assertEquals
25import org.junit.Assert.assertNull
26import org.junit.Assert.assertThrows
27import org.junit.Test
28import java.io.IOException
29import java.io.InputStream
30import java.io.OutputStream
31import java.math.BigInteger
32import java.security.KeyPairGenerator
33import java.security.SecureRandom
34import java.security.cert.X509Certificate
35import java.util.Date
36
37class ProtocolTest {
38 @Test
39 fun decodesSegments() {
40 assertNull(decode(frame(7, TRANSMIT, u16(3) + byteArrayOf(1, 2))))
41 val (s, size) = decode(frame(0xC0E1, TRANSMIT, u16(2) + byteArrayOf(1, 2)) + frame(0, HEARTBEAT))!!
42 assertEquals(0xC0E1, s.port)
43 assertArrayEquals(byteArrayOf(1, 2), s.data)
44 assertEquals(7, size)
45 // Example from the MIBBridge protocol notes: OPEN from port 0xC0E1 to port 80.
46 assertArrayEquals(byteArrayOf(0xE1.toByte(), 0xC0.toByte(), 1, 0x50, 0), frame(0xC0E1, OPEN, u16(80)))
47 }
48
49 @Test
50 fun plainHttpWithChunkedBody() {
51 val tunnel = fakeCar { _, input, output ->
52 input.readHead()
53 output.write("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nWVW\r\n2\r\nZZ\r\n0\r\n\r\n".toByteArray())
54 }
55 val r = tunnel.open(80).use { exchange(it.input, it.output, "GET", "h", "/car/info/vin") }
56 assertEquals(200, r.status)
57 assertEquals("WVWZZ", r.text)
58 }
59
60 @Test
61 fun pskSessionMatchesCarCipher() {
62 val tunnel = fakeCar { _, input, output ->
63 val tls = TlsServerProtocol(input, output)
64 tls.accept(object : PSKTlsServer(BcTlsCrypto(SecureRandom()), object : TlsPSKIdentityManager {
65 override fun getHint(): ByteArray? = null
66 override fun getPSK(identity: ByteArray) = if (identity.decodeToString() == "user") "secret".toByteArray() else null
67 }) {
68 override fun getSupportedCipherSuites() = intArrayOf(CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256)
69 })
70 val head = tls.inputStream.readHead()!!
71 val body = """{"data": [], "request": "${head.lines().first()}"}"""
72 tls.outputStream.write("HTTP/1.1 200 OK\r\nContent-Length: ${body.length}\r\n\r\n$body".toByteArray())
73 tls.close()
74 }
75 val r = tunnel.open(443).use { ch ->
76 val tls = sessionTls(ch.input, ch.output, "user", "secret")
77 exchange(tls.inputStream, tls.outputStream, "GET", "h", "/car/batteries").also { runCatching { tls.close() } }
78 }
79 assertEquals("""{"data": [], "request": "GET /car/batteries HTTP/1.1"}""", r.text)
80 }
81
82 @Test
83 fun pinMatchesMapsAndMore() {
84 // Computed with the maps+more algorithm in Python.
85 assertEquals("304163", pin(ByteArray(16) { it.toByte() }, ByteArray(16) { (it + 16).toByte() }))
86 }
87
88 @Test
89 fun registrationChecksCertificateAndAgreesOnPin() {
90 val ca = TestCa()
91 var serverPin = ""
92 val tunnel = fakeCar { _, input, output ->
93 val tls = TlsServerProtocol(input, output)
94 tls.accept(object : DefaultTlsServer(BcTlsCrypto(SecureRandom())) {
95 override fun getSupportedCipherSuites() = intArrayOf(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256)
96 override fun getRSASignerCredentials() = ca.signer(context, crypto as BcTlsCrypto)
97 override fun notifyHandshakeComplete() {
98 super.notifyHandshakeComplete()
99 serverPin = registrationPin(context)
100 }
101 })
102 tls.inputStream.readHead()
103 tls.outputStream.write("HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\nuser,pass".toByteArray())
104 tls.close()
105 }
106 val r = tunnel.open(443).use { ch ->
107 val (tls, pin) = registrationTls(ch.input, ch.output, ca.root)
108 assertEquals(serverPin, pin)
109 assertEquals(6, pin.length)
110 exchange(tls.inputStream, tls.outputStream, "GET", "h", "/auth/x").also { runCatching { tls.close() } }
111 }
112 assertEquals("user,pass", r.text)
113
114 val stranger = TestCa()
115 val ch = tunnel.open(443)
116 assertThrows(IOException::class.java) { registrationTls(ch.input, ch.output, stranger.root) }
117 }
118
119 /** A root CA and a server certificate signed by it. The server certificate expired an hour ago. */
120 private class TestCa {
121 private val gen = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }
122 private val rootKey = gen.generateKeyPair()
123 private val leafKey = gen.generateKeyPair()
124 private val now = System.currentTimeMillis()
125 val root = cert("CN=Test CA", rootKey.public, true, now - 86_400_000, now + 86_400_000)
126 private val leaf = cert("CN=Car", leafKey.public, false, now - 7_200_000, now - 3_600_000)
127
128 private fun cert(subject: String, key: java.security.PublicKey, ca: Boolean, from: Long, to: Long): X509Certificate {
129 val builder = JcaX509v3CertificateBuilder(X500Name("CN=Test CA"), BigInteger.valueOf(from), Date(from), Date(to), X500Name(subject), key)
130 .addExtension(Extension.basicConstraints, true, BasicConstraints(ca))
131 return JcaX509CertificateConverter().getCertificate(builder.build(JcaContentSignerBuilder("SHA256withRSA").build(rootKey.private)))
132 }
133
134 fun signer(context: TlsContext, crypto: BcTlsCrypto) = BcDefaultTlsCredentialedSigner(
135 TlsCryptoParameters(context), crypto, PrivateKeyFactory.createKey(leafKey.private.encoded),
136 Certificate(arrayOf(crypto.createCertificate(leaf.encoded), crypto.createCertificate(root.encoded))),
137 SignatureAndHashAlgorithm(HashAlgorithm.sha256, SignatureAlgorithm.rsa),
138 )
139 }
140
141 @Test
142 fun reusesSessionAndRetriesAfterCarCloses() {
143 var connections = 0
144 val tunnel = fakeCar { port, input, output ->
145 if (port == 80) {
146 input.readHead()
147 output.write("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nVIN".toByteArray())
148 return@fakeCar
149 }
150 val n = ++connections
151 val tls = TlsServerProtocol(input, output)
152 tls.accept(object : PSKTlsServer(BcTlsCrypto(SecureRandom()), object : TlsPSKIdentityManager {
153 override fun getHint(): ByteArray? = null
154 override fun getPSK(identity: ByteArray) = "secret".toByteArray()
155 }) {
156 override fun getSupportedCipherSuites() = intArrayOf(CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256)
157 })
158 // The car closes after three requests without notice.
159 repeat(3) {
160 val path = tls.inputStream.readHead()?.lines()?.first()?.split(' ')?.get(1) ?: return@repeat
161 val body = "$n $path"
162 tls.outputStream.write("HTTP/1.1 200 OK\r\nContent-Length: ${body.length}\r\n\r\n$body".toByteArray())
163 }
164 tls.close()
165 }
166 val car = Car(tunnel, storedCredentials("user,secret"))
167 car.connect {}
168 assertEquals("1 /a", car.get("a"))
169 assertEquals("1 /b", car.get("b"))
170 assertEquals("2 /c", car.get("c"))
171 assertEquals(2, connections)
172 }
173
174 private fun InputStream.readHead(): String? {
175 val sb = StringBuilder()
176 while (!sb.endsWith("\r\n\r\n")) sb.append(read().takeIf { it >= 0 }?.toChar() ?: return null)
177 return sb.toString()
178 }
179
180 private fun fakeCar(serve: (port: Int, InputStream, OutputStream) -> Unit) = CarSide(serve).tunnel
181}
182