Skip to content

Commit

Permalink
Break up Certificate#verify to top level Trifle APIs in android sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
alvinsee committed Aug 25, 2023
1 parent 3f43586 commit dcc1d6e
Show file tree
Hide file tree
Showing 15 changed files with 352 additions and 225 deletions.
1 change: 1 addition & 0 deletions android/trifle/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("junit:junit:4.13.2")
androidTestImplementation(project(":jvm-testing"))

api(project(":jvm"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package app.cash.trifle

import app.cash.trifle.TrifleErrors.CSRMismatch
import app.cash.trifle.TrifleErrors.ExpiredCertificate
import app.cash.trifle.TrifleErrors.NoTrustAnchor
import app.cash.trifle.TrifleErrors.NotValidYetCertificate
import app.cash.trifle.testing.TestCertificateAuthority
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException

import java.time.Duration
import java.time.Instant
import java.util.Date

class TrifleApiTest {
@JvmField
Expand Down Expand Up @@ -35,4 +42,85 @@ class TrifleApiTest {
@Test fun testDeleteKeyHandle() {
TrifleApi.delete(keyHandle)
}

@Test
fun testVerify_succeeds() {
val result = TrifleApi.verifyChain(
certificateChain = endEntity.certChain
)
assertTrue(result.isSuccess)
}

@Test
fun testVerifyCertificateValidity_succeeds() {
val result = TrifleApi.verifyValidity(
endEntity.certificate
)
assertTrue(result.isSuccess)
}

@Test
fun testVerifyAttributes_succeeds() {
val result = TrifleApi.verifyAttributes(
endEntity.certificate,
endEntity.certRequest
)
assertTrue(result.isSuccess)
}

@Test
fun testVerify_failsWithNoTrustAnchorForADifferentRootCertificate() {
val result = TrifleApi.verifyChain(
certificateChain = listOf(endEntity.certificate) + otherEndEntity.certChain.drop(1)
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is NoTrustAnchor)
}

@Test
fun testVerify_failsWithExpiredCertificateForAnExpiredCertificate() {
val result = TrifleApi.verifyChain(
certificateChain = endEntity.certChain,
date = Date.from(Instant.now().plus(Duration.ofDays(365)))
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is ExpiredCertificate)
}

@Test
fun testVerify_failsWithExpiredCertificateForAnExpiredStoredCertificate() {
val result = TrifleApi.verifyValidity(
endEntity.certificate,
Date.from(Instant.now().plus(Duration.ofDays(365)))
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is ExpiredCertificate)
}

@Test
fun testVerify_failsWithNotYetValidCertificateForAStoredCertificateYetToBeValid() {
val result = TrifleApi.verifyValidity(
endEntity.certificate,
date = Date.from(Instant.now().minus(Duration.ofDays(1)))
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is NotValidYetCertificate)
}

@Test
fun testVerifyAttributes_failsWithCSRMismatch() {
val result = TrifleApi.verifyAttributes(
endEntity.certificate,
otherEndEntity.certRequest
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is CSRMismatch)
}

companion object {
private val certificateAuthority = TestCertificateAuthority("issuingEntity")
private val otherCertificateAuthority = TestCertificateAuthority("otherIssuingEntity")
private val endEntity = certificateAuthority.createTestEndEntity("entity")
private val otherEndEntity = otherCertificateAuthority.createTestEndEntity("otherEntity")
}
}
64 changes: 63 additions & 1 deletion android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package app.cash.trifle

import app.cash.trifle.validators.CertChainValidatorFactory
import app.cash.trifle.validators.CertificateValidatorFactory
import java.util.Date

object TrifleApi {
/**
* Create a new mobile Trifle keypair for which can be used to create a
Expand Down Expand Up @@ -50,7 +54,8 @@ object TrifleApi {
*
* @param data - raw data to be signed.
* @param keyHandle - key handle used for the signing.
* @param certificates - certificate chain to be included in the SignedData message. Must match the key in keyHandle.
* @param certificates - certificate chain to be included in the SignedData message.
* Must match the key in keyHandle.
*
* @return A signed data message in the Trifle format [SignedData].
*/
Expand All @@ -59,4 +64,61 @@ object TrifleApi {
keyHandle: KeyHandle,
certificates: List<Certificate>
): SignedData = Trifle.EndEntity(keyHandle.keyPair).createSignedData(data, certificates)

/**
* Verify that the provided Trifle Certificate Chain is valid.
*
* @param certificateChain - list of certificates. Namely, the first
* entry should be the certificate corresponding to the subject, and the subsequent being
* the issuer of the former certificate, and each thereafter should be the issuer of the
* one before it.
* @param anchorCertificate - the trust anchor against which we would like to verify the
* certificateChain instead. This may be the terminal (root) certificate of the chain or may be
* an intermediate certificate in the chain which is already trusted.
* @param date - The date to use for verification against certificates' validity windows. If null,
* the current time is used.
*
* @return - [Result] indicating [Result.isSuccess] or [Result.isFailure]:
* - success value is expressed as a [Unit] (Nothing)
* - failure value is expressed as a [TrifleErrors]
*/
fun verifyChain(
certificateChain: List<Certificate>,
anchorCertificate: Certificate? = null,
date: Date? = null
): Result<Unit> = CertChainValidatorFactory.get(
certAnchor = anchorCertificate ?: certificateChain.last(),
date = date
).validate(certificateChain)

/**
* Verify that the provided Trifle Certificate is valid.
*
* @param certificate - the certificate to verify
* @param date - The date to use for verification against certificate' validity windows. If null,
* the current time is used.
*
* @return - [Result] indicating [Result.isSuccess] or [Result.isFailure]:
* - success value is expressed as a [Unit] (Nothing)
* - failure value is expressed as a [TrifleErrors]
*/
fun verifyValidity(
certificate: Certificate,
date: Date? = null
): Result<Unit> = CertificateValidatorFactory.get(certificate).validate(date)

/**
* Verify that the provided Trifle Certificate matches the Certificate Requests' attributes.
*
* @param certificate - the certificate to verify
* @param certificateRequest - request used to generate this certificate
*
* @return - [Result] indicating [Result.isSuccess] or [Result.isFailure]:
* - success value is expressed as a [Unit] (Nothing)
* - failure value is expressed as a [TrifleErrors]
*/
fun verifyAttributes(
certificate: Certificate,
certificateRequest: CertificateRequest
): Result<Unit> = CertificateValidatorFactory.get(certificate).validate(certificateRequest)
}
50 changes: 0 additions & 50 deletions jvm/src/main/kotlin/app/cash/trifle/Certificate.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package app.cash.trifle

import app.cash.trifle.CertificateRequest.PKCS10Request
import app.cash.trifle.TrifleErrors.CSRMismatch
import app.cash.trifle.internal.validators.CertChainValidatorFactory
import okio.ByteString.Companion.toByteString
import org.bouncycastle.cert.X509CertificateHolder
import java.util.Date
import app.cash.trifle.protos.api.alpha.Certificate as CertificateProto

/**
Expand All @@ -27,51 +22,6 @@ data class Certificate internal constructor(
version = CERTIFICATE_VERSION
).encode()

/**
* Verify that the provided certificate matches what we expected.
* It matches the CSR that we have and the root cert is what
* we expect.
*
* @param certificateRequest - request used to generate this certificate
* @param ancestorCertificateChain - list of certificates preceding *this* one. Namely, the first
* entry should be the certificate corresponding to the issuer of this certificate, and each
* thereafter should be the issuer of the one before it.
* @param anchorCertificate - the trust anchor against which we would like to verify the
* ancestorCertificateChain. This may be the terminal (root) certificate of the chain or may be
* an intermediate certificate in the chain which is already trusted.
* @param date - The date to use for verification against certificates' validity windows. If null,
* the current time is used.
*
* @return - [Result] indicating [Result.isSuccess] or [Result.isFailure]:
* - success value is expressed as a [Unit] (Nothing)
* - failure value is expressed as a [TrifleErrors]
*/
fun verify(
certificateRequest: CertificateRequest,
ancestorCertificateChain: List<Certificate>,
anchorCertificate: Certificate,
date: Date? = null
): Result<Unit> {
// First check to see if the certificate chain validates
val certChainResult = CertChainValidatorFactory.get(anchorCertificate, 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
}
}
}
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
Expand Down
2 changes: 1 addition & 1 deletion jvm/src/main/kotlin/app/cash/trifle/SignedData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package app.cash.trifle
import app.cash.trifle.TrifleErrors.InvalidSignature
import app.cash.trifle.internal.TrifleAlgorithmIdentifier
import app.cash.trifle.internal.providers.JCAContentVerifierProvider
import app.cash.trifle.internal.validators.CertChainValidatorFactory
import app.cash.trifle.validators.CertChainValidatorFactory
import okio.ByteString.Companion.toByteString
import app.cash.trifle.protos.api.alpha.Certificate as CertificateProto
import app.cash.trifle.protos.api.alpha.SignedData as SignedDataProto
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

This file was deleted.

This file was deleted.

Loading

0 comments on commit dcc1d6e

Please sign in to comment.