Skip to content

Commit

Permalink
feat: Implement WebSocket handshake (#16)
Browse files Browse the repository at this point in the history
Fixes #3
  • Loading branch information
gnarea authored Jul 15, 2020
1 parent c9db588 commit 7262b6d
Show file tree
Hide file tree
Showing 9 changed files with 767 additions and 17 deletions.
57 changes: 41 additions & 16 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

buildscript {
ext {
kotlinVersion = '1.3.72'
protobufVersion = '3.12.2'
protobufGradleVersion = '0.8.12'
}
ext.kotlinVersion = '1.3.72'
ext.protobufVersion = '3.12.2'
ext.protobufGradleVersion = '0.8.12'
ext.kotlinCoroutinesVersion = '1.3.7'
ext.ktorVersion = '1.3.2'
ext.okhttpVersion = '4.8.0'
}

plugins {
Expand All @@ -32,36 +33,46 @@ dependencies {

implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")

api('tech.relaycorp:relaynet:1.16.1')

// Handshake nonce signatures
implementation("org.bouncycastle:bcpkix-jdk15on:1.64")

implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion")
testImplementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
testImplementation("com.squareup.okhttp3:mockwebserver:$okhttpVersion")
testImplementation("com.squareup.okio:okio:2.7.0")

// Protobuf
implementation "com.google.protobuf:protobuf-gradle-plugin:$protobufGradleVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.google.protobuf:protobuf-java-util:$protobufVersion"
implementation("com.google.protobuf:protobuf-gradle-plugin:$protobufGradleVersion")
implementation("com.google.protobuf:protobuf-java:$protobufVersion")
implementation("com.google.protobuf:protobuf-java-util:$protobufVersion")

testImplementation("org.jetbrains.kotlin:kotlin-test")

// Use the Kotlin JUnit5 integration.
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.6.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
testImplementation("org.mockito:mockito-inline:3.4.0")
}

java {
withJavadocJar()
withSourcesJar()
}

tasks.withType(KotlinCompile).configureEach {
kotlinOptions.jvmTarget = "1.8"
tasks.withType(KotlinCompile).all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
}

protobuf {
protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" }
}

tasks.withType(KotlinCompile).configureEach {
kotlinOptions.jvmTarget = "1.8"
}

tasks.dokka {
outputFormat = "html"
outputDirectory = "$buildDir/docs/api"
Expand Down Expand Up @@ -124,3 +135,17 @@ spotless {
ktlint().userData(ktlintUserData)
}
}

// Workaround for https://github.com/google/protobuf-gradle-plugin/issues/391
configurations {
"compileProtoPath" {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, "java-runtime"))
}
}
"testCompileProtoPath" {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, "java-runtime"))
}
}
}
3 changes: 2 additions & 1 deletion jacoco.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ jacocoTestCoverageVerification {
limit {
counter = "BRANCH"
value = "MISSEDCOUNT"
maximum = "0".toBigDecimal()
// Workaround for https://github.com/jacoco/jacoco/issues/1036
maximum = "1".toBigDecimal()
}
}
}
Expand Down
67 changes: 67 additions & 0 deletions src/main/kotlin/tech/relaycorp/poweb/PoWebClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package tech.relaycorp.poweb

import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.features.websocket.DefaultClientWebSocketSession
import io.ktor.client.features.websocket.WebSockets
import io.ktor.client.features.websocket.webSocket
import io.ktor.http.cio.websocket.CloseReason
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.close
import io.ktor.http.cio.websocket.readBytes
import io.ktor.util.KtorExperimentalAPI
import tech.relaycorp.poweb.handshake.Challenge
import tech.relaycorp.poweb.handshake.InvalidMessageException
import tech.relaycorp.poweb.handshake.NonceSigner
import tech.relaycorp.poweb.handshake.Response
import java.io.Closeable

class PoWebClient internal constructor(
internal val hostName: String,
internal val port: Int,
internal val useTls: Boolean
) : Closeable {
@KtorExperimentalAPI
internal var ktorClient = HttpClient(OkHttp) {
install(WebSockets)
}

@KtorExperimentalAPI
override fun close() = ktorClient.close()

private val wsUrl = "ws${if (useTls) "s" else ""}://$hostName:$port"

@KtorExperimentalAPI
internal suspend fun wsConnect(
path: String,
block: suspend DefaultClientWebSocketSession.() -> Unit
) = ktorClient.webSocket("${wsUrl}$path", block = block)

companion object {
private const val defaultLocalPort = 276
private const val defaultRemotePort = 443

fun initLocal(port: Int = defaultLocalPort) =
PoWebClient("127.0.0.1", port, false)

fun initRemote(hostName: String, port: Int = defaultRemotePort) =
PoWebClient(hostName, port, true)
}
}

@Throws(PoWebException::class)
internal suspend fun DefaultClientWebSocketSession.handshake(nonceSigners: Array<NonceSigner>) {
if (nonceSigners.isEmpty()) {
throw PoWebException("At least one nonce signer must be specified")
}
val challengeRaw = incoming.receive()
val challenge = try {
Challenge.deserialize(challengeRaw.readBytes())
} catch (exc: InvalidMessageException) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, ""))
throw PoWebException("Server sent an invalid handshake challenge", exc)
}
val nonceSignatures = nonceSigners.map { it.sign(challenge.nonce) }.toTypedArray()
val response = Response(nonceSignatures)
outgoing.send(Frame.Binary(true, response.serialize()))
}
3 changes: 3 additions & 0 deletions src/main/kotlin/tech/relaycorp/poweb/PoWebException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package tech.relaycorp.poweb

class PoWebException(message: String, cause: Throwable? = null) : Exception(message, cause)
33 changes: 33 additions & 0 deletions src/main/kotlin/tech/relaycorp/poweb/handshake/NonceSigner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package tech.relaycorp.poweb.handshake

import org.bouncycastle.cms.CMSProcessableByteArray
import org.bouncycastle.cms.CMSSignedDataGenerator
import org.bouncycastle.cms.CMSTypedData
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
import org.bouncycastle.operator.ContentSigner
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
import tech.relaycorp.relaynet.wrappers.x509.Certificate
import java.security.PrivateKey

class NonceSigner(internal val certificate: Certificate, private val privateKey: PrivateKey) {
fun sign(nonce: ByteArray): ByteArray {
val signedDataGenerator = CMSSignedDataGenerator()

val signerBuilder = JcaContentSignerBuilder("SHA256withRSA")
val contentSigner: ContentSigner = signerBuilder.build(privateKey)
val signerInfoGenerator = JcaSignerInfoGeneratorBuilder(
JcaDigestCalculatorProviderBuilder()
.build()
).build(contentSigner, certificate.certificateHolder)
signedDataGenerator.addSignerInfoGenerator(
signerInfoGenerator
)

signedDataGenerator.addCertificate(certificate.certificateHolder)

val plaintextCms: CMSTypedData = CMSProcessableByteArray(nonce)
val cmsSignedData = signedDataGenerator.generate(plaintextCms, false)
return cmsSignedData.encoded
}
}
Loading

0 comments on commit 7262b6d

Please sign in to comment.