Skip to content

Commit

Permalink
feat: Implement verification of Signature Bundles (#28)
Browse files Browse the repository at this point in the history
Fixes #11.

- [x] Implement actual verification.
  • Loading branch information
gnarea authored Mar 17, 2023
1 parent 7f8a539 commit 3f9a364
Show file tree
Hide file tree
Showing 21 changed files with 896 additions and 85 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ plugins {
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
}

apply from: 'jacoco.gradle'
apply from: 'unitTest.gradle'
apply from: 'release.gradle'

group = "tech.relaycorp"
Expand Down
17 changes: 17 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
rootProject.name = "veraid"

pluginManagement {
repositories {
gradlePluginPortal()
google()
}
}
plugins {
id("com.gradle.enterprise").version("3.12.4")
}
gradleEnterprise {
buildScan {
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"
publishOnFailureIf(!System.getenv("CI").isNullOrEmpty())
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/tech/relaycorp/veraid/Periods.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package tech.relaycorp.veraid
import org.bouncycastle.asn1.ASN1Sequence
import tech.relaycorp.veraid.utils.asn1.ASN1Utils
import tech.relaycorp.veraid.utils.asn1.toGeneralizedTime
import tech.relaycorp.veraid.utils.intersect
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime

internal typealias InstantPeriod = ClosedRange<Instant>
Expand All @@ -18,3 +20,10 @@ internal fun DatePeriod.encode(): ASN1Sequence = ASN1Utils.makeSequence(
listOf(start.toGeneralizedTime(), endInclusive.toGeneralizedTime()),
false,
)

private fun ZonedDateTime.toUtc(): ZonedDateTime = withZoneSameInstant(ZoneOffset.UTC)

private fun DatePeriod.toUtc(): DatePeriod = start.toUtc()..endInclusive.toUtc()

internal fun DatePeriod.intersect(otherDatePeriod: DatePeriod): DatePeriod? =
toUtc().intersect(otherDatePeriod.toUtc())
115 changes: 107 additions & 8 deletions src/main/kotlin/tech/relaycorp/veraid/SignatureBundle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,91 @@ import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.DERSet
import org.bouncycastle.asn1.cms.Attribute
import tech.relaycorp.veraid.dns.InvalidChainException
import tech.relaycorp.veraid.dns.VeraDnssecChain
import tech.relaycorp.veraid.pki.Member
import tech.relaycorp.veraid.pki.MemberCertificate
import tech.relaycorp.veraid.pki.MemberIdBundle
import tech.relaycorp.veraid.pki.OrgCertificate
import tech.relaycorp.veraid.pki.PkiException
import tech.relaycorp.veraid.utils.asn1.ASN1Exception
import tech.relaycorp.veraid.utils.asn1.ASN1Utils
import tech.relaycorp.veraid.utils.cms.SignedData
import tech.relaycorp.veraid.utils.cms.SignedDataException
import tech.relaycorp.veraid.utils.x509.CertificateException
import java.security.PrivateKey
import java.time.ZonedDateTime

public class SignatureBundle internal constructor(
internal val chain: VeraDnssecChain,
internal val orgCertificate: OrgCertificate,
internal val memberIdBundle: MemberIdBundle,
internal val signedData: SignedData,
) {
public fun serialise(): ByteArray = ASN1Utils.serializeSequence(
listOf(
ASN1Integer(0),
chain.encode(),
orgCertificate.encode(),
memberIdBundle.dnssecChain.encode(),
memberIdBundle.orgCertificate.encode(),
signedData.encode(),
),
false,
)

public suspend fun verify(
plaintext: ByteArray,
serviceOid: String,
date: ZonedDateTime,
): Member = verify(plaintext, serviceOid, date..date)

public suspend fun verify(
plaintext: ByteArray,
serviceOid: String,
datePeriod: DatePeriod? = null,
): Member {
val now = ZonedDateTime.now()
val verificationPeriod = datePeriod ?: now..now
if (verificationPeriod.endInclusive < verificationPeriod.start) {
throw SignatureException("Verification expiry date cannot be before start date")
}

try {
signedData.verify(plaintext)
} catch (exc: SignedDataException) {
throw SignatureException("Signature is invalid", exc)
}

val metadata = getSignatureMetadata()

val signaturePeriodIntersection = metadata.validityPeriod.intersect(verificationPeriod)
?: throw SignatureException("Signature period does not overlap with required period")

if (metadata.service.id != serviceOid) {
throw SignatureException(
"Signature is bound to a different service (${metadata.service.id})",
)
}

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

private fun getSignatureMetadata(): SignatureMetadata {
val signedAttrs = signedData.signedAttrs
val metadataAttribute = signedAttrs?.get(VeraOids.SIGNATURE_METADATA_ATTR)
?: throw SignatureException("SignedData should have VeraId metadata attribute")
if (metadataAttribute.attrValues.size() == 0) {
throw SignatureException("Metadata attribute should have at least one value")
}
val metadataAttributeValue = metadataAttribute.attrValues.getObjectAt(0)
return try {
SignatureMetadata.decode(metadataAttributeValue)
} catch (exc: SignatureException) {
throw SignatureException("Metadata attribute is malformed", exc)
}
}

public companion object {
public fun generate(
plaintext: ByteArray,
Expand All @@ -52,11 +114,48 @@ public class SignatureBundle internal constructor(
encapsulatePlaintext = false,
extraSignedAttrs = listOf(metadataAttribute),
)
return SignatureBundle(
memberIdBundle.dnssecChain,
memberIdBundle.orgCertificate,
signedData,
return SignatureBundle(memberIdBundle, signedData)
}

@Throws(SignatureException::class)
public fun deserialise(serialisation: ByteArray): SignatureBundle {
val sequence = try {
ASN1Utils.deserializeHeterogeneousSequence(serialisation)
} catch (exc: ASN1Exception) {
throw SignatureException("Signature bundle should be a SEQUENCE", exc)
}

if (sequence.size < 4) {
throw SignatureException("Signature bundle should have at least 4 items")
}

val orgCertificate = try {
OrgCertificate.decode(sequence[2])
} catch (exc: CertificateException) {
throw SignatureException("Organisation certificate is malformed", exc)
}

val veraDnssecChain = try {
VeraDnssecChain.decode(orgCertificate.commonName, sequence[1])
} catch (exc: InvalidChainException) {
throw SignatureException("VeraId DNSSEC chain is malformed", exc)
}

val signedData = try {
SignedData.decode(sequence[3])
} catch (exc: SignedDataException) {
throw SignatureException("SignedData is malformed", exc)
}

val signerCertificate = signedData.signerCertificate
?: throw SignatureException("SignedData should have signer certificate attached")

val memberIdBundle = MemberIdBundle(
veraDnssecChain,
orgCertificate,
MemberCertificate(signerCertificate.certificateHolder),
)
return SignatureBundle(memberIdBundle, signedData)
}
}
}
10 changes: 8 additions & 2 deletions src/main/kotlin/tech/relaycorp/veraid/SignatureMetadata.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package tech.relaycorp.veraid

import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DERSequence
import tech.relaycorp.veraid.utils.asn1.ASN1Exception
Expand All @@ -25,7 +25,13 @@ internal class SignatureMetadata(

companion object {
@Throws(SignatureException::class)
fun decode(attributeValue: ASN1Sequence): SignatureMetadata {
fun decode(attributeValueTagged: ASN1Encodable): SignatureMetadata {
val attributeValue = try {
DERSequence.getInstance(attributeValueTagged)
} catch (exc: IllegalArgumentException) {
throw SignatureException("Encoding isn't a SEQUENCE", exc)
}

if (attributeValue.size() < 2) {
throw SignatureException(
"Metadata SEQUENCE should have at least 2 items " +
Expand Down
23 changes: 19 additions & 4 deletions src/main/kotlin/tech/relaycorp/veraid/dns/VeraDnssecChain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tech.relaycorp.veraid.dns
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1Set
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSet
import org.xbill.DNS.DClass
Expand All @@ -18,10 +19,11 @@ import tech.relaycorp.veraid.InstantPeriod
import tech.relaycorp.veraid.OrganisationKeySpec
import tech.relaycorp.veraid.toInstantPeriod
import tech.relaycorp.veraid.utils.intersect
import java.lang.IllegalStateException
import kotlin.time.toJavaDuration

/**
* Vera DNSSEC chain.
* VeraId DNSSEC chain.
*
* It contains the DNSSEC chain for the Vera TXT RRSet (e.g., `_veraid.example.com./TXT`).
*/
Expand Down Expand Up @@ -146,7 +148,7 @@ public class VeraDnssecChain internal constructor(
internal var dnssecChainRetriever: ChainRetriever = DnssecChain.Companion::retrieve

/**
* Retrieve Vera DNSSEC chain for [organisationName].
* Retrieve VeraId DNSSEC chain for [organisationName].
*
* @param organisationName The domain name of the organisation
* @param resolverHost The IPv4 address for the DNSSEC-aware, recursive resolver
Expand All @@ -164,12 +166,25 @@ public class VeraDnssecChain internal constructor(
return VeraDnssecChain(organisationName, dnssecChain.responses)
}

@Throws(DnsException::class)
@Throws(InvalidChainException::class)
internal fun decode(
organisationName: String,
setTagged: ASN1TaggedObject,
): VeraDnssecChain {
val set = try {
ASN1Set.getInstance(setTagged, false)
} catch (exc: IllegalStateException) {
throw InvalidChainException("Chain is not an implicitly-tagged SET", exc)
}
return decode(organisationName, set)
}

@Throws(InvalidChainException::class)
internal fun decode(organisationName: String, set: ASN1Set): VeraDnssecChain {
val responses = set.map {
if (it !is DEROctetString) {
throw InvalidChainException(
"Chain SET contains non-OCTET STRING item ($it)",
"Chain SET contains non-OCTET STRING item (${it::class.simpleName})",
)
}
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public class MemberIdBundle(
} catch (exc: DnsException) {
throw PkiException("DNS/DNSSEC resolution failed", exc)
} catch (exc: InvalidChainException) {
throw PkiException("Vera DNSSEC chain verification failed", exc)
throw PkiException("VeraId DNSSEC chain verification failed", exc)
}

return Member(orgCertificate.commonName, userName)
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/tech/relaycorp/veraid/pki/OrgCertificate.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package tech.relaycorp.veraid.pki

import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.cert.X509CertificateHolder
import tech.relaycorp.veraid.utils.x509.Certificate
import tech.relaycorp.veraid.utils.x509.CertificateException
import java.security.KeyPair
import java.time.ZonedDateTime

Expand All @@ -24,5 +26,9 @@ public class OrgCertificate internal constructor(certificateHolder: X509Certific
validityStartDate = startDate,
).certificateHolder,
)

@Throws(CertificateException::class)
internal fun decode(encoding: ASN1TaggedObject): OrgCertificate =
OrgCertificate(Certificate.decode(encoding).certificateHolder)
}
}
12 changes: 9 additions & 3 deletions src/main/kotlin/tech/relaycorp/veraid/utils/cms/SignedData.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tech.relaycorp.veraid.utils.cms

import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.cms.Attribute
import org.bouncycastle.asn1.cms.AttributeTable
import org.bouncycastle.asn1.cms.ContentInfo
Expand Down Expand Up @@ -126,7 +127,7 @@ internal class SignedData(val bcSignedData: CMSSignedData) {
encapsulatedCertificates: Set<Certificate> = setOf(),
hashingAlgorithm: Hash? = null,
encapsulatePlaintext: Boolean = true,
extraSignedAttrs: List<Attribute> = emptyList(),
extraSignedAttrs: Collection<Attribute> = emptyList(),
): SignedData {
val signerInfoGenerator = makeSignerInfoGenerator(
signerPrivateKey,
Expand Down Expand Up @@ -157,7 +158,7 @@ internal class SignedData(val bcSignedData: CMSSignedData) {
private fun makeSignerInfoGenerator(
signerPrivateKey: PrivateKey,
hashingAlgorithm: Hash?,
extraSignedAttrs: List<Attribute>,
extraSignedAttrs: Collection<Attribute>,
signerCertificate: Certificate,
): SignerInfoGenerator? {
val contentSigner = makeContentSigner(signerPrivateKey, hashingAlgorithm)
Expand Down Expand Up @@ -190,7 +191,12 @@ internal class SignedData(val bcSignedData: CMSSignedData) {
}

@JvmStatic
fun decode(contentInfo: ContentInfo): SignedData {
fun decode(contentInfoTagged: ASN1TaggedObject): SignedData {
val contentInfo = try {
ContentInfo.getInstance(contentInfoTagged, false)
} catch (exc: IllegalStateException) {
throw SignedDataException("Encoding is not an implicitly-tagged ContentInfo", exc)
}
val bcSignedData = try {
CMSSignedData(contentInfo)
} catch (_: CMSException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import tech.relaycorp.veraid.DatePeriod
import tech.relaycorp.veraid.utils.BC_PROVIDER
import tech.relaycorp.veraid.utils.Hash
import tech.relaycorp.veraid.utils.generateRandomBigInteger
Expand Down Expand Up @@ -183,7 +184,7 @@ public open class Certificate internal constructor(
public val issuerCommonName: String
get() = getCommonName(certificateHolder.issuer)

public val validityPeriod: ClosedRange<ZonedDateTime> by lazy {
public val validityPeriod: DatePeriod by lazy {
val start = dateToZonedDateTime(certificateHolder.notBefore)
val end = dateToZonedDateTime(certificateHolder.notAfter)
start..end
Expand Down
Loading

0 comments on commit 3f9a364

Please sign in to comment.