Skip to content

Commit

Permalink
feat: Implement nonce signature (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
gnarea authored Aug 11, 2020
1 parent 8fc146e commit aae0c50
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 68 deletions.
2 changes: 2 additions & 0 deletions src/main/kotlin/tech/relaycorp/relaynet/OIDs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ internal object OIDs {
CLIENT_REGISTRATION_PREFIX.branch("0").intern()
val CRA_COUNTERSIGNATURE: ASN1ObjectIdentifier =
CLIENT_REGISTRATION_PREFIX.branch("1").intern()

val NONCE_SIGNATURE: ASN1ObjectIdentifier = RELAYNET.branch("3").intern()
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import java.security.PublicKey
/**
* Relaynet-specific, CMS SignedData representation.
*/
class SignedData(internal val bcSignedData: CMSSignedData) {
internal class SignedData(internal val bcSignedData: CMSSignedData) {
/**
* The signed plaintext, if it was encapsulated.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package tech.relaycorp.relaynet.messages.control

import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DEROctetString
import tech.relaycorp.relaynet.OIDs
import tech.relaycorp.relaynet.crypto.SignedData
import tech.relaycorp.relaynet.crypto.SignedDataException
import tech.relaycorp.relaynet.messages.InvalidMessageException
import tech.relaycorp.relaynet.wrappers.asn1.ASN1Exception
import tech.relaycorp.relaynet.wrappers.asn1.ASN1Utils
import tech.relaycorp.relaynet.wrappers.x509.Certificate
import java.security.PrivateKey

/**
* Nonce signature.
*/
class NonceSignature(val nonce: ByteArray, val signerCertificate: Certificate) {
/**
* Serialize signature.
*/
fun serialize(privateKey: PrivateKey): ByteArray {
val plaintext = ASN1Utils.serializeSequence(
arrayOf(OIDs.NONCE_SIGNATURE, DEROctetString(nonce)),
false
)
val signedData = SignedData.sign(
plaintext,
privateKey,
signerCertificate,
encapsulatedCertificates = setOf(signerCertificate)
)
return signedData.serialize()
}

companion object {
/**
* Deserialize and validate nonce signature.
*/
@Throws(InvalidMessageException::class)
fun deserialize(serialization: ByteArray): NonceSignature {
val signedData = try {
SignedData.deserialize(serialization).also { it.verify() }
} catch (exc: SignedDataException) {
throw InvalidMessageException("SignedData value is invalid", exc)
}
val sequence = try {
ASN1Utils.deserializeSequence(signedData.plaintext!!)
} catch (exc: ASN1Exception) {
throw InvalidMessageException("Signature plaintext is not a DER sequence", exc)
}
if (sequence.size < 2) {
throw InvalidMessageException(
"Signature sequence should have at least 2 items (got ${sequence.size})"
)
}
val oid = ASN1Utils.deserializeOID(sequence.first() as ASN1TaggedObject)
if (oid != OIDs.NONCE_SIGNATURE) {
throw InvalidMessageException("Signature OID is invalid (got ${oid.id})")
}
val nonce = DEROctetString.getInstance(sequence[1] as ASN1TaggedObject, false)
return NonceSignature(nonce.octets, signedData.signerCertificate!!)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package tech.relaycorp.relaynet.wrappers.asn1

class ASN1Exception(message: String) : Exception(message)
class ASN1Exception(message: String, cause: Throwable? = null) : Exception(message, cause)
14 changes: 14 additions & 0 deletions src/main/kotlin/tech/relaycorp/relaynet/wrappers/asn1/ASN1Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package tech.relaycorp.relaynet.wrappers.asn1
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DERGeneralizedTime
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERTaggedObject
Expand Down Expand Up @@ -49,4 +51,16 @@ internal object ASN1Utils {
val dateUTC = date.withZoneSameInstant(ZoneOffset.UTC)
return DERGeneralizedTime(dateUTC.format(BER_DATETIME_FORMATTER))
}

@Throws(ASN1Exception::class)
fun deserializeOID(
oidSerialized: ASN1TaggedObject,
explicitTagging: Boolean = false
): ASN1ObjectIdentifier {
return try {
ASN1ObjectIdentifier.getInstance(oidSerialized, explicitTagging)
} catch (exc: IllegalArgumentException) {
throw ASN1Exception("Value is not an OID", exc)
}
}
}
36 changes: 0 additions & 36 deletions src/test/kotlin/tech/relaycorp/relaynet/DummyCertPath.kt

This file was deleted.

31 changes: 31 additions & 0 deletions src/test/kotlin/tech/relaycorp/relaynet/FullCertPath.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package tech.relaycorp.relaynet

import java.time.ZonedDateTime

object FullCertPath {
private val tomorrow = ZonedDateTime.now().plusDays(1)

val PUBLIC_GW = issueGatewayCertificate(
KeyPairSet.PUBLIC_GW.public,
KeyPairSet.PUBLIC_GW.private,
tomorrow
)
val PRIVATE_GW = issueGatewayCertificate(
KeyPairSet.PRIVATE_GW.public,
KeyPairSet.PUBLIC_GW.private,
tomorrow,
PUBLIC_GW
)
val PRIVATE_ENDPOINT = issueEndpointCertificate(
KeyPairSet.PRIVATE_ENDPOINT.public,
KeyPairSet.PRIVATE_GW.private,
tomorrow,
PRIVATE_GW
)
val PDA = issueParcelDeliveryAuthorization(
KeyPairSet.PDA_GRANTEE.public,
KeyPairSet.PRIVATE_ENDPOINT.private,
tomorrow,
PRIVATE_ENDPOINT
)
}
10 changes: 10 additions & 0 deletions src/test/kotlin/tech/relaycorp/relaynet/KeyPairSet.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package tech.relaycorp.relaynet

import tech.relaycorp.relaynet.wrappers.generateRSAKeyPair

object KeyPairSet {
val PUBLIC_GW = generateRSAKeyPair()
val PRIVATE_GW = generateRSAKeyPair()
val PRIVATE_ENDPOINT = generateRSAKeyPair()
val PDA_GRANTEE = generateRSAKeyPair()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.bouncycastle.asn1.DERNull
import org.bouncycastle.asn1.DEROctetString
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.assertThrows
import tech.relaycorp.relaynet.DummyCertPath
import tech.relaycorp.relaynet.FullCertPath
import tech.relaycorp.relaynet.messages.InvalidMessageException
import tech.relaycorp.relaynet.wrappers.asn1.ASN1Exception
import tech.relaycorp.relaynet.wrappers.asn1.ASN1Utils
Expand All @@ -20,31 +20,31 @@ class ClientRegistrationTest {
@Test
fun `Client certificate should be serialized`() {
val registration =
ClientRegistration(DummyCertPath.endpointCert, DummyCertPath.privateGatewayCert)
ClientRegistration(FullCertPath.PRIVATE_ENDPOINT, FullCertPath.PRIVATE_GW)

val serialization = registration.serialize()

val sequence = ASN1Utils.deserializeSequence(serialization)
val clientCertificateASN1 =
DEROctetString.getInstance(sequence.first() as ASN1TaggedObject, false)
assertEquals(
DummyCertPath.endpointCert.serialize().asList(),
FullCertPath.PRIVATE_ENDPOINT.serialize().asList(),
clientCertificateASN1.octets.asList()
)
}

@Test
fun `Server certificate should be serialized`() {
val registration =
ClientRegistration(DummyCertPath.endpointCert, DummyCertPath.privateGatewayCert)
ClientRegistration(FullCertPath.PRIVATE_ENDPOINT, FullCertPath.PRIVATE_GW)

val serialization = registration.serialize()

val sequence = ASN1Utils.deserializeSequence(serialization)
val serverCertificateASN1 =
DEROctetString.getInstance(sequence[1] as ASN1TaggedObject, false)
assertEquals(
DummyCertPath.privateGatewayCert.serialize().asList(),
FullCertPath.PRIVATE_GW.serialize().asList(),
serverCertificateASN1.octets.asList()
)
}
Expand Down Expand Up @@ -99,7 +99,7 @@ class ClientRegistrationTest {
fun `Invalid server certificates should be refused`() {
val invalidSerialization = ASN1Utils.serializeSequence(
arrayOf(
DEROctetString(DummyCertPath.endpointCert.serialize()),
DEROctetString(FullCertPath.PRIVATE_ENDPOINT.serialize()),
DERNull.INSTANCE
), false
)
Expand All @@ -118,14 +118,14 @@ class ClientRegistrationTest {
@Test
fun `Valid registration should be accepted`() {
val registration =
ClientRegistration(DummyCertPath.endpointCert, DummyCertPath.privateGatewayCert)
ClientRegistration(FullCertPath.PRIVATE_ENDPOINT, FullCertPath.PRIVATE_GW)
val serialization = registration.serialize()

val registrationDeserialized = ClientRegistration.deserialize(serialization)

assertEquals(DummyCertPath.endpointCert, registrationDeserialized.clientCertificate)
assertEquals(FullCertPath.PRIVATE_ENDPOINT, registrationDeserialized.clientCertificate)
assertEquals(
DummyCertPath.privateGatewayCert,
FullCertPath.PRIVATE_GW,
registrationDeserialized.serverCertificate
)
}
Expand Down
Loading

0 comments on commit aae0c50

Please sign in to comment.