Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement WebSocket handshake #16

Merged
merged 8 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"))
}
}
}
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