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: Reimplement nonce and parcel delivery signers as detached signatures #94

Merged
merged 2 commits into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/main/kotlin/tech/relaycorp/relaynet/OIDs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ internal object OIDs {
val PNRA_COUNTERSIGNATURE: ASN1ObjectIdentifier =
PRIVATE_NODE_REGISTRATION_PREFIX.branch("1").intern()

val NONCE_SIGNATURE: ASN1ObjectIdentifier = RELAYNET.branch("3").intern()
val DETACHED_SIGNATURE: ASN1ObjectIdentifier = RELAYNET.branch("3").intern()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package tech.relaycorp.relaynet.bindings.pdc

import org.bouncycastle.asn1.ASN1ObjectIdentifier
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.ASN1Utils
import tech.relaycorp.relaynet.wrappers.x509.Certificate
import tech.relaycorp.relaynet.wrappers.x509.CertificateException
import java.security.PrivateKey

/**
* Utility to sign and verify CMS SignedData values where the plaintext is not encapsulated (to
* avoid re-encoding the plaintext for performance reasons), and the signer's certificate is
* encapsulated.
*/
enum class DetachedSignatureType(internal val oid: ASN1ObjectIdentifier) {
PARCEL_DELIVERY(OIDs.DETACHED_SIGNATURE.branch("0").intern()),
NONCE(OIDs.DETACHED_SIGNATURE.branch("1").intern());

/**
* Sign the `plaintext` and return the CMS SignedData serialized.
*/
fun sign(
plaintext: ByteArray,
privateKey: PrivateKey,
signerCertificate: Certificate
): ByteArray {
val safePlaintext = makePlaintextSafe(plaintext)
val signedData = SignedData.sign(
safePlaintext,
privateKey,
signerCertificate,
encapsulatedCertificates = setOf(signerCertificate),
encapsulatePlaintext = false
)
return signedData.serialize()
}

/**
* Verify `signatureSerialized` and return the signer's certificate if valid.
*/
@Throws(InvalidMessageException::class)
fun verify(
signatureSerialized: ByteArray,
expectedPlaintext: ByteArray,
trustedCertificates: List<Certificate>
): Certificate {
val safePlaintext = makePlaintextSafe(expectedPlaintext)
val signedData = try {
SignedData.deserialize(signatureSerialized).also { it.verify(safePlaintext) }
} catch (exc: SignedDataException) {
throw InvalidMessageException("SignedData value is invalid", exc)
}
val signerCertificate = signedData.signerCertificate!!
try {
signerCertificate.getCertificationPath(emptyList(), trustedCertificates)
} catch (exc: CertificateException) {
throw InvalidMessageException("Signer is not trusted", exc)
}
return signerCertificate
}

private fun makePlaintextSafe(plaintext: ByteArray) = ASN1Utils.serializeSequence(
arrayOf(oid, DEROctetString(plaintext)),
false
)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface PDCClient : Closeable {
suspend fun deliverParcel(parcelSerialized: ByteArray)

suspend fun collectParcels(
nonceSigners: Array<NonceSigner>,
nonceSigners: Array<Signer>,
streamingMode: StreamingMode = StreamingMode.KeepAlive
): Flow<ParcelCollection>
}
16 changes: 16 additions & 0 deletions src/main/kotlin/tech/relaycorp/relaynet/bindings/pdc/Signer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package tech.relaycorp.relaynet.bindings.pdc

import tech.relaycorp.relaynet.wrappers.x509.Certificate
import java.security.PrivateKey

/**
* Object to produce detached signatures given a key pair.
*
* @param certificate The certificate of the private node
* @param privateKey The private key of the private node
*/
class Signer(val certificate: Certificate, private val privateKey: PrivateKey) {
fun sign(plaintext: ByteArray, detachedSignatureType: DetachedSignatureType): ByteArray {
return detachedSignatureType.sign(plaintext, privateKey, certificate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ internal class SignedData(internal val bcSignedData: CMSSignedData) {
val isValid = try {
signerInfo.verify(verifier)
} catch (exc: CMSException) {
throw SignedDataException("Invalid signature", exc)
throw SignedDataException("Could not verify signature", exc)
}
if (!isValid) {
throw SignedDataException("Invalid signature")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package tech.relaycorp.relaynet.bindings.pdc

import org.bouncycastle.asn1.DEROctetString
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import tech.relaycorp.relaynet.FullCertPath
import tech.relaycorp.relaynet.KeyPairSet
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.ASN1Utils
import tech.relaycorp.relaynet.wrappers.x509.CertificateException
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class DetachedSignatureTypeTest {
val signatureType = DetachedSignatureType.NONCE

val signerPrivateKey = KeyPairSet.PRIVATE_ENDPOINT.private!!
val signerCertificate = FullCertPath.PRIVATE_ENDPOINT
val plaintext = "the plaintext".toByteArray()

@Nested
inner class Sign {
@Test
fun `Plaintext should not be encapsulated`() {
val serialization = signatureType.sign(plaintext, signerPrivateKey, signerCertificate)

val signedData = SignedData.deserialize(serialization)
assertNull(signedData.plaintext)
}

@Test
fun `Certificate should be encapsulated`() {
val serialization = signatureType.sign(plaintext, signerPrivateKey, signerCertificate)

val signedData = SignedData.deserialize(serialization)
assertNotNull(signedData.signerCertificate)
assertEquals(signerCertificate, signedData.signerCertificate)
}

@Test
fun `Signature should validate`() {
val serialization = signatureType.sign(plaintext, signerPrivateKey, signerCertificate)

val signedData = SignedData.deserialize(serialization)
val expectedPlaintext = ASN1Utils.serializeSequence(
arrayOf(
signatureType.oid,
DEROctetString(plaintext)
),
false
)
signedData.verify(expectedPlaintext)
}
}

@Nested
inner class Verify {
@Test
fun `SignedData value should be valid`() {
val invalidSignedData = SignedData.sign(
byteArrayOf(),
KeyPairSet.PDA_GRANTEE.private, // Key doesn't correspond to certificate
FullCertPath.PRIVATE_ENDPOINT,
setOf(FullCertPath.PRIVATE_ENDPOINT)
).serialize()

val exception = assertThrows<InvalidMessageException> {
signatureType.verify(invalidSignedData, plaintext, listOf(FullCertPath.PRIVATE_GW))
}

assertEquals("SignedData value is invalid", exception.message)
assertTrue(exception.cause is SignedDataException)
}

@Test
fun `Untrusted signers should be refused`() {
val serialization =
signatureType.sign(plaintext, KeyPairSet.PUBLIC_GW.private, FullCertPath.PUBLIC_GW)

val exception = assertThrows<InvalidMessageException> {
signatureType.verify(serialization, plaintext, listOf(FullCertPath.PRIVATE_GW))
}

assertEquals("Signer is not trusted", exception.message)
assertTrue(exception.cause is CertificateException)
}

@Test
fun `Signer certificate should be output if trusted and signature is valid`() {
val serialization = signatureType.sign(plaintext, signerPrivateKey, signerCertificate)

val actualSignerCertificate =
signatureType.verify(serialization, plaintext, listOf(FullCertPath.PRIVATE_GW))

assertEquals(signerCertificate, actualSignerCertificate)
}
}

@Nested
inner class Types {
@Test
fun `PARCEL_DELIVERY should use the right OID`() {
assertEquals(
OIDs.DETACHED_SIGNATURE.branch("0"),
DetachedSignatureType.PARCEL_DELIVERY.oid
)
}

@Test
fun `NONCE should use the right OID`() {
assertEquals(OIDs.DETACHED_SIGNATURE.branch("1"), DetachedSignatureType.NONCE.oid)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class MockPDCClient : PDCClient {
override fun close() = throw NotImplementedError()
override suspend fun deliverParcel(parcelSerialized: ByteArray) = throw NotImplementedError()
override suspend fun collectParcels(
nonceSigners: Array<NonceSigner>,
nonceSigners: Array<Signer>,
streamingMode: StreamingMode
): Flow<ParcelCollection> = flow {
parcelsCollected = true
Expand Down
26 changes: 26 additions & 0 deletions src/test/kotlin/tech/relaycorp/relaynet/bindings/pdc/SignerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tech.relaycorp.relaynet.bindings.pdc

import org.junit.jupiter.api.Test
import tech.relaycorp.relaynet.FullCertPath
import tech.relaycorp.relaynet.KeyPairSet
import kotlin.test.assertEquals

class SignerTest {
private val plaintext = "The plaintext".toByteArray()
private val keyPair = KeyPairSet.PRIVATE_ENDPOINT
private val certificate = FullCertPath.PRIVATE_ENDPOINT
private val signer = Signer(certificate, keyPair.private)

@Test
fun `Signature should be valid`() {
val signatureType = DetachedSignatureType.NONCE
val serialization = signer.sign(plaintext, signatureType)

signatureType.verify(serialization, plaintext, listOf(FullCertPath.PRIVATE_GW))
}

@Test
fun `Signer certificate should be exposed`() {
assertEquals(certificate, signer.certificate)
}
}
Loading