diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt
index d92d8f117..a2ffbe0e1 100644
--- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt
+++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt
@@ -245,36 +245,6 @@ class SigningViewModelTest {
assertFalse(isEncryptButtonShown)
}
- @Test
- fun signingViewModel_isShareButtonShown_returnTrue() =
- runTest {
- val file =
- getResourceFileAsFile(
- context,
- "example_no_signatures.asice",
- R.raw.example_no_signatures,
- )
-
- val container = SignedContainer.openOrCreate(context, file, listOf(file), true)
-
- val isShareButtonShown = viewModel.isShareButtonShown(container, false)
-
- assertTrue(isShareButtonShown)
- }
-
- @Test
- fun signingViewModel_isShareButtonShown_returnFalse() =
- runTest {
- val file = File.createTempFile("temp", ".txt")
- file.writeText("test content")
-
- val container = SignedContainer.openOrCreate(context, file, listOf(file), true)
-
- val isShareButtonShown = viewModel.isShareButtonShown(container, false)
-
- assertFalse(isShareButtonShown)
- }
-
@Test
fun signingViewModel_isExistingContainer_returnTrue() =
runTest {
diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedContainerViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedContainerViewModelTest.kt
index 16b689ebd..82d81a0b6 100644
--- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedContainerViewModelTest.kt
+++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedContainerViewModelTest.kt
@@ -41,6 +41,7 @@ import ee.ria.DigiDoc.configuration.repository.CentralConfigurationRepositoryImp
import ee.ria.DigiDoc.configuration.repository.ConfigurationRepository
import ee.ria.DigiDoc.configuration.repository.ConfigurationRepositoryImpl
import ee.ria.DigiDoc.configuration.service.CentralConfigurationServiceImpl
+import ee.ria.DigiDoc.cryptolib.CryptoContainer
import ee.ria.DigiDoc.domain.model.ContainerFileOpeningResult
import ee.ria.DigiDoc.domain.repository.fileopening.FileOpeningRepository
import ee.ria.DigiDoc.libdigidoclib.SignedContainer
@@ -585,6 +586,97 @@ class SharedContainerViewModelTest {
assertFalse(viewModel.isNestedContainer(signedContainer))
}
+ @Test
+ fun sharedContainerViewModel_isNestedContainer_returnFalseForFreshContainerWhenStackClearedFirst() {
+ val staleContainer = SignedContainer(context)
+ val freshContainer = SignedContainer(context)
+
+ viewModel.setSignedContainer(staleContainer)
+
+ viewModel.clearContainers()
+ viewModel.setSignedContainer(freshContainer)
+
+ assertEquals(1, viewModel.nestedContainers.size)
+ assertFalse(viewModel.isNestedContainer(freshContainer))
+ }
+
+ @Test
+ fun sharedContainerViewModel_isNestedContainer_returnTrueForFreshContainerWhenStaleContainerLeftInStack() {
+ val staleContainer = SignedContainer(context)
+ val freshContainer = SignedContainer(context)
+
+ viewModel.setSignedContainer(staleContainer)
+ viewModel.setSignedContainer(freshContainer)
+
+ assertEquals(2, viewModel.nestedContainers.size)
+ assertTrue(viewModel.isNestedContainer(freshContainer))
+ }
+
+ @Test
+ fun sharedContainerViewModel_setSignedContainer_doesNotGrowStackWhenSameContainerIsSetAgain() {
+ val signedContainer = SignedContainer(context)
+
+ viewModel.setSignedContainer(signedContainer)
+ viewModel.setSignedContainer(signedContainer)
+
+ assertEquals(1, viewModel.nestedContainers.size)
+ assertTrue(viewModel.nestedContainers.contains(signedContainer))
+ assertFalse(viewModel.isNestedContainer(signedContainer))
+ }
+
+ @Test
+ fun sharedContainerViewModel_resetSignedContainer_leavesNestedContainerStackPopulated() {
+ val signedContainer = SignedContainer(context)
+
+ viewModel.setSignedContainer(signedContainer)
+ viewModel.resetSignedContainer()
+
+ assertNull(viewModel.signedContainer.value)
+ assertEquals(1, viewModel.nestedContainers.size)
+ }
+
+ @Test
+ fun sharedContainerViewModel_clearContainers_makesFreshCryptoContainerNonNested() {
+ val staleContainer = SignedContainer(context)
+ val freshCryptoContainer =
+ CryptoContainer(
+ context = context,
+ file = File.createTempFile("crypto", ".cdoc"),
+ dataFiles = arrayListOf(),
+ recipients = arrayListOf(),
+ decrypted = false,
+ encrypted = false,
+ )
+
+ viewModel.setSignedContainer(staleContainer)
+
+ viewModel.clearContainers()
+ viewModel.setCryptoContainer(freshCryptoContainer)
+
+ assertEquals(1, viewModel.nestedContainers.size)
+ assertFalse(viewModel.isNestedContainer(freshCryptoContainer))
+ }
+
+ @Test
+ fun sharedContainerViewModel_isNestedContainer_returnTrueForFreshCryptoContainerWhenStaleContainerLeftInStack() {
+ val staleContainer = SignedContainer(context)
+ val freshCryptoContainer =
+ CryptoContainer(
+ context = context,
+ file = File.createTempFile("crypto", ".cdoc"),
+ dataFiles = arrayListOf(),
+ recipients = arrayListOf(),
+ decrypted = false,
+ encrypted = false,
+ )
+
+ viewModel.setSignedContainer(staleContainer)
+ viewModel.setCryptoContainer(freshCryptoContainer)
+
+ assertEquals(2, viewModel.nestedContainers.size)
+ assertTrue(viewModel.isNestedContainer(freshCryptoContainer))
+ }
+
@Test
fun sharedContainerViewModel_setExternalFileUri_success() =
runTest {
diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt b/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt
index 25880619f..8646c8f52 100644
--- a/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt
+++ b/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt
@@ -67,6 +67,14 @@ sealed class NFCError {
override val message: Int,
) : NFCError()
+ data class CertificateExpired(
+ override val message: Int,
+ ) : NFCError()
+
+ data class CertificateNotYetValid(
+ override val message: Int,
+ ) : NFCError()
+
data class CertificateUnknown(
override val message: Int,
) : NFCError()
diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/CryptoFileOpeningNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/CryptoFileOpeningNavigation.kt
index ff2b5c8b3..1468360c6 100644
--- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/CryptoFileOpeningNavigation.kt
+++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/CryptoFileOpeningNavigation.kt
@@ -145,6 +145,9 @@ fun CryptoFileOpeningNavigation(
LaunchedEffect(cryptoFileOpeningViewModel.cryptoContainer) {
cryptoFileOpeningViewModel.cryptoContainer.asFlow().collect { cryptoContainer ->
cryptoContainer?.let {
+ if (!sharedContainerViewModel.nestedContainers.contains(it)) {
+ sharedContainerViewModel.clearContainers()
+ }
sharedContainerViewModel.setCryptoContainer(it)
delay(1000)
diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/FileOpeningNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/FileOpeningNavigation.kt
index 009647035..0facd67c4 100644
--- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/FileOpeningNavigation.kt
+++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/FileOpeningNavigation.kt
@@ -219,6 +219,9 @@ fun FileOpeningNavigation(
LaunchedEffect(fileOpeningViewModel.signedContainer) {
fileOpeningViewModel.signedContainer.asFlow().collect { signedContainer ->
signedContainer?.let {
+ if (!sharedContainerViewModel.nestedContainers.contains(it)) {
+ sharedContainerViewModel.clearContainers()
+ }
sharedContainerViewModel.setSignedContainer(it)
delay(1000)
@@ -235,6 +238,9 @@ fun FileOpeningNavigation(
LaunchedEffect(fileOpeningViewModel.cryptoContainer) {
fileOpeningViewModel.cryptoContainer.asFlow().collect { cryptoContainer ->
cryptoContainer?.let {
+ if (!sharedContainerViewModel.nestedContainers.contains(it)) {
+ sharedContainerViewModel.clearContainers()
+ }
sharedContainerViewModel.setCryptoContainer(it)
navController.navigate(Route.Encrypt.route) {
diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt
index 15579dfbc..bc4676290 100644
--- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt
+++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt
@@ -131,6 +131,12 @@ class MobileIdViewModel
).put(
MobileCreateSignatureProcessStatus.CERTIFICATE_REVOKED,
R.string.signature_update_signature_error_message_certificate_revoked,
+ ).put(
+ MobileCreateSignatureProcessStatus.CERTIFICATE_EXPIRED,
+ R.string.signature_update_signature_error_message_certificate_expired,
+ ).put(
+ MobileCreateSignatureProcessStatus.CERTIFICATE_NOT_YET_VALID,
+ R.string.signature_update_signature_error_message_certificate_not_yet_valid,
).put(
MobileCreateSignatureProcessStatus.GENERAL_ERROR,
R.string.error_general_client,
diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt
index d867c92c8..12eabc1d3 100644
--- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt
+++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt
@@ -62,6 +62,7 @@ import ee.ria.DigiDoc.utils.pin.PinCodeUtil.isPINLengthValid
import ee.ria.DigiDoc.utilsLib.extensions.clearSensitive
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog
+import ee.ria.DigiDoc.utilsLib.signing.CertificateUtil
import ee.ria.libdigidocpp.ExternalSigner
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.MutableStateFlow
@@ -70,6 +71,8 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.bouncycastle.util.encoders.Hex
+import java.security.cert.CertificateExpiredException
+import java.security.cert.CertificateNotYetValidException
import java.util.Base64
import javax.inject.Inject
@@ -329,6 +332,12 @@ class NFCViewModel
val message = ex.message.orEmpty()
when {
+ ex is CertificateExpiredException ->
+ showExpiredCertificateError(ex)
+
+ ex is CertificateNotYetValidException ->
+ showNotYetValidCertificateError(ex)
+
message.contains("Certificate status: revoked") ->
showRevokedCertificateError(ex)
@@ -695,6 +704,8 @@ class NFCViewModel
}
}
+ CertificateUtil.x509Certificate(signerCert).checkValidity()
+
val hashBytes = Base64.getDecoder().decode(hash)
val (_, signatureArray) = idCardService.sign(card, pin2Code, hashBytes)
@@ -712,6 +723,12 @@ class NFCViewModel
val message = ex.message.orEmpty()
when {
+ ex is CertificateExpiredException ->
+ showExpiredCertificateError(ex)
+
+ ex is CertificateNotYetValidException ->
+ showNotYetValidCertificateError(ex)
+
message.contains("Certificate status: revoked") ->
showRevokedCertificateError(ex)
@@ -791,6 +808,22 @@ class NFCViewModel
errorLog(logTag, "Unable to sign with NFC - Certificate status: revoked", e)
}
+ private fun showExpiredCertificateError(e: Exception) {
+ _errorState.update {
+ NFCError.CertificateExpired(R.string.signature_update_signature_error_message_certificate_expired)
+ }
+ errorLog(logTag, "Unable to sign with NFC - Signing certificate has expired", e)
+ }
+
+ private fun showNotYetValidCertificateError(e: Exception) {
+ _errorState.update {
+ NFCError.CertificateNotYetValid(
+ R.string.signature_update_signature_error_message_certificate_not_yet_valid,
+ )
+ }
+ errorLog(logTag, "Unable to sign with NFC - Signing certificate is not yet valid", e)
+ }
+
private fun showUnknownCertificateError(e: Exception) {
_errorState.update {
NFCError.CertificateUnknown(R.string.signature_update_signature_error_message_certificate_unknown)
diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt
index c13902378..819efd9aa 100644
--- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt
+++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt
@@ -142,16 +142,6 @@ class SigningViewModel
) &&
!isNestedContainer
- fun isShareButtonShown(
- signedContainer: SignedContainer?,
- isNestedContainer: Boolean,
- ): Boolean =
- (
- isExistingContainer(signedContainer) ||
- !isContainerWithoutSignatures(signedContainer)
- ) &&
- !isNestedContainer
-
fun isBottomContainerButtonShown(
signedContainer: SignedContainer?,
isNestedContainer: Boolean,
diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt
index 65ced52f5..23c86b62b 100644
--- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt
+++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt
@@ -151,6 +151,12 @@ class SmartIdViewModel
).put(
SessionStatusResponseProcessStatus.CERTIFICATE_REVOKED,
R.string.signature_update_signature_error_message_certificate_revoked,
+ ).put(
+ SessionStatusResponseProcessStatus.CERTIFICATE_EXPIRED,
+ R.string.signature_update_signature_error_message_certificate_expired,
+ ).put(
+ SessionStatusResponseProcessStatus.CERTIFICATE_NOT_YET_VALID,
+ R.string.signature_update_signature_error_message_certificate_not_yet_valid,
).put(
SessionStatusResponseProcessStatus.NOT_QUALIFIED,
R.string.signature_update_smart_id_error_message_not_qualified,
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 35eff8865..e7b6e0d8a 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -320,6 +320,8 @@
https://www.id.ee/artikkel/id-kaardi-pin-ja-puk-koodide-muutmine/
Sertifikaat on kehtetu
Sertifikaadi staatus on teadmata
+ Sertifikaat on aegunud
+ Sertifikaat pole veel kehtiv
ID-kaardi
Mobiil-ID
Smart-ID
@@ -344,10 +346,8 @@
Mobiilse allkirjastamise sessioon on lukustunud, proovi hiljem uuesti
Viga mobiil-ID kasutamisel kasutaja telefonis
Kasutaja ei ole mobiil-ID klient
- Sinu mobiil-ID allkirjastamise sertifikaat on tühistatud
Sinu mobiil-ID allkirjastamise sertifikaadi olek on teadmata
Sinu mobiil-ID allkirjastamise sertifikaadi kehtivus on peatatud
- Sinu mobiil-ID allkirjastamise sertifikaat on aegunud
Liiga palju faile ühes konteineris
Ümbriku allkirjastamine ebaõnnestus. Kontrolli mobiil-ID teenuse ligipääsu seadeid. https://www.id.ee/artikkel/asutustele-kus-allkirjastatakse-digidoc4-kliendi-kaudu-suuremates-kogustes-dokumente/
Samaaegsete pöördumiste limiit ületatud
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d23132282..dcc8a6722 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -320,6 +320,8 @@
https://www.id.ee/en/article/changing-id-card-pin-codes-and-puk-code/
Certificate status revoked
Certificate status unknown
+ Certificate has expired
+ Certificate not yet valid
ID-card\'s
Mobile-ID
Smart-ID
@@ -344,10 +346,8 @@
Mobile signing session is locked, try again later
An error occurred with the signers mobile phone
User is not a mobile-ID client
- Your mobile signing certificate is revoked
Your mobile signing certificate status is unknown
Your mobile signing certificate is suspended
- Your mobile signing certificate is expired
Too many datafiles in container
Failed to sign container. Check your mobile-ID service access settings. https://www.id.ee/en/article/for-organisations-that-sign-large-quantities-of-documents-using-digidoc4-client/
Simultaneous requests limit exceeded
diff --git a/libdigidoc-lib/build.gradle.kts b/libdigidoc-lib/build.gradle.kts
index 11e208b84..cd4a0d8f5 100644
--- a/libdigidoc-lib/build.gradle.kts
+++ b/libdigidoc-lib/build.gradle.kts
@@ -71,6 +71,7 @@ dependencies {
implementation(libs.androidx.hilt)
testImplementation(libs.junit)
+ testImplementation(libs.mockito.kotlin)
androidTestImplementation(libs.byte.buddy)
androidTestImplementation(libs.mockito.android) {
diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt
index f2560f3d6..a3e25a29a 100644
--- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt
+++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt
@@ -25,6 +25,7 @@ import ee.ria.DigiDoc.libdigidoclib.SignedContainer
import ee.ria.DigiDoc.libdigidoclib.init.libdigidocppDispatcher
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog
+import ee.ria.DigiDoc.utilsLib.signing.CertificateUtil
import ee.ria.DigiDoc.utilsLib.text.TextUtil.removeEmptyStrings
import ee.ria.libdigidocpp.ExternalSigner
import ee.ria.libdigidocpp.Signature
@@ -61,6 +62,7 @@ class ContainerWrapperImpl : ContainerWrapper {
): ByteArray =
withContext(libdigidocppDispatcher) {
debugLog(logTag, "Preparing signature (with role data: ${roleData != null})")
+ checkCertificateValidity(cert)
signature =
when {
roleData != null && signedContainer != null -> {
@@ -103,4 +105,14 @@ class ContainerWrapperImpl : ContainerWrapper {
signedContainer?.rawContainer()?.save()
debugLog(logTag, "Signature finalized and container saved")
}
+
+ @Throws(CertificateException::class)
+ private fun checkCertificateValidity(cert: ByteArray?) {
+ if (cert == null) {
+ throw CertificateException("Unable to validate a missing signing certificate")
+ }
+ val x509Certificate = CertificateUtil.x509Certificate(cert)
+ debugLog(logTag, "Signing certificate valid from ${x509Certificate.notBefore} to ${x509Certificate.notAfter}")
+ x509Certificate.checkValidity()
+ }
}
diff --git a/libdigidoc-lib/src/test/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapperTest.kt b/libdigidoc-lib/src/test/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapperTest.kt
new file mode 100644
index 000000000..63813fd06
--- /dev/null
+++ b/libdigidoc-lib/src/test/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapperTest.kt
@@ -0,0 +1,91 @@
+@file:Suppress("PackageName")
+
+package ee.ria.DigiDoc.libdigidoclib.domain.model
+
+import ee.ria.libdigidocpp.ExternalSigner
+import kotlinx.coroutines.runBlocking
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.kotlin.mock
+import java.math.BigInteger
+import java.security.KeyPairGenerator
+import java.security.cert.CertificateException
+import java.security.cert.CertificateExpiredException
+import java.security.cert.CertificateNotYetValidException
+import java.util.Calendar
+import java.util.Date
+
+class ContainerWrapperTest {
+ private val containerWrapper = ContainerWrapperImpl()
+ private val signer = mock()
+
+ @Test
+ fun containerWrapper_prepareSignature_throwsWhenSigningCertificateHasExpired() {
+ val expiredCertificate = certificate(fromYearsFromNow = -2, toYearsFromNow = -1)
+
+ assertThrows(CertificateExpiredException::class.java) {
+ runBlocking { containerWrapper.prepareSignature(signer, null, expiredCertificate, null) }
+ }
+ }
+
+ @Test
+ fun containerWrapper_prepareSignature_throwsWhenSigningCertificateIsNotYetValid() {
+ val futureCertificate = certificate(fromYearsFromNow = 1, toYearsFromNow = 2)
+
+ assertThrows(CertificateNotYetValidException::class.java) {
+ runBlocking { containerWrapper.prepareSignature(signer, null, futureCertificate, null) }
+ }
+ }
+
+ @Test
+ fun containerWrapper_prepareSignature_throwsCertificateExceptionWhenCertificateMissing() {
+ val exception =
+ assertThrows(CertificateException::class.java) {
+ runBlocking { containerWrapper.prepareSignature(signer, null, null, null) }
+ }
+
+ assertTrue(exception.message?.contains("missing signing certificate") == true)
+ }
+
+ @Test
+ fun containerWrapper_prepareSignature_passesCertificateCheckWhenCertificateIsCurrentlyValid() {
+ val validCertificate = certificate(fromYearsFromNow = -1, toYearsFromNow = 1)
+
+ val exception =
+ assertThrows(IllegalStateException::class.java) {
+ runBlocking { containerWrapper.prepareSignature(signer, null, validCertificate, null) }
+ }
+
+ assertTrue(exception.message?.contains("Unable to get container") == true)
+ }
+
+ private fun certificate(
+ fromYearsFromNow: Int,
+ toYearsFromNow: Int,
+ ): ByteArray {
+ val keyPair = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.generateKeyPair()
+ val name = X500Name("CN=RIA DigiDoc test")
+ val builder =
+ JcaX509v3CertificateBuilder(
+ name,
+ BigInteger.valueOf(1),
+ yearsFromNow(fromYearsFromNow),
+ yearsFromNow(toYearsFromNow),
+ name,
+ keyPair.public,
+ )
+ val contentSigner = JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private)
+ return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)).encoded
+ }
+
+ private fun yearsFromNow(years: Int): Date =
+ Calendar
+ .getInstance()
+ .apply { add(Calendar.YEAR, years) }
+ .time
+}
diff --git a/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt b/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt
index 35a522f0f..1cc1008af 100644
--- a/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt
+++ b/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt
@@ -70,6 +70,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.eq
import org.mockito.kotlin.isNull
+import org.mockito.kotlin.never
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever
import retrofit2.Call
@@ -78,6 +79,8 @@ import retrofit2.mock.Calls
import java.net.UnknownHostException
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateException
+import java.security.cert.CertificateExpiredException
+import java.security.cert.CertificateNotYetValidException
import javax.net.ssl.SSLPeerUnverifiedException
@RunWith(MockitoJUnitRunner::class)
@@ -376,6 +379,113 @@ class MobileSignServiceImplTest {
request.relyingPartyUUID = RELYING_PARTY_UUID
}
+ @Test
+ fun mobileSignService_processMobileIdRequest_prepareSignatureCertificateExpired() =
+ runTest {
+ doReturn(midRestServiceClient).whenever(serviceGenerator).createService(
+ context = context,
+ sslContext = null,
+ midSignServiceUrl = request.url,
+ certBundle = certBundle,
+ trustManagers = trustManagers,
+ proxySetting = proxySetting,
+ manualProxySettings = manualProxy,
+ )
+
+ doReturn(
+ Calls.response(
+ Response.success(
+ MobileCreateSignatureCertificateResponse(
+ result = MobileCertificateResultType.OK,
+ cert = cert,
+ ),
+ ),
+ ),
+ ).whenever(midRestServiceClient)
+ .getCertificate(
+ body = getMobileCertificateRequest,
+ )
+
+ whenever(
+ containerWrapper.prepareSignature(
+ signer = any(),
+ signedContainer = any(),
+ cert = any(),
+ roleData = isNull(),
+ ),
+ ).thenThrow(CertificateExpiredException())
+
+ mobileSignServiceImpl.processMobileIdRequest(
+ context = context,
+ signedContainer = signedContainer,
+ request = request,
+ roleDataRequest = null,
+ proxySetting = proxySetting,
+ manualProxySettings = manualProxy,
+ certificateBundle = certBundle,
+ accessTokenPath = "accessTokenPath",
+ accessTokenPass = "accessTokenPass",
+ )
+
+ verify(statusObserver, atLeastOnce()).onChanged(MobileCreateSignatureProcessStatus.CERTIFICATE_EXPIRED)
+
+ verify(containerWrapper, never()).finalizeSignature(any(), any(), any())
+ }
+
+ @Test
+ fun mobileSignService_processMobileIdRequest_prepareSignatureCertificateNotYetValid() =
+ runTest {
+ doReturn(midRestServiceClient).whenever(serviceGenerator).createService(
+ context = context,
+ sslContext = null,
+ midSignServiceUrl = request.url,
+ certBundle = certBundle,
+ trustManagers = trustManagers,
+ proxySetting = proxySetting,
+ manualProxySettings = manualProxy,
+ )
+
+ doReturn(
+ Calls.response(
+ Response.success(
+ MobileCreateSignatureCertificateResponse(
+ result = MobileCertificateResultType.OK,
+ cert = cert,
+ ),
+ ),
+ ),
+ ).whenever(midRestServiceClient)
+ .getCertificate(
+ body = getMobileCertificateRequest,
+ )
+
+ whenever(
+ containerWrapper.prepareSignature(
+ signer = any(),
+ signedContainer = any(),
+ cert = any(),
+ roleData = isNull(),
+ ),
+ ).thenThrow(CertificateNotYetValidException())
+
+ mobileSignServiceImpl.processMobileIdRequest(
+ context = context,
+ signedContainer = signedContainer,
+ request = request,
+ roleDataRequest = null,
+ proxySetting = proxySetting,
+ manualProxySettings = manualProxy,
+ certificateBundle = certBundle,
+ accessTokenPath = "accessTokenPath",
+ accessTokenPass = "accessTokenPass",
+ )
+
+ verify(statusObserver, atLeastOnce())
+ .onChanged(MobileCreateSignatureProcessStatus.CERTIFICATE_NOT_YET_VALID)
+
+ verify(containerWrapper, never()).finalizeSignature(any(), any(), any())
+ }
+
@Test
fun mobileSignService_processMobileIdRequest_success() =
runTest {
diff --git a/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt b/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt
index 000748484..1affcf8ac 100644
--- a/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt
+++ b/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt
@@ -75,6 +75,8 @@ import java.security.KeyStoreException
import java.security.NoSuchAlgorithmException
import java.security.UnrecoverableKeyException
import java.security.cert.CertificateException
+import java.security.cert.CertificateExpiredException
+import java.security.cert.CertificateNotYetValidException
import java.util.Base64
import javax.inject.Inject
import javax.inject.Singleton
@@ -394,6 +396,24 @@ class MobileSignServiceImpl
return
}
}
+ } catch (cee: CertificateExpiredException) {
+ errorLog(
+ logTag,
+ "Failed to sign with Mobile-ID. Signing certificate has expired. " +
+ "Exception message: ${cee.message}",
+ cee,
+ )
+ postFault(RESTServiceFault(MobileCreateSignatureProcessStatus.CERTIFICATE_EXPIRED))
+ return
+ } catch (cnyve: CertificateNotYetValidException) {
+ errorLog(
+ logTag,
+ "Failed to sign with Mobile-ID. Signing certificate is not yet valid. " +
+ "Exception message: ${cnyve.message}",
+ cnyve,
+ )
+ postFault(RESTServiceFault(MobileCreateSignatureProcessStatus.CERTIFICATE_NOT_YET_VALID))
+ return
} catch (e: UnknownHostException) {
errorLog(
logTag,
diff --git a/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/mid/dto/response/MobileCreateSignatureProcessStatus.kt b/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/mid/dto/response/MobileCreateSignatureProcessStatus.kt
index acbb7ab81..9f37c91af 100644
--- a/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/mid/dto/response/MobileCreateSignatureProcessStatus.kt
+++ b/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/mid/dto/response/MobileCreateSignatureProcessStatus.kt
@@ -35,6 +35,8 @@ enum class MobileCreateSignatureProcessStatus {
INVALID_ACCESS_RIGHTS,
OCSP_INVALID_TIME_SLOT,
CERTIFICATE_REVOKED,
+ CERTIFICATE_EXPIRED,
+ CERTIFICATE_NOT_YET_VALID,
GENERAL_ERROR,
NO_RESPONSE,
INVALID_COUNTRY_CODE,
diff --git a/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/sid/dto/response/SessionStatusResponseProcessStatus.kt b/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/sid/dto/response/SessionStatusResponseProcessStatus.kt
index c322818af..2bb18c166 100644
--- a/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/sid/dto/response/SessionStatusResponseProcessStatus.kt
+++ b/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/sid/dto/response/SessionStatusResponseProcessStatus.kt
@@ -44,6 +44,8 @@ enum class SessionStatusResponseProcessStatus {
INVALID_ACCESS_RIGHTS,
OCSP_INVALID_TIME_SLOT,
CERTIFICATE_REVOKED,
+ CERTIFICATE_EXPIRED,
+ CERTIFICATE_NOT_YET_VALID,
OLD_API,
UNDER_MAINTENANCE,
GENERAL_ERROR,
diff --git a/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt b/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt
index ec3edaeb1..0320eb13b 100644
--- a/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt
+++ b/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt
@@ -79,6 +79,8 @@ import java.io.IOException
import java.net.UnknownHostException
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateException
+import java.security.cert.CertificateExpiredException
+import java.security.cert.CertificateNotYetValidException
import javax.net.ssl.SSLPeerUnverifiedException
@RunWith(MockitoJUnitRunner::class)
@@ -387,6 +389,163 @@ class SmartSignServiceImplTest {
verifyNoInteractions(call)
}
+ @Test
+ fun smartSignService_processSmartIdRequest_prepareSignatureCertificateExpired() =
+ runTest {
+ doReturn(sidRestServiceClient).whenever(serviceGenerator).createService(
+ context = context,
+ sidSignServiceUrl = request.url + "/",
+ certBundle = certBundle,
+ proxySetting = proxySetting,
+ manualProxySettings = manualProxy,
+ )
+
+ doReturn(
+ Calls.response(
+ Response.success(
+ SessionResponse(
+ sessionID = "sessionID",
+ ),
+ ),
+ ),
+ ).whenever(sidRestServiceClient)
+ .getCertificateV2(
+ semanticsIdentifier = "PNOEE-60001019906",
+ body = getSmartCertificateRequest,
+ )
+
+ doReturn(
+ Calls.response(
+ Response.success(
+ SessionStatusResponse(
+ state = SessionStatusResponseProcessState.COMPLETE,
+ result =
+ SessionStatusResponseStatus(
+ endResult = SessionStatusResponseProcessStatus.OK,
+ documentNumber = "documentNumber",
+ ),
+ signature =
+ SmartSignatureResponse(
+ value = "dTzZ20E8kmoXXBAh5cV5yw==",
+ algorithm = "SmartIDSignatureAlgorithm",
+ ),
+ cert =
+ SmartCertificateResponse(
+ value = cert,
+ assuranceLevel = "assuranceLevel",
+ certificateLevel = "certificateLevel",
+ ),
+ ),
+ ),
+ ),
+ ).whenever(sidRestServiceClient)
+ .getSessionStatus("sessionID", 5000)
+
+ whenever(
+ containerWrapper.prepareSignature(
+ signer = any(),
+ signedContainer = any(),
+ cert = any(),
+ roleData = isNull(),
+ ),
+ ).thenThrow(CertificateExpiredException())
+
+ smartSignServiceImpl.processSmartIdRequest(
+ context = context,
+ signedContainer = signedContainer,
+ request = request,
+ roleDataRequest = null,
+ proxySetting = proxySetting,
+ manualProxySettings = manualProxy,
+ certificateBundle = certBundle,
+ accessTokenPath = "accessTokenPath",
+ accessTokenPass = "accessTokenPass",
+ )
+
+ verify(statusObserver, atLeastOnce()).onChanged(SessionStatusResponseProcessStatus.CERTIFICATE_EXPIRED)
+
+ verify(containerWrapper, never()).finalizeSignature(any(), any(), any())
+ }
+
+ @Test
+ fun smartSignService_processSmartIdRequest_prepareSignatureCertificateNotYetValid() =
+ runTest {
+ doReturn(sidRestServiceClient).whenever(serviceGenerator).createService(
+ context = context,
+ sidSignServiceUrl = request.url + "/",
+ certBundle = certBundle,
+ proxySetting = proxySetting,
+ manualProxySettings = manualProxy,
+ )
+
+ doReturn(
+ Calls.response(
+ Response.success(
+ SessionResponse(
+ sessionID = "sessionID",
+ ),
+ ),
+ ),
+ ).whenever(sidRestServiceClient)
+ .getCertificateV2(
+ semanticsIdentifier = "PNOEE-60001019906",
+ body = getSmartCertificateRequest,
+ )
+
+ doReturn(
+ Calls.response(
+ Response.success(
+ SessionStatusResponse(
+ state = SessionStatusResponseProcessState.COMPLETE,
+ result =
+ SessionStatusResponseStatus(
+ endResult = SessionStatusResponseProcessStatus.OK,
+ documentNumber = "documentNumber",
+ ),
+ signature =
+ SmartSignatureResponse(
+ value = "dTzZ20E8kmoXXBAh5cV5yw==",
+ algorithm = "SmartIDSignatureAlgorithm",
+ ),
+ cert =
+ SmartCertificateResponse(
+ value = cert,
+ assuranceLevel = "assuranceLevel",
+ certificateLevel = "certificateLevel",
+ ),
+ ),
+ ),
+ ),
+ ).whenever(sidRestServiceClient)
+ .getSessionStatus("sessionID", 5000)
+
+ whenever(
+ containerWrapper.prepareSignature(
+ signer = any(),
+ signedContainer = any(),
+ cert = any(),
+ roleData = isNull(),
+ ),
+ ).thenThrow(CertificateNotYetValidException())
+
+ smartSignServiceImpl.processSmartIdRequest(
+ context = context,
+ signedContainer = signedContainer,
+ request = request,
+ roleDataRequest = null,
+ proxySetting = proxySetting,
+ manualProxySettings = manualProxy,
+ certificateBundle = certBundle,
+ accessTokenPath = "accessTokenPath",
+ accessTokenPass = "accessTokenPass",
+ )
+
+ verify(statusObserver, atLeastOnce())
+ .onChanged(SessionStatusResponseProcessStatus.CERTIFICATE_NOT_YET_VALID)
+
+ verify(containerWrapper, never()).finalizeSignature(any(), any(), any())
+ }
+
@Test
fun smartSignService_processSmartIdRequest_success() =
runTest {
diff --git a/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt b/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt
index c3b1ac2ca..8ad9e2fbb 100644
--- a/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt
+++ b/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt
@@ -64,6 +64,8 @@ import java.net.UnknownHostException
import java.nio.charset.StandardCharsets
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateException
+import java.security.cert.CertificateExpiredException
+import java.security.cert.CertificateNotYetValidException
import java.util.Base64
import javax.inject.Inject
import javax.inject.Singleton
@@ -323,6 +325,24 @@ class SmartSignServiceImpl
setErrorState(errorString)
return
}
+ } catch (cee: CertificateExpiredException) {
+ errorLog(
+ logTag,
+ "Failed to sign with Smart-ID. Signing certificate has expired. " +
+ "Exception message: ${cee.message}",
+ cee,
+ )
+ postFault(ServiceFault(SessionStatusResponseProcessStatus.CERTIFICATE_EXPIRED))
+ return
+ } catch (cnyve: CertificateNotYetValidException) {
+ errorLog(
+ logTag,
+ "Failed to sign with Smart-ID. Signing certificate is not yet valid. " +
+ "Exception message: ${cnyve.message}",
+ cnyve,
+ )
+ postFault(ServiceFault(SessionStatusResponseProcessStatus.CERTIFICATE_NOT_YET_VALID))
+ return
} catch (e: UnknownHostException) {
postFault(ServiceFault(SessionStatusResponseProcessStatus.NO_RESPONSE))
errorLog(