Skip to content

Commit

Permalink
Update Certificate#verify to allow nullable params
Browse files Browse the repository at this point in the history
  • Loading branch information
alvinsee committed Aug 24, 2023
1 parent ecb65ee commit 03a3fc5
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 25 deletions.
39 changes: 25 additions & 14 deletions jvm/src/main/kotlin/app/cash/trifle/Certificate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.cash.trifle
import app.cash.trifle.CertificateRequest.PKCS10Request
import app.cash.trifle.TrifleErrors.CSRMismatch
import app.cash.trifle.internal.validators.CertChainValidatorFactory
import app.cash.trifle.internal.validators.CertificateValidatorFactory
import okio.ByteString.Companion.toByteString
import org.bouncycastle.cert.X509CertificateHolder
import java.util.Date
Expand Down Expand Up @@ -47,25 +48,35 @@ data class Certificate internal constructor(
* - failure value is expressed as a [TrifleErrors]
*/
fun verify(
certificateRequest: CertificateRequest,
ancestorCertificateChain: List<Certificate>,
anchorCertificate: Certificate,
certificateRequest: CertificateRequest? = null,
ancestorCertificateChain: List<Certificate> = emptyList(),
anchorCertificate: Certificate? = null,
date: Date? = null
): Result<Unit> {
val rootCertificate = anchorCertificate ?:
if (ancestorCertificateChain.isNotEmpty()) {
ancestorCertificateChain.last()
} else {
return CertificateValidatorFactory.get(this).validate(this, date)
}
// First check to see if the certificate chain validates
val certChainResult = CertChainValidatorFactory.get(anchorCertificate, date)
val certChainResult = CertChainValidatorFactory.get(rootCertificate, date)
.validate(listOf(this) + ancestorCertificateChain)

return certChainResult.mapCatching {
val x509Certificate = X509CertificateHolder(certificate)
when (certificateRequest) {
is PKCS10Request -> {
// Certificate chain matches, check with certificate request.
// TODO(dcashman): Check other attributes as well.
if (certificateRequest.pkcs10Req.subject != x509Certificate.subject ||
certificateRequest.pkcs10Req.subjectPublicKeyInfo != x509Certificate.subjectPublicKeyInfo
) {
throw CSRMismatch
return if (certificateRequest == null) {
certChainResult
} else {
certChainResult.mapCatching {
val x509Certificate = X509CertificateHolder(certificate)
when (certificateRequest) {
is PKCS10Request -> {
// Certificate chain matches, check with certificate request.
// TODO(dcashman): Check other attributes as well.
if (certificateRequest.pkcs10Req.subject != x509Certificate.subject ||
certificateRequest.pkcs10Req.subjectPublicKeyInfo != x509Certificate.subjectPublicKeyInfo
) {
throw CSRMismatch
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions jvm/src/main/kotlin/app/cash/trifle/TrifleErrors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ sealed class TrifleErrors(message: String, cause: Throwable? = null) : Exception
object NoTrustAnchor : TrifleErrors("No acceptable Trifle trust anchor found")
object InvalidCertPath : TrifleErrors("Invalid Trifle certificate path found")
object ExpiredCertificate : TrifleErrors("Expired Trifle certificate")
object NotValidYetCertificate : TrifleErrors("Trifle certificate is not valid yet")
object InvalidSignature : TrifleErrors("Invalid Trifle signature")
object CSRMismatch : TrifleErrors("Trifle certificate does not match CSR")
class UnspecifiedFailure(message: String, cause: Throwable) : TrifleErrors(message, cause)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.cash.trifle.internal.validators

import app.cash.trifle.Certificate
import java.util.Date

sealed interface CertificateValidator {
/**
* Validates if a specific Trifle Certificate is within the validity window of the date.
*
* @param certificate - The certificate to validate.
* @param date - The date to use for verification against certificates' validity windows. If null,
* the current time is used.
*/
fun validate(certificate: Certificate, date: Date?): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package app.cash.trifle.internal.validators

import app.cash.trifle.Certificate

object CertificateValidatorFactory {
/**
* Return a certificate validator matching the provided certificate.
* @param certificate - The certificate to validate. Its format will determine how
* verification should be performed.
*/
fun get(certificate: Certificate): CertificateValidator {
return when (certificate.version) {
Certificate.CERTIFICATE_VERSION -> X509CertificateValidator
else -> throw UnsupportedOperationException("Unsupported version of Trifle Certificate")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import app.cash.trifle.TrifleErrors.InvalidCertPath
import app.cash.trifle.TrifleErrors.InvalidSignature
import app.cash.trifle.TrifleErrors.NoTrustAnchor
import app.cash.trifle.TrifleErrors.UnspecifiedFailure
import app.cash.trifle.internal.validators.X509CertificateUtil.generateCertPath
import app.cash.trifle.internal.validators.X509CertificateUtil.toX509Certificate
import java.security.cert.CertPathValidatorException
import java.security.cert.CertPathValidatorException.BasicReason
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import java.security.cert.PKIXReason
import java.security.cert.TrustAnchor
Expand All @@ -27,9 +28,7 @@ internal class X509CertChainValidator(
private val pkixParams: PKIXParameters = PKIXParameters(
setOf(
TrustAnchor(
certAnchor.certificate.inputStream().use {
X509FACTORY.generateCertificate(it)
} as X509Certificate,
certAnchor.toX509Certificate(),
null
)
)
Expand All @@ -42,16 +41,12 @@ internal class X509CertChainValidator(

override fun validate(certChain: List<Certificate>): Result<Unit> =
try {
val x509Certs = certChain.map { trifleCert ->
trifleCert.certificate.inputStream().use {
X509FACTORY.generateCertificate(it) as X509Certificate
}
}.maybeDropRoot()
val x509Certs = certChain.map { it.toX509Certificate() }.maybeDropRoot()

if (x509Certs.isEmpty()) {
Result.failure(InvalidCertPath)
} else {
PATH_VALIDATOR.validate(X509FACTORY.generateCertPath(x509Certs), pkixParams)
PATH_VALIDATOR.validate(x509Certs.generateCertPath(), pkixParams)
Result.success(Unit)
}
} catch (e: CertPathValidatorException) {
Expand Down Expand Up @@ -86,6 +81,5 @@ internal class X509CertChainValidator(
private val PATH_VALIDATOR = JCACertPathValidator.getInstance(
JCACertPathValidator.getDefaultType()
)
private val X509FACTORY = CertificateFactory.getInstance("X509")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package app.cash.trifle.internal.validators

import app.cash.trifle.Certificate
import java.security.cert.CertPath
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate

internal object X509CertificateUtil {
internal fun Certificate.toX509Certificate() = certificate.inputStream().use {
X509FACTORY.generateCertificate(it) as X509Certificate
}

internal fun List<X509Certificate>.generateCertPath(): CertPath =
X509FACTORY.generateCertPath(this)

private val X509FACTORY = CertificateFactory.getInstance("X509")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package app.cash.trifle.internal.validators

import app.cash.trifle.Certificate
import app.cash.trifle.TrifleErrors
import app.cash.trifle.internal.validators.X509CertificateUtil.toX509Certificate
import java.security.cert.CertificateExpiredException
import java.security.cert.CertificateNotYetValidException
import java.util.Date

/**
* X.509 specific implementation for validating a certificate.
*/
internal object X509CertificateValidator : CertificateValidator {
override fun validate(certificate: Certificate, date: Date?): Result<Unit> =
try {
certificate.toX509Certificate().checkValidity(date ?: Date())
Result.success(Unit)
} catch (e: CertificateExpiredException) {
Result.failure(TrifleErrors.ExpiredCertificate)
} catch (e: CertificateNotYetValidException) {
Result.failure(TrifleErrors.NotValidYetCertificate)
}
}
32 changes: 32 additions & 0 deletions jvm/src/test/kotlin/app/cash/trifle/CertificateTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ internal class CertificateTests {
assertTrue(result.isSuccess)
}

@Test
fun `test verify() succeeds for a stored issued certificate with intermediate chain`() {
val result = endEntity.certificate.verify(
ancestorCertificateChain = endEntity.certChain.drop(1)
)
assertTrue(result.isSuccess)
}

@Test
fun `test verify() succeeds for a stored issued certificate`() {
val result = endEntity.certificate.verify()
assertTrue(result.isSuccess)
}

@Test
fun `test verify() fails with CSRMismatch for a legitimate certificate from the same CA, but wrong entity`() {
val result = endEntity.certificate.verify(
Expand Down Expand Up @@ -56,6 +70,24 @@ internal class CertificateTests {
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is ExpiredCertificate)
}

@Test
fun `test verify() fails with ExpiredCertificate for an expired stored certificate`() {
val result = endEntity.certificate.verify(
date = Date.from(Instant.now().plus(Duration.ofDays(365)))
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is ExpiredCertificate)
}

@Test
fun `test verify() fails with NotYetValidCertificate for a stored certificate yet to be valid`() {
val result = endEntity.certificate.verify(
date = Date.from(Instant.now().minus(Duration.ofDays(1)))
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is NotValidYetCertificate)
}
}

companion object {
Expand Down

0 comments on commit 03a3fc5

Please sign in to comment.