Skip to content

Commit

Permalink
Rework BouncyCastleSecureArea. (#392)
Browse files Browse the repository at this point in the history
First of all, rename this to SoftwareSecureArea since there is no need
to leak the fact that we're currently using BouncyCastle which could
change in the future.

Instead of always using self-signed keys, make it possible to specify
the attestation key to use at key creation time. Without this we
cannot easily support curves which cannot be used for signing (for
example X25519). Modify holder app to create an attestation root
on demand.

Introduce attestation extension so SoftwareSecureArea keys also
can convey the attestation challenge. The OID for this has been
reserved (it's 1.3.6.1.4.1.11129.2.1.39) and we can extend this
if needed. Add code for dealing with this and use it in
SoftwareSecureArea.

Show the curve of DeviceKey in the reader app.

Make sure we use the BouncyCastle library bundled with the app
instead of the Conscrypt-based implementation that may come
with the OS. Do this for both wallet and reader app.

This has been manually test with mdocs using both ECDSA and MAC
authentication with all curves except Ed25519, X25519, Ed448,
and X448. These curves still have some serialization problems,
we'll revisit this in a future PR.

The app still uses the name "Bouncy Castle", we'll address that
in a future PR.

Also update to the latest available BouncyCastle, version 1.70.

Test: Manually tested.
Test: All unit tests pass.
  • Loading branch information
davidz25 authored Oct 26, 2023
1 parent 954f661 commit b4e1051
Show file tree
Hide file tree
Showing 14 changed files with 532 additions and 269 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ import com.android.identity.util.Logger
import com.android.identity.wallet.util.PeriodicKeysRefreshWorkRequest
import com.android.identity.wallet.util.PreferencesHelper
import com.google.android.material.color.DynamicColors
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security

class HolderApp: Application() {

override fun onCreate() {
super.onCreate()
Logger.setLogPrinter(AndroidLogPrinter())
// This is needed to prefer BouncyCastle bundled with the app instead of the Conscrypt
// based implementation included in the OS itself.
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
DynamicColors.applyToActivitiesIfAvailable(this)
PreferencesHelper.initialize(this)
PeriodicKeysRefreshWorkRequest(this).schedulePeriodicKeysRefreshing()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.android.identity.android.securearea.AndroidKeystoreSecureArea
import com.android.identity.securearea.BouncyCastleSecureArea
import com.android.identity.securearea.SoftwareSecureArea
import com.android.identity.securearea.SecureArea.ALGORITHM_ES256
import com.android.identity.wallet.R
import com.android.identity.wallet.authprompt.UserAuthPromptBuilder
Expand Down Expand Up @@ -161,7 +161,7 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() {
}

private fun onPassphraseProvided(passphrase: String) {
val unlockData = BouncyCastleSecureArea.KeyUnlockData(passphrase)
val unlockData = SoftwareSecureArea.KeyUnlockData(passphrase)
val result = viewModel.sendResponseForSelection(unlockData)
onSendResponseResult(result)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import com.android.identity.internal.Util
import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator
import com.android.identity.mdoc.mso.StaticAuthDataGenerator
import com.android.identity.mdoc.util.MdocUtil
import com.android.identity.securearea.BouncyCastleSecureArea
import com.android.identity.securearea.SecureArea
import com.android.identity.securearea.SecureArea.KeyLockedException
import com.android.identity.securearea.SecureArea.KeyPurpose
import com.android.identity.securearea.SecureAreaRepository
import com.android.identity.securearea.SoftwareSecureArea
import com.android.identity.storage.EphemeralStorageEngine
import com.android.identity.util.Timestamp
import com.android.identity.wallet.document.DocumentInformation
import com.android.identity.wallet.document.KeysAndCertificates
Expand All @@ -25,6 +27,8 @@ import com.android.identity.wallet.selfsigned.ProvisionInfo
import com.android.identity.wallet.util.DocumentData.MICOV_DOCTYPE
import com.android.identity.wallet.util.DocumentData.MVR_DOCTYPE
import java.io.File
import java.security.KeyPair
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Instant
import java.time.ZoneId
Expand All @@ -45,16 +49,40 @@ class ProvisioningUtil private constructor(
private val androidKeystoreSecureArea: SecureArea
get() = AndroidKeystoreSecureArea(context, storageEngine)

private val bouncyCastleSecureArea: SecureArea
get() = BouncyCastleSecureArea(storageEngine)
private val softwareSecureArea: SecureArea
get() = SoftwareSecureArea(storageEngine)

val credentialStore by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
val keystoreEngineRepository = SecureAreaRepository()
keystoreEngineRepository.addImplementation(androidKeystoreSecureArea)
keystoreEngineRepository.addImplementation(bouncyCastleSecureArea)
keystoreEngineRepository.addImplementation(softwareSecureArea)
CredentialStore(storageEngine, keystoreEngineRepository)
}

private lateinit var softwareAttestationKey: PrivateKey
private lateinit var softwareAttestationKeySignatureAlgorithm: String
private lateinit var softwareAttestationKeyCertification: List<X509Certificate>

private fun initSoftwareAttestationKey() {
val secureArea = SoftwareSecureArea(EphemeralStorageEngine())
val now = Timestamp.now()
secureArea.createKey(
"SoftwareAttestationRoot",
SoftwareSecureArea.CreateKeySettings.Builder("".toByteArray())
.setEcCurve(SecureArea.EC_CURVE_P256)
.setKeyPurposes(SecureArea.KEY_PURPOSE_SIGN)
.setSubject("CN=Software Attestation Root")
.setValidityPeriod(
now,
Timestamp.ofEpochMilli(now.toEpochMilli() + 10L * 86400 * 365 * 1000)
)
.build()
)
softwareAttestationKey = secureArea.getPrivateKey("SoftwareAttestationRoot", null)
softwareAttestationKeySignatureAlgorithm = "SHA256withECDSA"
softwareAttestationKeyCertification = secureArea.getKeyInfo("SoftwareAttestationRoot").attestation
}

fun provisionSelfSigned(
nameSpacedData: NameSpacedData,
provisionInfo: ProvisionInfo,
Expand Down Expand Up @@ -217,8 +245,8 @@ class ProvisioningUtil private constructor(
)
}

is BouncyCastleSecureArea -> {
val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as BouncyCastleSecureArea.KeyInfo
is SoftwareSecureArea -> {
val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as SoftwareSecureArea.KeyInfo
createBouncyCastleKeystoreSettings(
mDocAuthOption = AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption),
ecCurve = keyInfo.ecCurve
Expand Down Expand Up @@ -266,13 +294,18 @@ class ProvisioningUtil private constructor(
passphrase: String? = null,
mDocAuthOption: AddSelfSignedScreenState.MdocAuthStateOption,
ecCurve: Int
): BouncyCastleSecureArea.CreateKeySettings {
): SoftwareSecureArea.CreateKeySettings {
if (!this::softwareAttestationKey.isInitialized) {
initSoftwareAttestationKey()
}
val keyPurpose = mDocAuthOption.toKeyPurpose()
val builder = BouncyCastleSecureArea.CreateKeySettings.Builder()
val builder = SoftwareSecureArea.CreateKeySettings.Builder("DoNotCare".toByteArray())
.setAttestationKey(softwareAttestationKey,
softwareAttestationKeySignatureAlgorithm, softwareAttestationKeyCertification)
.setPassphraseRequired(passphrase != null, passphrase)
.setKeyPurposes(keyPurpose)
.setEcCurve(ecCurve)
.setKeyPurposes(SecureArea.KEY_PURPOSE_SIGN or SecureArea.KEY_PURPOSE_AGREE_KEY)
.setKeyPurposes(mDocAuthOption.toKeyPurpose())
return builder.build()
}

Expand Down Expand Up @@ -351,7 +384,7 @@ class ProvisioningUtil private constructor(
private fun SecureArea.toSecureAreaState(): SecureAreaImplementationState {
return when (this) {
is AndroidKeystoreSecureArea -> SecureAreaImplementationState.Android
is BouncyCastleSecureArea -> SecureAreaImplementationState.BouncyCastle
is SoftwareSecureArea -> SecureAreaImplementationState.BouncyCastle
else -> throw IllegalStateException("Unknown Secure Area Implementation")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.android.identity.util.Logger
import androidx.preference.PreferenceManager
import com.android.mdl.appreader.settings.UserPreferences
import com.google.android.material.color.DynamicColors
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security

class VerifierApp : Application() {

Expand All @@ -17,6 +19,10 @@ class VerifierApp : Application() {
override fun onCreate() {
super.onCreate()
Logger.setLogPrinter(AndroidLogPrinter())
// This is needed to prefer BouncyCastle bundled with the app instead of the Conscrypt
// based implementation included in the OS itself.
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
DynamicColors.applyToActivitiesIfAvailable(this)
userPreferencesInstance = userPreferences
Logger.setDebugEnabled(userPreferences.isDebugLoggingEnabled())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.activity.OnBackPressedCallback
import androidx.annotation.AttrRes
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.android.identity.internal.Util
import com.android.identity.mdoc.response.DeviceResponseParser
import com.android.identity.securearea.SecureArea
import com.android.identity.securearea.SecureArea.EcCurve
Expand Down Expand Up @@ -262,6 +263,7 @@ class ShowDocumentFragment : Fragment() {
// Just show the SHA-1 of DeviceKey since all we're interested in here is whether
// we saw the same key earlier.
sb.append("<h6>DeviceKey</h6>")
sb.append("${getFormattedCheck(true)}Curve: <b>${curveNameFor(Util.getCurve(doc.deviceKey))}</b><br>")
val deviceKeySha1 = FormatUtil.encodeToString(
MessageDigest.getInstance("SHA-1").digest(doc.deviceKey.encoded)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ private CertificateGenerator() {

static X509Certificate generateCertificate(DataMaterial data, CertificateMaterial certMaterial, KeyMaterial keyMaterial)
throws CertIOException, CertificateException, OperatorCreationException {
Provider bcProvider = new BouncyCastleProvider();
Security.addProvider(bcProvider);

Optional<X509Certificate> issuerCert = keyMaterial.issuerCertificate();

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
biometrics = "1.2.0-alpha05"
cbor = "0.9"
exif = "1.3.6"
bouncy-castle = "1.67"
bouncy-castle = "1.70"
sonar-gradle-plugin = "3.4.0.2513"
jacoco = "0.47.0"
navigation = "2.6.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.identity.securearea

import co.nstant.`in`.cbor.CborBuilder
import co.nstant.`in`.cbor.CborDecoder
import co.nstant.`in`.cbor.CborEncoder
import co.nstant.`in`.cbor.CborException
import co.nstant.`in`.cbor.model.ByteString
import co.nstant.`in`.cbor.model.DataItem
import co.nstant.`in`.cbor.model.Map
import co.nstant.`in`.cbor.model.UnicodeString
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream

/**
* X.509 Extension which may be used by [SecureArea] implementations.
*
* The main purpose of the extension is to define a place to include the
* challenge passed at key creation time, for freshness.
*
* If used, the extension must be put in X.509 certificate for the created
* key (that is, included in the first certificate in the attestation for the key)
* at the OID defined by [ATTESTATION_OID] and the payload should be an
* OCTET STRING containing the bytes of the CBOR conforming to the following CDDL:
*
* ```
* Attestation = {
* "challenge" : bstr, ; contains the challenge
* }
* ```
*
* This map may be extended in the future with additional fields.
*/
object AttestationExtension {
/**
* The OID for the attestation extension.
*/
const val ATTESTATION_OID = "1.3.6.1.4.1.11129.2.1.39"

/**
* Generates the payload of the attestation extension.
*
* @param challenge the challenge to include
* @return the bytes of the CBOR for the extension.
*/
@JvmStatic
fun encode(challenge: ByteArray): ByteArray {
val baos = ByteArrayOutputStream()
try {
CborEncoder(baos).nonCanonical().encode(
CborBuilder()
.addMap()
.put("challenge", challenge)
.end()
.build().get(0)
)
} catch (e: CborException) {
throw IllegalStateException("Unexpected failure encoding data", e)
}
return baos.toByteArray()
}

/**
* Extracts the challenge from the attestation extension.
*
* @param attestationExtension the bytes of the CBOR for the extension.
* @return the challenge value.
*/
@JvmStatic
fun decode(attestationExtension: ByteArray): ByteArray {
val dataItems: List<DataItem> = try {
val bais = ByteArrayInputStream(attestationExtension)
CborDecoder(bais).decode()
} catch (e: CborException) {
throw IllegalArgumentException("Error decoding CBOR", e)
}
return ((dataItems[0] as Map).get(UnicodeString("challenge")) as ByteString).bytes
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public interface SecureArea {
/**
* Class with information about a key.
*
* <p>Concrete {@link SecureArea} implementations may subclass this to provide
* <p>Concrete {@link SecureArea} implementations may subclass this to provide additional
* implementation-specific information about the key.
*/
class KeyInfo {
Expand All @@ -247,7 +247,7 @@ protected KeyInfo(@NonNull List<X509Certificate> attestation,
}

/**
* Gets the attestation for a key.
* Gets the attestation for the key.
*
* @return A list of certificates representing a certificate chain.
*/
Expand Down
Loading

0 comments on commit b4e1051

Please sign in to comment.