Skip to content

Commit

Permalink
feat: Allow SignatureBundles to encapsulate plaintext (#67)
Browse files Browse the repository at this point in the history
LTR-180
  • Loading branch information
gnarea authored Oct 25, 2023
1 parent 8d3a7c1 commit ef9f487
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 13 deletions.
6 changes: 5 additions & 1 deletion src/integrationTest/kotlin/MainTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ class MainTest {
now,
)

val member = signatureBundle.verify(TestStubs.PLAINTEXT, TestStubs.TEST_SERVICE_OID)
val (plaintext, member) = signatureBundle.verify(
TestStubs.PLAINTEXT,
TestStubs.TEST_SERVICE_OID,
)

assert(plaintext.contentEquals(TestStubs.PLAINTEXT))
assert(member.orgName == TestStubs.ORG_NAME)
assert(member.userName == TestStubs.USER_NAME)
}
Expand Down
25 changes: 16 additions & 9 deletions src/main/kotlin/tech/relaycorp/veraid/SignatureBundle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,30 @@ public class SignatureBundle internal constructor(
* @param plaintext The plaintext whose signature is to be verified.
* @param serviceOid The OID of the service to which the signature is bound.
* @param date The date against which to verify the signature.
* @return The member that signed the signature, if verification succeeds.
* @return The member that produced the signature and the respective plaintext.
* @throws SignatureException If the bundle is invalid.
*/
public suspend fun verify(
plaintext: ByteArray,
plaintext: ByteArray?,
serviceOid: String,
date: ZonedDateTime,
): Member = verify(plaintext, serviceOid, date..date)
): SignatureBundleVerification = verify(plaintext, serviceOid, date..date)

/**
* Verify the bundle.
*
* @param plaintext The plaintext whose signature is to be verified.
* @param plaintext The plaintext whose signature is to be verified (if the bundle isn't
* expected to encapsulate it).
* @param serviceOid The OID of the service to which the signature is bound.
* @param datePeriod The period against which to verify the signature.
* @return The member that signed the signature, if verification succeeds.
* @return The member that produced the signature and the respective plaintext.
* @throws SignatureException If the bundle is invalid.
*/
public suspend fun verify(
plaintext: ByteArray,
plaintext: ByteArray?,
serviceOid: String,
datePeriod: DatePeriod? = null,
): Member {
): SignatureBundleVerification {
val now = ZonedDateTime.now()
val verificationPeriod = datePeriod ?: now..now
if (verificationPeriod.endInclusive < verificationPeriod.start) {
Expand All @@ -90,11 +91,16 @@ public class SignatureBundle internal constructor(
)
}

return try {
val member = try {
memberIdBundle.verify(serviceOid, signaturePeriodIntersection)
} catch (exc: PkiException) {
throw SignatureException("Member id bundle is invalid", exc)
}

return SignatureBundleVerification(
signedData.plaintext ?: plaintext!!,
member,
)
}

private fun getSignatureMetadata(): SignatureMetadata {
Expand Down Expand Up @@ -133,6 +139,7 @@ public class SignatureBundle internal constructor(
signingKey: PrivateKey,
expiryDate: ZonedDateTime,
startDate: ZonedDateTime = ZonedDateTime.now(),
encapsulatePlaintext: Boolean = false,
): SignatureBundle {
val metadata = SignatureMetadata(
ASN1ObjectIdentifier(serviceOid),
Expand All @@ -147,7 +154,7 @@ public class SignatureBundle internal constructor(
signingKey,
memberIdBundle.memberCertificate,
setOf(memberIdBundle.memberCertificate, memberIdBundle.orgCertificate),
encapsulatePlaintext = false,
encapsulatePlaintext = encapsulatePlaintext,
extraSignedAttrs = listOf(metadataAttribute),
)
return SignatureBundle(memberIdBundle, signedData)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tech.relaycorp.veraid

/**
* Successful signature bundle verification.
*/
public data class SignatureBundleVerification(
/**
* The plaintext whose signature was verified.
*/
public val plaintext: ByteArray,

/**
* The member that produced the signature.
*/
public val member: Member,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as SignatureBundleVerification

if (!plaintext.contentEquals(other.plaintext)) return false
return member == other.member
}

override fun hashCode(): Int {
var result = plaintext.contentHashCode()
result = 31 * result + member.hashCode()
return result
}
}
99 changes: 96 additions & 3 deletions src/test/kotlin/tech/relaycorp/veraid/SignatureBundleTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ class SignatureBundleTest {

signatureBundle.signedData.plaintext shouldBe null
}

@Test
fun `Plaintext should be attached if requested`() {
val signatureBundle = SignatureBundle.generate(
plaintext,
SERVICE_OID.id,
memberIdBundle,
MEMBER_KEY_PAIR.private,
signatureExpiry,
encapsulatePlaintext = true,
)

signatureBundle.signedData.plaintext shouldBe plaintext
}
}

@Nested
Expand Down Expand Up @@ -683,15 +697,94 @@ class SignatureBundleTest {
}
}

@Nested
inner class Plaintext {
@Test
fun `Verification should fail if plaintext is attached and passed`() = runTest {
val bundle = SignatureBundle.generate(
plaintext,
SERVICE_OID.id,
memberIdBundle,
MEMBER_KEY_PAIR.private,
validityPeriod.endInclusive,
validityPeriod.start,
encapsulatePlaintext = true,
)

val exception = assertThrows<SignatureException> {
bundle.verify(plaintext, SERVICE_OID.id)
}

exception.cause shouldBe instanceOf<SignedDataException>()
}

@Test
fun `Verification should fail if plaintext is detached and not passed`() = runTest {
val bundle = SignatureBundle.generate(
plaintext,
SERVICE_OID.id,
memberIdBundle,
MEMBER_KEY_PAIR.private,
validityPeriod.endInclusive,
validityPeriod.start,
encapsulatePlaintext = false,
)

val exception = assertThrows<SignatureException> {
bundle.verify(null, SERVICE_OID.id)
}

exception.cause shouldBe instanceOf<SignedDataException>()
}
}

@Nested
inner class ValidResult {
@Test
fun `Plaintext should be taken from bundle if attached`() = runTest {
val attachedPlaintext = "attached".toByteArray()
val signedData = SignatureBundle.generate(
attachedPlaintext,
SERVICE_OID.id,
memberIdBundle,
MEMBER_KEY_PAIR.private,
validityPeriod.endInclusive,
validityPeriod.start,
encapsulatePlaintext = true,
).signedData
val bundle = SignatureBundle(mockMemberIdBundle(), signedData)

val result = bundle.verify(null, SERVICE_OID.id)

result.plaintext shouldBe attachedPlaintext
}

@Test
fun `Plaintext should be taken from parameter if detached`() = runTest {
val detachedPlaintext = "deattached".toByteArray()
val signedData = SignatureBundle.generate(
detachedPlaintext,
SERVICE_OID.id,
memberIdBundle,
MEMBER_KEY_PAIR.private,
validityPeriod.endInclusive,
validityPeriod.start,
encapsulatePlaintext = false,
).signedData
val bundle = SignatureBundle(mockMemberIdBundle(), signedData)

val result = bundle.verify(detachedPlaintext, SERVICE_OID.id)

result.plaintext shouldBe detachedPlaintext
}

@Test
fun `Organisation name should be output`() = runTest {
val bundle = SignatureBundle(mockMemberIdBundle(), validBundle.signedData)

val result = bundle.verify(plaintext, SERVICE_OID.id)

result.orgName shouldBe ORG_NAME
result.member.orgName shouldBe ORG_NAME
}

@Test
Expand All @@ -700,7 +793,7 @@ class SignatureBundleTest {

val result = bundle.verify(plaintext, SERVICE_OID.id)

result.userName shouldBe USER_NAME
result.member.userName shouldBe USER_NAME
}

@Test
Expand All @@ -710,7 +803,7 @@ class SignatureBundleTest {

val result = bundle.verify(plaintext, SERVICE_OID.id)

result.userName shouldBe null
result.member.userName shouldBe null
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package tech.relaycorp.veraid

import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

class SignatureBundleVerificationTest {
val verification = SignatureBundleVerification(
"plaintext".toByteArray(),
Member(ORG_NAME, USER_NAME),
)

@Nested
inner class Equals {
@Test
@Suppress("ReplaceCallWithBinaryOperator")
fun `The same object should equal`() {
verification.equals(verification) shouldBe true
}

@Test
fun `Null should not equal`() {
verification.equals(null) shouldBe false
}

@Test
fun `Different class should not equal`() {
verification.equals("foo") shouldBe false
}

@Test
fun `Different plaintext should not equal`() {
verification shouldNotBe verification.copy(plaintext = "foo".toByteArray())
}

@Test
fun `Different member should not equal`() {
val differentMember = verification.member.copy(orgName = "not-$ORG_NAME")

verification shouldNotBe verification.copy(member = differentMember)
}
}

@Test
fun `Hashcode should compine member and plaintext`() {
val constant = 31
val expectedHashCode =
verification.member.hashCode() + constant * verification.plaintext.contentHashCode()

verification.hashCode() shouldBe expectedHashCode
}
}

0 comments on commit ef9f487

Please sign in to comment.