Skip to content

Commit 6708543

Browse files
committed
Add support for passphrases.
Intodouce PassphraseConstraints class which can be used to specify details about a passphrase, for example whether all characters are numerical and the min/max length. This is helpful when e.g. the passphrase is always a six-digit PIN because it allows the application to present an UI/UX optimized towards that. Add support for this in SoftwareSecureArea. Introduce applyConfiguration() method on builders for classes derived from CreateKeystoreSettings to allow the issuer to specify configuration of how to create a key. Add a requireUserAuthenticationToViewDocument setting to DocumentConfiguration so the issuer can specify if they want the user to authenticate in order to view document data. Enforce it in the wallet app. For now just require LSKF/biometric authentication, in the future we can use other methods as well. Add new evidence type for having the user create a passphrase/PIN, using PassphraseConstraints. In the wallet app, rename some types to properly reflect the reality of the recent Credential->Document and AuthenticationKey->Credential rename. When in developer mode, add a number of extra screens to the provisioning of an mDL including which Secure Area to use (Android Keystore or Software), whether to use StrongBox (if using Android Keystore), the passphrase/PIN (if using Software), and mdoc authentication mode and curve. Other minor fixes/changes - rename dataItem to toDataItem on CertificateChain, for consistency - use mutableState for evidenceRequest - fix focus bug in MultipleChoice evidence request Test: Manually tested and all unit tests pass. Signed-off-by: David Zeuthen <zeuthen@google.com>
1 parent 8b32779 commit 6708543

File tree

66 files changed

+1702
-721
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1702
-721
lines changed

appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ class ProvisioningUtil private constructor(
203203
unprotectedHeaders = mapOf(
204204
Pair(
205205
CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN),
206-
CertificateChain(listOf(Certificate(issuerCert.encoded))).dataItem
206+
CertificateChain(listOf(Certificate(issuerCert.encoded))).toDataItem
207207
)
208208
),
209209
).toDataItem

identity-android/src/androidTest/java/com/android/identity/android/mdoc/deviceretrieval/DeviceRetrievalHelperTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ class DeviceRetrievalHelperTest {
195195
)
196196
val unprotectedHeaders = java.util.Map.of<CoseLabel, DataItem>(
197197
CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN),
198-
CertificateChain(java.util.List.of(mDocumentSignerCert)).dataItem
198+
CertificateChain(java.util.List.of(mDocumentSignerCert)).toDataItem
199199
)
200200
val encodedIssuerAuth = encode(
201201
coseSign1Sign(

identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreCreateKeySettings.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.android.identity.android.securearea
22

33
import android.os.Build
4+
import com.android.identity.cbor.DataItem
45
import com.android.identity.crypto.EcCurve
56
import com.android.identity.securearea.CreateKeySettings
67
import com.android.identity.securearea.KeyPurpose
@@ -70,6 +71,33 @@ class AndroidKeystoreCreateKeySettings private constructor(
7071
private var validFrom: Timestamp? = null
7172
private var validUntil: Timestamp? = null
7273

74+
/**
75+
* Apply settings from configuration object.
76+
*
77+
* @param configuration configuration from a CBOR map.
78+
* @return the builder.
79+
*/
80+
fun applyConfiguration(configuration: DataItem) = apply {
81+
var userAutenticationRequired = false
82+
var userAuthenticationTimeoutMillis = 0L
83+
var userAuthenticationTypes = setOf<UserAuthenticationType>()
84+
for ((key, value) in configuration.asMap) {
85+
when (key.asTstr) {
86+
"purposes" -> setKeyPurposes(KeyPurpose.decodeSet(value.asNumber))
87+
"curve" -> setEcCurve(EcCurve.fromInt(value.asNumber.toInt()))
88+
"useStrongBox" -> setUseStrongBox(value.asBoolean)
89+
"userAuthenticationRequired" -> userAutenticationRequired = value.asBoolean
90+
"userAuthenticationTimeoutMillis" -> userAuthenticationTimeoutMillis = value.asNumber
91+
"userAuthenticationTypes" -> userAuthenticationTypes = UserAuthenticationType.decodeSet(value.asNumber)
92+
}
93+
}
94+
setUserAuthenticationRequired(
95+
userAutenticationRequired,
96+
userAuthenticationTimeoutMillis,
97+
userAuthenticationTypes
98+
)
99+
}
100+
73101
/**
74102
* Sets the key purpose.
75103
*

identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreSecureArea.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.android.identity.crypto.CertificateChain
3333
import com.android.identity.crypto.EcCurve
3434
import com.android.identity.crypto.EcPublicKey
3535
import com.android.identity.crypto.javaPublicKey
36+
import com.android.identity.securearea.CreateKeySettings
3637
import com.android.identity.securearea.KeyInvalidatedException
3738
import com.android.identity.securearea.KeyLockedException
3839
import com.android.identity.securearea.KeyPurpose
@@ -574,7 +575,7 @@ class AndroidKeystoreSecureArea(
574575
map.put("userAuthenticationRequired", settings.userAuthenticationRequired)
575576
map.put("userAuthenticationTimeoutMillis", settings.userAuthenticationTimeoutMillis)
576577
map.put("useStrongBox", settings.useStrongBox)
577-
map.put("attestation", attestation.dataItem)
578+
map.put("attestation", attestation.toDataItem)
578579
storageEngine.put(PREFIX + alias, Cbor.encode(map.end().build()))
579580
}
580581

identity-mdoc/src/main/java/com/android/identity/mdoc/request/DeviceRequestGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class DeviceRequestGenerator(
121121
val unprotectedHeaders = mapOf<CoseLabel, DataItem>(
122122
Pair(
123123
CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN),
124-
readerKeyCertificateChain.dataItem
124+
readerKeyCertificateChain.toDataItem
125125
)
126126
)
127127
readerAuth = coseSign1Sign(

identity-mdoc/src/test/java/com/android/identity/mdoc/response/DeviceResponseGeneratorTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ class DeviceResponseGeneratorTest {
177177
)
178178
val unprotectedHeaders = java.util.Map.of<CoseLabel, DataItem>(
179179
CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN),
180-
CertificateChain(java.util.List.of(documentSignerCert)).dataItem
180+
CertificateChain(java.util.List.of(documentSignerCert)).toDataItem
181181
)
182182
val encodedIssuerAuth = Cbor.encode(
183183
Cose.coseSign1Sign(

identity/src/main/java/com/android/identity/crypto/CertificateChain.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ data class CertificateChain(
2222
*
2323
* Use [fromDataItem] to decode the returned data item.
2424
*/
25-
val dataItem: DataItem
25+
val toDataItem: DataItem
2626
get() = if (certificates.size == 1) {
2727
certificates[0].toDataItem
2828
} else {

identity/src/main/java/com/android/identity/document/DocumentUtil.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ object DocumentUtil {
4343
* from the issuer.
4444
*
4545
* @param document the document to manage credentials for.
46-
* @param secureArea the secure area to use for new credentials.
46+
* @param secureArea the secure area to use for new credentials, must not be null if `dryRun` is false.
4747
* @param createKeySettings the settings used to create new credentials.
4848
* @param domain the domain to use for created credentials.
4949
* @param now the time right now, used for determining which existing credentials to replace.
@@ -56,7 +56,7 @@ object DocumentUtil {
5656
@JvmStatic
5757
fun managedCredentialHelper(
5858
document: Document,
59-
secureArea: SecureArea,
59+
secureArea: SecureArea?,
6060
createKeySettings: CreateKeySettings?,
6161
domain: String,
6262
now: Timestamp,
@@ -65,6 +65,7 @@ object DocumentUtil {
6565
minValidTimeMillis: Long,
6666
dryRun: Boolean
6767
): Int {
68+
check(dryRun || (secureArea != null && createKeySettings != null))
6869
// First determine which of the existing credentials need a replacement...
6970
var numCredentialsNotNeedingReplacement = 0
7071
var numReplacementsGenerated = 0
@@ -85,7 +86,7 @@ object DocumentUtil {
8586
if (!dryRun) {
8687
document.createCredential(
8788
domain,
88-
secureArea,
89+
secureArea!!,
8990
createKeySettings!!,
9091
authCredential
9192
)
@@ -112,7 +113,7 @@ object DocumentUtil {
112113
for (n in 0 until numNonReplacementsToGenerate) {
113114
val pendingCredential = document.createCredential(
114115
domain,
115-
secureArea,
116+
secureArea!!,
116117
createKeySettings!!,
117118
null
118119
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.android.identity.securearea
2+
3+
import com.android.identity.cbor.annotation.CborSerializable
4+
5+
/**
6+
* Enumeration used to convey constraints on passphrases and PINs.
7+
*
8+
* This information is helpful for user interfaces when creating and validating passphrases.
9+
*
10+
* @param minLength the minimum allowed length of the passphrase.
11+
* @param maxLength the maximum allowed length of the passphrase.
12+
* @param requireNumerical if `true`, each character in the passphrase must be decimal digits (0-9).
13+
*/
14+
@CborSerializable
15+
data class PassphraseConstraints(
16+
val minLength: Int,
17+
val maxLength: Int,
18+
val requireNumerical: Boolean
19+
) {
20+
companion object {
21+
val NONE = PassphraseConstraints(0, Int.MAX_VALUE, false)
22+
23+
val PIN_FOUR_DIGITS = PassphraseConstraints(4, 4, true)
24+
val PIN_FOUR_DIGITS_OR_LONGER = PassphraseConstraints(4, Int.MAX_VALUE, true)
25+
val PASSPHRASE_FOUR_CHARS = PassphraseConstraints(4, 4, false)
26+
val PASSPHRASE_FOUR_CHARS_OR_LONGER = PassphraseConstraints(4, Int.MAX_VALUE, true)
27+
28+
val PIN_SIX_DIGITS = PassphraseConstraints(6, 6, true)
29+
val PIN_SIX_DIGITS_OR_LONGER = PassphraseConstraints(6, Int.MAX_VALUE, true)
30+
val PASSPHRASE_SIX_CHARS = PassphraseConstraints(6, 6, false)
31+
val PASSPHRASE_SIX_CHARS_OR_LONGER = PassphraseConstraints(6, Int.MAX_VALUE, true)
32+
}
33+
}

identity/src/main/java/com/android/identity/securearea/software/SoftwareCreateKeySettings.kt

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package com.android.identity.securearea.software
22

3+
import com.android.identity.cbor.DataItem
34
import com.android.identity.crypto.Algorithm
45
import com.android.identity.crypto.CertificateChain
56
import com.android.identity.crypto.EcCurve
67
import com.android.identity.crypto.EcPrivateKey
8+
import com.android.identity.securearea.CreateKeySettings
79
import com.android.identity.securearea.KeyPurpose
10+
import com.android.identity.securearea.PassphraseConstraints
11+
import com.android.identity.securearea.fromDataItem
812
import com.android.identity.util.Timestamp
913

1014
/**
1115
* Class used to indicate key creation settings for software-backed keys.
1216
*/
13-
class SoftwareCreateKeySettings private constructor(
17+
class SoftwareCreateKeySettings internal constructor(
1418
val passphraseRequired: Boolean,
15-
val passphrase: String,
19+
val passphrase: String?,
20+
val passphraseConstraints: PassphraseConstraints?,
1621
ecCurve: EcCurve,
1722
keyPurposes: Set<KeyPurpose>,
1823
attestationChallenge: ByteArray,
@@ -23,7 +28,7 @@ class SoftwareCreateKeySettings private constructor(
2328
val attestationKeySignatureAlgorithm: Algorithm?,
2429
val attestationKeyIssuer: String?,
2530
val attestationKeyCertification: CertificateChain?,
26-
) : com.android.identity.securearea.CreateKeySettings(
31+
) : CreateKeySettings(
2732
attestationChallenge,
2833
keyPurposes,
2934
ecCurve
@@ -34,20 +39,47 @@ class SoftwareCreateKeySettings private constructor(
3439
* @param attestationChallenge challenge to include in attestation for the key.
3540
*/
3641
class Builder(
37-
private val attestationChallenge: ByteArray,
38-
private var keyPurposes: Set<KeyPurpose> = setOf(KeyPurpose.SIGN),
39-
private var ecCurve: EcCurve = EcCurve.P256,
40-
private var passphraseRequired: Boolean = false,
41-
private var passphrase: String? = "",
42-
private var subject: String? = null,
43-
private var validFrom: Timestamp? = null,
44-
private var validUntil: Timestamp? = null,
45-
private var attestationKey: EcPrivateKey? = null,
46-
private var attestationKeySignatureAlgorithm: Algorithm? = null,
47-
private var attestationKeyIssuer: String? = null,
48-
private var attestationKeyCertification: CertificateChain? = null
42+
private val attestationChallenge: ByteArray
4943
) {
50-
constructor(challenge: ByteArray) : this(challenge, setOf(KeyPurpose.SIGN))
44+
private var keyPurposes: Set<KeyPurpose> = setOf(KeyPurpose.SIGN)
45+
private var ecCurve: EcCurve = EcCurve.P256
46+
private var passphraseRequired: Boolean = false
47+
private var passphrase: String? = null
48+
private var passphraseConstraints: PassphraseConstraints? = null
49+
private var subject: String? = null
50+
private var validFrom: Timestamp? = null
51+
private var validUntil: Timestamp? = null
52+
private var attestationKey: EcPrivateKey? = null
53+
private var attestationKeySignatureAlgorithm: Algorithm? = null
54+
private var attestationKeyIssuer: String? = null
55+
private var attestationKeyCertification: CertificateChain? = null
56+
57+
/**
58+
* Apply settings from configuration object.
59+
*
60+
* @param configuration configuration from a CBOR map.
61+
* @return the builder.
62+
*/
63+
fun applyConfiguration(configuration: DataItem) = apply {
64+
var passphraseRequired = false
65+
var passphrase: String? = null
66+
var passphraseConstraints: PassphraseConstraints? = null
67+
for ((key, value) in configuration.asMap) {
68+
when (key.asTstr) {
69+
"purposes" -> setKeyPurposes(KeyPurpose.decodeSet(value.asNumber))
70+
"curve" -> setEcCurve(EcCurve.fromInt(value.asNumber.toInt()))
71+
"passphrase" -> {
72+
passphraseRequired = true
73+
passphrase = value.asTstr
74+
}
75+
"passphraseConstraints" -> {
76+
passphraseRequired = true
77+
passphraseConstraints = PassphraseConstraints.fromDataItem(value)
78+
}
79+
}
80+
}
81+
setPassphraseRequired(passphraseRequired, passphrase, passphraseConstraints)
82+
}
5183

5284
/**
5385
* Sets the attestation key to use for attesting to the key.
@@ -102,12 +134,20 @@ class SoftwareCreateKeySettings private constructor(
102134
*
103135
* @param required whether a passphrase is required.
104136
* @param passphrase the passphrase to use, must not be `null` if `required` is `true`.
137+
* @param constraints constraints for the passphrase or `null` if not constrained.
105138
* @return the builder.
106139
*/
107-
fun setPassphraseRequired(required: Boolean, passphrase: String?) = apply {
108-
check(!(passphraseRequired && passphrase == null)) { "Passphrase cannot be null if it's required" }
140+
fun setPassphraseRequired(
141+
required: Boolean,
142+
passphrase: String?,
143+
constraints: PassphraseConstraints?
144+
) = apply {
145+
check(!passphraseRequired || passphrase != null) {
146+
"Passphrase cannot be null if passphrase is required"
147+
}
109148
passphraseRequired = required
110149
this.passphrase = passphrase
150+
this.passphraseConstraints = constraints
111151
}
112152

113153
/**
@@ -141,7 +181,7 @@ class SoftwareCreateKeySettings private constructor(
141181
*/
142182
fun build(): SoftwareCreateKeySettings =
143183
SoftwareCreateKeySettings(
144-
passphraseRequired, passphrase!!, ecCurve,
184+
passphraseRequired, passphrase, passphraseConstraints, ecCurve,
145185
keyPurposes, attestationChallenge,
146186
subject, validFrom, validUntil,
147187
attestationKey, attestationKeySignatureAlgorithm,

identity/src/main/java/com/android/identity/securearea/software/SoftwareKeyInfo.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ import com.android.identity.crypto.CertificateChain
44
import com.android.identity.crypto.EcPublicKey
55
import com.android.identity.securearea.KeyInfo
66
import com.android.identity.securearea.KeyPurpose
7+
import com.android.identity.securearea.PassphraseConstraints
78

89
/**
910
* Specialization of [KeyInfo] specific to software-backed keys.
1011
*
1112
* @param isPassphraseProtected whether the key is passphrase protected.
13+
* @param passphraseConstraints constraints on the passphrase, if any.
1214
*/
1315
class SoftwareKeyInfo internal constructor(
1416
publicKey: EcPublicKey,
1517
attestation: CertificateChain,
1618
keyPurposes: Set<KeyPurpose>,
17-
val isPassphraseProtected: Boolean
19+
val isPassphraseProtected: Boolean,
20+
val passphraseConstraints: PassphraseConstraints?
1821
): KeyInfo(
1922
publicKey,
2023
attestation,

identity/src/main/java/com/android/identity/securearea/software/SoftwareSecureArea.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ import com.android.identity.securearea.KeyLockedException
2828
import com.android.identity.securearea.KeyPurpose
2929
import com.android.identity.securearea.KeyPurpose.Companion.encodeSet
3030
import com.android.identity.securearea.KeyUnlockData
31+
import com.android.identity.securearea.PassphraseConstraints
3132
import com.android.identity.securearea.SecureArea
33+
import com.android.identity.securearea.fromDataItem
3234
import com.android.identity.securearea.keyPurposeSet
35+
import com.android.identity.securearea.toDataItem
3336
import com.android.identity.storage.StorageEngine
3437
import kotlinx.datetime.Clock
3538
import kotlinx.datetime.Instant
@@ -82,7 +85,7 @@ class SoftwareSecureArea(private val storageEngine: StorageEngine) : SecureArea
8285
val encodedPublicKey = Cbor.encode(privateKey.publicKey.toCoseKey().toDataItem)
8386
val secretKey = derivePrivateKeyEncryptionKey(
8487
encodedPublicKey,
85-
settings.passphrase
88+
settings.passphrase!!
8689
)
8790
val cleartextPrivateKey = Cbor.encode(privateKey.toCoseKey().toDataItem)
8891
val iv = Random.Default.nextBytes(12)
@@ -150,7 +153,10 @@ class SoftwareSecureArea(private val storageEngine: StorageEngine) : SecureArea
150153
}
151154
}
152155
mapBuilder.put("publicKey", privateKey.publicKey.toCoseKey().toDataItem)
153-
val attestationBuilder = mapBuilder.put("attestation", CertificateChain(certs).dataItem)
156+
if (settings.passphraseConstraints != null) {
157+
mapBuilder.put("passphraseConstraints", settings.passphraseConstraints.toDataItem)
158+
}
159+
val attestationBuilder = mapBuilder.put("attestation", CertificateChain(certs).toDataItem)
154160
attestationBuilder.end()
155161
storageEngine.put(PREFIX + alias, Cbor.encode(mapBuilder.end().build()))
156162
} catch (e: Exception) {
@@ -259,7 +265,16 @@ class SoftwareSecureArea(private val storageEngine: StorageEngine) : SecureArea
259265
val passphraseRequired = map["passphraseRequired"].asBoolean
260266
val publicKey = map["publicKey"].asCoseKey.ecPublicKey
261267
val attestation = map["attestation"].asCertificateChain
262-
return SoftwareKeyInfo(publicKey, attestation, keyPurposes, passphraseRequired)
268+
val passphraseConstraints = map.getOrNull("passphraseConstraints")?.let {
269+
PassphraseConstraints.fromDataItem(it)
270+
}
271+
return SoftwareKeyInfo(
272+
publicKey,
273+
attestation,
274+
keyPurposes,
275+
passphraseRequired,
276+
passphraseConstraints
277+
)
263278
}
264279

265280
override fun getKeyInvalidated(alias: String): Boolean {

identity/src/main/java/com/android/identity/util/Timestamp.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package com.android.identity.util
1717

18+
import com.android.identity.cbor.DataItem
19+
import com.android.identity.cbor.toDataItem
20+
1821
/**
1922
* Represents a single instant in time. Ideally, we'd use `java.time.Instant`, but we cannot
2023
* do so until we move to API level 26.

0 commit comments

Comments
 (0)