Skip to content

Commit

Permalink
Add debug feature for installing custom csig validation certs
Browse files Browse the repository at this point in the history
This is useful when the csig is signed by a different key than the OTA
and installing the certificate in `/system/etc/security/otacerts.zip`
isn't desired.

Fixes: #47

Signed-off-by: Andrew Gunnerson <[email protected]>
  • Loading branch information
chenxiaolong committed Mar 26, 2024
1 parent 132c23c commit 82ce7e7
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 35 deletions.
35 changes: 34 additions & 1 deletion app/src/main/java/com/chiller3/custota/Preferences.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

Expand All @@ -9,9 +9,13 @@ import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Base64
import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import java.io.ByteArrayInputStream
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate

class Preferences(private val context: Context) {
companion object {
Expand All @@ -36,9 +40,11 @@ class Preferences(private val context: Context) {
const val PREF_OPEN_LOG_DIR = "open_log_dir"
const val PREF_ALLOW_REINSTALL = "allow_reinstall"
const val PREF_REVERT_COMPLETED = "revert_completed"
const val PREF_INSTALL_CSIG_CERT = "install_csig_cert"

// Not associated with a UI preference
private const val PREF_DEBUG_MODE = "debug_mode"
private const val PREF_CSIG_CERTS = "csig_certs"

// Legacy preferences
private const val PREF_OTA_SERVER_URL = "ota_server_url"
Expand Down Expand Up @@ -118,6 +124,33 @@ class Preferences(private val context: Context) {
get() = prefs.getBoolean(PREF_ALLOW_REINSTALL, false)
set(enabled) = prefs.edit { putBoolean(PREF_ALLOW_REINSTALL, enabled) }

var csigCerts: Set<X509Certificate>
get() {
val encoded = prefs.getStringSet(PREF_CSIG_CERTS, emptySet())!!
val factory = CertificateFactory.getInstance("X.509")

return encoded
.asSequence()
.map { base64 ->
val der = Base64.decode(base64, Base64.DEFAULT)

ByteArrayInputStream(der).use {
factory.generateCertificate(it) as X509Certificate
}
}
.toSet()
}
set(certs) {
val encoded = certs
.asSequence()
.map {
Base64.encodeToString(it.encoded, Base64.NO_WRAP)
}
.toSet()

prefs.edit { putStringSet(PREF_CSIG_CERTS, encoded) }
}

/** Migrate legacy preferences to current preferences. */
fun migrate() {
if (prefs.contains(PREF_OTA_SERVER_URL)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
* Based on BCR code.
*/
Expand Down Expand Up @@ -61,6 +61,7 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic
private lateinit var prefVersion: LongClickablePreference
private lateinit var prefOpenLogDir: Preference
private lateinit var prefRevertCompleted: Preference
private lateinit var prefInstallCsigCert: Preference

private lateinit var scheduledAction: UpdaterThread.Action

Expand All @@ -72,6 +73,12 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic
startActivity(Permissions.getAppInfoIntent(requireContext()))
}
}
private val requestSafInstallCsigCert =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let {
viewModel.installCsigCert(it)
}
}

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences_root, rootKey)
Expand Down Expand Up @@ -116,6 +123,9 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic
prefRevertCompleted = findPreference(Preferences.PREF_REVERT_COMPLETED)!!
prefRevertCompleted.onPreferenceClickListener = this

prefInstallCsigCert = findPreference(Preferences.PREF_INSTALL_CSIG_CERT)!!
prefInstallCsigCert.onPreferenceClickListener = this

refreshCheckForUpdates()
refreshOtaSource()
refreshVersion()
Expand Down Expand Up @@ -252,23 +262,38 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic
performAction()
return true
}
prefInstallCsigCert -> {
// See AOSP's frameworks/base/mime/java-res/android.mime.types
requestSafInstallCsigCert.launch(arrayOf(
"application/x-x509-ca-cert",
"application/x-x509-user-cert",
"application/x-x509-server-cert",
"application/x-pem-file",
))
return true
}
}

return false
}

override fun onPreferenceLongClick(preference: Preference): Boolean {
when (preference) {
prefOtaSource -> {
when {
preference === prefOtaSource -> {
prefs.otaSource = null
return true
}
prefVersion -> {
preference === prefVersion -> {
prefs.isDebugMode = !prefs.isDebugMode
refreshVersion()
refreshDebugPrefs()
return true
}
preference.key.startsWith(PREF_CERT_PREFIX) -> {
val index = preference.key.removePrefix(PREF_CERT_PREFIX).toInt()
viewModel.removeCsigCert(index)
return true
}
}

return false
Expand All @@ -286,7 +311,7 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic
}
}

private fun addCertPreferences(certs: List<X509Certificate>) {
private fun addCertPreferences(certs: List<Pair<X509Certificate, Boolean>>) {
val context = requireContext()

prefNoCertificates.isVisible = certs.isEmpty()
Expand All @@ -299,11 +324,14 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic
}
}

for ((i, cert) in certs.withIndex()) {
val p = Preference(context).apply {
for ((i, item) in certs.withIndex()) {
val (cert, isSystem) = item
val validates = if (isSystem) { "OTA + csig" } else { "csig" }

val p = LongClickablePreference(context).apply {
key = PREF_CERT_PREFIX + i
isPersistent = false
title = getString(R.string.pref_certificate_name, (i + 1).toString())
title = getString(R.string.pref_certificate_name, (i + 1).toString(), validates)
summary = buildString {
append(getString(R.string.pref_certificate_desc_subject,
cert.subjectDN.toString()))
Expand All @@ -316,6 +344,10 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic
append(getString(R.string.pref_certificate_desc_type, cert.typeName))
}
isIconSpaceReserved = false

if (!isSystem) {
onPreferenceLongClickListener = this@SettingsFragment
}
}

categoryCertificates.addPreference(p)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

package com.chiller3.custota.settings

import android.app.Application
import android.net.Uri
import android.service.oemlock.IOemLockService
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.chiller3.custota.Preferences
import com.chiller3.custota.extension.toSingleLineString
import com.chiller3.custota.updater.OtaPaths
import com.chiller3.custota.wrapper.ServiceManagerProxy
Expand All @@ -18,34 +21,96 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate

class SettingsViewModel : ViewModel() {
private val _certs = MutableStateFlow<List<X509Certificate>>(emptyList())
val certs: StateFlow<List<X509Certificate>> = _certs
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val prefs = Preferences(getApplication())

private val _certs = MutableStateFlow<List<Pair<X509Certificate, Boolean>>>(emptyList())
val certs: StateFlow<List<Pair<X509Certificate, Boolean>>> = _certs

private val _bootloaderStatus = MutableStateFlow<BootloaderStatus?>(null)
val bootloaderStatus: StateFlow<BootloaderStatus?> = _bootloaderStatus

init {
loadCertificates()
loadCerts()
}

private fun loadCertificates() {
private fun loadCerts() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val certs = try {
val systemCerts = try {
withContext(Dispatchers.IO) {
OtaPaths.otaCerts
} catch (e: Exception) {
Log.w(TAG, "Failed to load certificates")
emptyList()
}
} catch (e: Exception) {
Log.w(TAG, "Failed to load system certificates", e)
emptySet()
}

val csigCerts = try {
// Avoid duplicates.
prefs.csigCerts.subtract(systemCerts)
} catch (e: Exception) {
Log.w(TAG, "Failed to load user csig certificates", e)
emptySet()
}

_certs.update { systemCerts.sortedWith(certCompare).map { it to true } +
csigCerts.sortedWith(certCompare).map { it to false } }
}
}

fun installCsigCert(uri: Uri) {
viewModelScope.launch {
val cert = try {
withContext(Dispatchers.IO) {
val factory = CertificateFactory.getInstance("X.509")

getApplication<Application>().contentResolver.openInputStream(uri)?.use {
factory.generateCertificate(it) as X509Certificate
} ?: throw IOException("Null input stream: $uri")
}
} catch (e: Exception) {
Log.w(TAG, "Failed to load certificate: $uri")
return@launch
}

val allCerts = _certs.value

_certs.update { certs }
if (allCerts.any { it.first == cert }) {
Log.w(TAG, "Certificate already exists: $cert")
return@launch
}

Log.d(TAG, "Installing user csig certificate: $cert")

prefs.csigCerts = sequence {
yieldAll(allCerts.asSequence().filter { !it.second }.map { it.first })
yield(cert)
}.toSet()

loadCerts()
}
}

fun removeCsigCert(index: Int) {
val allCerts = _certs.value
val cert = allCerts[index].first
require(!allCerts[index].second) { "Tried to delete system certificate at $index: $cert" }

Log.d(TAG, "Removing user csig certificate: $cert")

prefs.csigCerts = allCerts
.asSequence()
.filterIndexed { i, _ -> i != index }
.map { it.first }
.toSet()

loadCerts()
}

fun refreshBootloaderStatus() {
val status = try {
val service = IOemLockService.Stub.asInterface(
Expand Down Expand Up @@ -76,5 +141,10 @@ class SettingsViewModel : ViewModel() {

companion object {
private val TAG = SettingsViewModel::class.java.simpleName

private val certCompare = compareBy<X509Certificate>(
{ it.subjectDN.name },
{ it.serialNumber },
)
}
}
6 changes: 3 additions & 3 deletions app/src/main/java/com/chiller3/custota/updater/OtaPaths.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

Expand Down Expand Up @@ -35,9 +35,9 @@ object OtaPaths {
const val METADATA_NAME = "metadata.pb"

/** Parse X509 certificates from [OTACERTS_ZIP]. */
val otaCerts: List<X509Certificate>
val otaCerts: Set<X509Certificate>
get() {
val result = mutableListOf<X509Certificate>()
val result = mutableSetOf<X509Certificate>()
val factory = CertificateFactory.getInstance("X.509")

ZipFile(OTACERTS_ZIP).use { zip ->
Expand Down
11 changes: 4 additions & 7 deletions app/src/main/java/com/chiller3/custota/updater/UpdaterThread.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

Expand Down Expand Up @@ -376,15 +376,12 @@ class UpdaterThread(
val csigCms = CMSSignedData(csigRaw)

// Verify the signature against the same OTA certs as what's used for the payload.
val csigValid = OtaPaths.otaCerts.any { cert ->
val csigCert = (OtaPaths.otaCerts + prefs.csigCerts).find { cert ->
csigCms.signerInfos.any { signerInfo ->
signerInfo.verify(JcaSimpleSignerInfoVerifierBuilder().build(cert))
}
}
if (!csigValid) {
throw ValidationException("csig is not signed by a trusted key")
}
Log.d(TAG, "csig signature is valid")
} ?: throw ValidationException("csig is not signed by a trusted key")
Log.d(TAG, "csig is signed by: $csigCert")

val csigInfoRaw = String(csigCms.signedContent.content as ByteArray)
val csigInfo: CsigInfo = Json.decodeFromString(csigInfoRaw)
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-vi/values.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<!-- General preferences -->
<string name="pref_check_for_updates_name">Kiểm tra cập nhật OTA</string>
<string name="pref_check_for_updates_desc">Đặt lịch kiểm tra cập nhật OTA.</string>
<string name="pref_certificate_name">Chứng chỉ %1$s</string>
<string name="pref_certificate_name">Chứng chỉ %1$s (%2$s)</string>
<string name="pref_certificate_desc_subject">Chủ đề: %1$s</string>
<string name="pref_certificate_desc_serial">Số sê-ri: %1$s</string>
<string name="pref_certificate_desc_type">Loại: %1$s</string>
Expand Down
Loading

0 comments on commit 82ce7e7

Please sign in to comment.