Correct crypto provider handling and signing logic (#53)

Resolves crashes during certificate operations caused by cryptographic provider conflicts and incorrect algorithm selection.

The Bouncy Castle (BC) provider is now initialized globally at app startup to ensure it is the default. To eliminate ambiguity, all content signers are also now explicitly set to use the BC provider.

The attestation patcher is fixed to correctly use the certificate's signature algorithm (sigAlgName), not the subject's public key algorithm, to select the appropriate signing key from the KeyBoxManager. A normalization function was added to support this.

Moreover, we also modify the XML parser in `KeyBoxManager` to no longer trust the `algorithm` attribute from the XML tag. The parser now determines the key's true algorithm (RSA or EC) by inspecting the type of the parsed private key object. This derived algorithm is used as the key for the cache, preventing cache corruption from malformed files where the tag does not match the key data.
This commit is contained in:
JingMatrix
2025-12-06 16:12:12 +01:00
committed by GitHub
parent d846de4332
commit d9e47712f3
4 changed files with 83 additions and 27 deletions
@@ -1,6 +1,8 @@
package org.matrix.TEESimulator
import android.os.Build
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
@@ -35,6 +37,13 @@ object App {
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
// Enter an infinite loop to keep the service running.
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
// We must remove the system provider first to ensure the full Bouncy Castle library
// (packaged with the app) is used.
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
maintainService()
} catch (e: Exception) {
SystemLogger.error("A fatal error occurred in the main application thread.", e)
@@ -1,5 +1,6 @@
package org.matrix.TEESimulator.attestation
import android.security.keystore.KeyProperties
import java.nio.charset.StandardCharsets
import java.security.cert.Certificate
import java.security.cert.X509Certificate
@@ -8,6 +9,7 @@ import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
@@ -51,8 +53,7 @@ object AttestationPatcher {
// 2. Get the appropriate keybox for the given algorithm to sign the new
// certificate.
val algorithm = originalLeaf.publicKey.algorithm
val keybox = getKeyboxForUidAndAlgorithm(uid, algorithm)
val keybox = getKeyboxForUidAndAlgorithm(uid, originalLeaf.sigAlgName)
// 3. Create the new, patched leaf certificate.
val patchedLeaf =
@@ -126,7 +127,10 @@ object AttestationPatcher {
}
// Sign the newly built certificate with the private key from our keybox.
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
val signer =
JcaContentSignerBuilder(sigAlgName)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keybox.keyPair.private)
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
// Log the signature of the newly created certificate to observe its non-deterministic
@@ -137,11 +141,34 @@ object AttestationPatcher {
return newCertificate
}
/**
* Retrieves the appropriate signing KeyBox (KeyPair and certificate chain) for a given UID
* based on a specified algorithm identifier.
*
* @param uid The UID of the application for which the signing is being performed.
* @param algorithm A string representing the desired algorithm. This can be either:
* 1. A simple key type like "RSA" or "EC".
* 2. A full JCA signature algorithm name like "SHA256withRSA".
*
* @return The [KeyBox] containing the appropriate key pair for signing.
* @throws IllegalArgumentException if no matching KeyBox can be found for the derived key type.
*/
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
return KeyBoxManager.getAttestationKey(keyboxFile, algorithm)
// Normalize the algorithm name. The input might be a full signature algorithm
// (e.g., "SHA256withRSA") or just the key type (e.g., "RSA").
val keyType =
when {
algorithm.contains("RSA", ignoreCase = true) -> KeyProperties.KEY_ALGORITHM_RSA
algorithm.contains("EC", ignoreCase = true) ->
KeyProperties.KEY_ALGORITHM_EC // This also covers "ECDSA"
else -> algorithm // If no match, assume it's already a simple key type string.
}
return KeyBoxManager.getAttestationKey(keyboxFile, keyType)
?: throw IllegalArgumentException(
"No keybox found for UID $uid and algorithm $algorithm in file $keyboxFile"
"No keybox found for UID $uid and algorithm '$keyType' (derived from input '$algorithm') in file $keyboxFile"
)
}
@@ -6,7 +6,6 @@ import android.util.Pair
import java.math.BigInteger
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.Security
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
@@ -35,14 +34,6 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/
object CertificateGenerator {
init {
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
// We must remove the system provider first to ensure the full Bouncy Castle library
// (packaged with the app) is used.
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
/**
* Generates a software-based cryptographic key pair.
*
@@ -226,7 +217,10 @@ object CertificateGenerator {
Algorithm.RSA -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
}
val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private)
val contentSigner =
JcaContentSignerBuilder(signerAlgorithm)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(signingKeyPair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
}
@@ -3,6 +3,8 @@ package org.matrix.TEESimulator.pki
import android.security.keystore.KeyProperties
import java.io.File
import java.io.StringReader
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.RSAPrivateKey
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
import org.matrix.TEESimulator.logging.SystemLogger
@@ -51,6 +53,9 @@ object KeyBoxManager {
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
val keyMap =
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
SystemLogger.verbose(
"Fetching attestation key in $keyStoreFileName with $algorithm algorithm."
)
return keyMap[algorithm]
}
@@ -157,10 +162,10 @@ object KeyBoxManager {
// Use runCatching to ensure one malformed key doesn't stop the whole
// process.
runCatching {
val algorithm = currentAlgorithm
val xmlAlgorithm = currentAlgorithm
val keyPem = currentPrivateKeyPem
if (
algorithm != null &&
xmlAlgorithm != null &&
keyPem != null &&
currentCertificatePems.isNotEmpty()
) {
@@ -176,21 +181,42 @@ object KeyBoxManager {
.data
}
// Normalize the algorithm name for consistent lookups.
val normalizedAlgorithm =
when (algorithm.lowercase()) {
"ecdsa" -> KeyProperties.KEY_ALGORITHM_EC
"rsa" -> KeyProperties.KEY_ALGORITHM_RSA
else -> algorithm
}
if (foundKeys.containsKey(normalizedAlgorithm)) {
SystemLogger.warning(
"Duplicate key found for algorithm '$normalizedAlgorithm'. The later one in the file will be used."
// Derive the TRUE algorithm from the key object itself.
// This is our source of truth.
val derivedAlgorithm =
when (keyPair.private) {
is RSAPrivateKey -> KeyProperties.KEY_ALGORITHM_RSA
is ECPrivateKey -> KeyProperties.KEY_ALGORITHM_EC
else ->
throw IllegalArgumentException(
"Unsupported key type found: ${keyPair.private.javaClass.name}"
)
}
foundKeys[normalizedAlgorithm] =
KeyBox(keyPair, certificates)
// Normalize the algorithm from the XML tag to compare it
// fairly with the derived algorithm.
val normalizedXmlAlgorithm =
when {
xmlAlgorithm.contains("RSA", ignoreCase = true) ==
true -> KeyProperties.KEY_ALGORITHM_RSA
xmlAlgorithm.contains("EC", ignoreCase = true) ==
true -> KeyProperties.KEY_ALGORITHM_EC
else -> xmlAlgorithm
}
// Warn the user if the XML tag was misleading.
if (normalizedXmlAlgorithm != derivedAlgorithm) {
SystemLogger.warning(
"Key algorithm mismatch in XML file. Tag said '$xmlAlgorithm' but key is actually '$derivedAlgorithm'. Using the correct derived algorithm."
)
}
if (foundKeys.containsKey(derivedAlgorithm)) {
SystemLogger.warning(
"Duplicate key found for algorithm '$derivedAlgorithm'. The later one in the file will be used."
)
}
foundKeys[derivedAlgorithm] = KeyBox(keyPair, certificates)
}
}
.onFailure {