Skip to content

Commit

Permalink
Support verification of non-keybound credentials.
Browse files Browse the repository at this point in the history
Most of this just loosens class casting and plumbs Credential or SdJwtCredential
through to functions that don't care about keybinding. For the couple of
functions that do care, this provides empty values and removes checks, allowing
non-keybound credentials to pass verification.

Tested by:
- Manual testing.
- ./gradlew check
- ./gradlew connectedCheck

Signed-off-by: Kevin Deus <[email protected]>
  • Loading branch information
kdeus committed Nov 19, 2024
1 parent 468e99c commit 57e2dd3
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,27 @@ class SdJwtVerifiableCredential(
}
}

fun createPresentation(secureArea: SecureArea,
alias: String,
fun createPresentation(secureArea: SecureArea?,
alias: String?,
keyUnlockData: KeyUnlockData?,
alg: Algorithm,
nonce: String,
audience: String,
creationTime: Instant = Clock.System.now()): SdJwtVerifiablePresentation {

val keyBindingHeaderStr = KeyBindingHeader(alg).toString()

val sdHash = Crypto.digest(this.sdHashAlg, toString().toByteArray()).toBase64Url()
val keyBindingBodyStr = KeyBindingBody(nonce, audience, creationTime, sdHash).toString()

val toBeSigned = "$keyBindingHeaderStr.$keyBindingBodyStr".toByteArray(Charsets.US_ASCII)
val signature = secureArea.sign(alias, alg, toBeSigned, keyUnlockData)
val signatureStr = signature.toCoseEncoded().toBase64Url()
val (keyBindingHeaderStr, keyBindingBodyStr, signatureStr) =
if (secureArea != null && alias != null) {
val keyBindingHeaderStr = KeyBindingHeader(alg).toString()
val sdHash = Crypto.digest(this.sdHashAlg, toString().toByteArray()).toBase64Url()
val keyBindingBodyStr = KeyBindingBody(nonce, audience, creationTime, sdHash).toString()

val toBeSigned = "$keyBindingHeaderStr.$keyBindingBodyStr".toByteArray(Charsets.US_ASCII)
val signature = secureArea.sign(alias, alg, toBeSigned, keyUnlockData)
val signatureStr = signature.toCoseEncoded().toBase64Url()
listOf(keyBindingHeaderStr, keyBindingBodyStr, signatureStr)
} else {
// Non-keybound credentials don't need to sign the key binding JWT.
listOf("", "", "")
}

return SdJwtVerifiablePresentation(
this,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import com.android.identity.crypto.EcPublicKey
import com.android.identity.crypto.EcSignature
import com.android.identity.sdjwt.SdJwtVerifiableCredential
import com.android.identity.sdjwt.vc.JwtBody
import com.android.identity.util.Logger
import com.android.identity.util.fromBase64Url
import com.android.identity.util.toBase64Url
import kotlinx.datetime.Instant

private const val TAG = "SdJwtVerifiablePresentation"

/**
* A presentation of a SD-JWT. It consists of:
* (1) an SD-JWT (which itself has the form
Expand Down Expand Up @@ -57,13 +60,25 @@ class SdJwtVerifiablePresentation(
*
* The caller MUST pass in three functions that will validate the nonce, audience, and
* creation time of the SD-JWT presentation.
*
* @return True if the presentation is key-bound and the binding is valid. False if it is
* not key-bound. Throws an exception if the binding is invalid.
*/
fun verifyKeyBinding(
checkNonce: (String) -> Boolean,
checkAudience: (String) -> Boolean,
checkCreationTime: (Instant) -> Boolean,
) {
val key = JwtBody.fromString(sdJwtVc.body).publicKey?.asEcPublicKey ?:
): Boolean {
val jwtBody = JwtBody.fromString(sdJwtVc.body)
if (jwtBody.publicKey == null) {
// Non-keybound credential. No verification needed.
Logger.i(TAG, "verifyKeyBinding found a body with no public key. Assuming a"
+ " non-keybound credential.")
// TODO: If we're expecting a keybound credential, this should be an error.
return false
}

val key = jwtBody.publicKey?.asEcPublicKey ?:
throw MalformedSdJwtPresentationError("couldn't parse public holder key from JWT: ${sdJwtVc.body}")
val keyBindingBodyObj = verifyHolderSignature(key)

Expand All @@ -78,6 +93,7 @@ class SdJwtVerifiablePresentation(
if (!checkCreationTime(keyBindingBodyObj.creationTime)) {
throw IllegalStateException("creation time didn't verify in key binding JWT: ${keyBindingBodyObj.toString()}")
}
return true
}

override fun toString() = "$sdJwtVc$keyBindingHeader.$keyBindingBody.$keyBindingSignature"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,7 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI=

// on the verifier, check that the key binding can be verified with the
// key mentioned in the SD-JWT:
presentation.verifyKeyBinding(
val isKeyBound = presentation.verifyKeyBinding(
checkAudience = { clientId == it },
checkNonce = { nonceStr == it },
checkCreationTime = { true /* TODO: sometimes flaky it < Clock.System.now() */ }
Expand Down Expand Up @@ -1155,6 +1155,9 @@ lrW+vvdmRHBgS+ss56uWyYor6W7ah9ygBwYFK4EEACI=
disclosedClaims.add(key)
Logger.i(TAG, "Adding special case $key: $value")
}
if (!isKeyBound) {
lines.add(OpenID4VPResultLine("Key bound", "false"))
}

val json = Json { ignoreUnknownKeys = true }
resp.outputStream.write(json.encodeToString(OpenID4VPResultData(lines)).encodeToByteArray())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.android.identity.appsupport.ui.consent.ConsentDocument
import com.android.identity.cbor.Cbor
import com.android.identity.cbor.CborArray
import com.android.identity.cbor.Simple
import com.android.identity.credential.Credential
import com.android.identity.credential.SecureAreaBoundCredential
import com.android.identity.crypto.Algorithm
import com.android.identity.crypto.Crypto
Expand All @@ -58,6 +59,7 @@ import com.android.identity.appsupport.ui.consent.ConsentField
import com.android.identity.appsupport.ui.consent.ConsentRelyingParty
import com.android.identity.appsupport.ui.consent.MdocConsentField
import com.android.identity.appsupport.ui.consent.VcConsentField
import com.android.identity.sdjwt.credential.SdJwtVcCredential
import com.android.identity_credential.wallet.ui.theme.IdentityCredentialTheme
// TODO: replace the nimbusds library usage with non-java-based alternative
import com.nimbusds.jose.EncryptionMethod
Expand Down Expand Up @@ -382,7 +384,7 @@ class OpenID4VPPresentationActivity : FragmentActivity() {
credentialFormat: CredentialFormat,
inputDescriptor: JsonObject,
now: Instant):
Pair<SecureAreaBoundCredential, List<ConsentField>> {
Pair<Credential, List<ConsentField>> {
return when(credentialFormat) {
CredentialFormat.MDOC_MSO -> {
val credential =
Expand All @@ -395,7 +397,7 @@ class OpenID4VPPresentationActivity : FragmentActivity() {
walletApp.documentTypeRepository,
credential as MdocCredential
)
return Pair(credential as SecureAreaBoundCredential, consentFields)
return Pair(credential, consentFields)
}
CredentialFormat.SD_JWT_VC -> {
val (vct, requestedClaims) = parseInputDescriptorForVc(inputDescriptor)
Expand All @@ -406,9 +408,9 @@ class OpenID4VPPresentationActivity : FragmentActivity() {
vct,
requestedClaims,
walletApp.documentTypeRepository,
credential as KeyBoundSdJwtVcCredential
credential as SdJwtVcCredential
)
return Pair(credential as SecureAreaBoundCredential, consentFields)
return Pair(credential, consentFields)
}
}
}
Expand Down Expand Up @@ -640,7 +642,7 @@ class OpenID4VPPresentationActivity : FragmentActivity() {
@OptIn(ExperimentalEncodingApi::class)
private suspend fun generateVpToken(
consentFields: List<ConsentField>,
credential: SecureAreaBoundCredential,
credential: Credential,
trustPoint: TrustPoint?,
authorizationRequest: AuthorizationRequest,
sessionTranscript: ByteArray // TODO: Only needed for mdoc. Generate internally.
Expand Down Expand Up @@ -670,7 +672,7 @@ class OpenID4VPPresentationActivity : FragmentActivity() {
val deviceResponseCbor = deviceResponseGenerator.generate()
deviceResponseCbor
}
is KeyBoundSdJwtVcCredential -> {
is SdJwtVcCredential -> {
showSdJwtPresentmentFlow(
activity = this,
consentFields = consentFields,
Expand Down Expand Up @@ -1028,7 +1030,7 @@ private fun VcConsentField.Companion.generateConsentFields(
vct: String,
claims: List<String>,
documentTypeRepository: DocumentTypeRepository,
vcCredential: KeyBoundSdJwtVcCredential?,
vcCredential: SdJwtVcCredential?,
): List<VcConsentField> {
val vcType = documentTypeRepository.getDocumentTypeForVc(vct)?.vcDocumentType
val ret = mutableListOf<VcConsentField>()
Expand All @@ -1047,7 +1049,7 @@ private fun VcConsentField.Companion.generateConsentFields(

private fun filterConsentFields(
list: List<VcConsentField>,
credential: KeyBoundSdJwtVcCredential?
credential: SdJwtVcCredential?
): List<VcConsentField> {
if (credential == null) {
return list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private suspend fun showPresentmentFlowImpl(
consentFields: List<ConsentField>,
document: ConsentDocument,
relyingParty: ConsentRelyingParty,
credential: SecureAreaBoundCredential,
credential: Credential,
signAndGenerate: (KeyUnlockData?) -> ByteArray
): ByteArray {
// always show the Consent Prompt first
Expand All @@ -72,11 +72,14 @@ private suspend fun showPresentmentFlowImpl(
// if KeyLockedException is raised show the corresponding Prompt to unlock
// the auth key for a Credential's Secure Area
catch (e: KeyLockedException) {
when (credential.secureArea) {
// The only way we should get a KeyLockedException is if this is a secure area bound
// credential.
val secureAreaBoundCredential = credential as SecureAreaBoundCredential
when (secureAreaBoundCredential.secureArea) {
// show Biometric prompt
is AndroidKeystoreSecureArea -> {
val unlockData =
AndroidKeystoreKeyUnlockData(credential.alias)
AndroidKeystoreKeyUnlockData(secureAreaBoundCredential.alias)
val cryptoObject =
unlockData.getCryptoObjectForSigning(Algorithm.ES256)

Expand Down Expand Up @@ -107,7 +110,7 @@ private suspend fun showPresentmentFlowImpl(
remainingPassphraseAttempts--

val softwareKeyInfo =
credential.secureArea.getKeyInfo(credential.alias) as SoftwareKeyInfo
secureAreaBoundCredential.secureArea.getKeyInfo(secureAreaBoundCredential.alias) as SoftwareKeyInfo
val constraints = softwareKeyInfo.passphraseConstraints!!
val title =
if (constraints.requireNumerical)
Expand Down Expand Up @@ -141,8 +144,8 @@ private suspend fun showPresentmentFlowImpl(
is CloudSecureArea -> {
if (keyUnlockData == null) {
keyUnlockData = CloudKeyUnlockData(
credential.secureArea as CloudSecureArea,
credential.alias,
secureAreaBoundCredential.secureArea as CloudSecureArea,
secureAreaBoundCredential.alias,
)
}

Expand All @@ -154,7 +157,7 @@ private suspend fun showPresentmentFlowImpl(
}
remainingPassphraseAttempts--

val constraints = (credential.secureArea as CloudSecureArea).passphraseConstraints
val constraints = (secureAreaBoundCredential.secureArea as CloudSecureArea).passphraseConstraints
val title =
if (constraints.requireNumerical)
activity.resources.getString(R.string.passphrase_prompt_csa_pin_title)
Expand Down Expand Up @@ -202,7 +205,7 @@ private suspend fun showPresentmentFlowImpl(

// for secure areas not yet implemented
else -> {
throw IllegalStateException("No prompts implemented for Secure Area ${credential.secureArea.displayName}")
throw IllegalStateException("No prompts implemented for Secure Area ${secureAreaBoundCredential.secureArea.displayName}")
}
}
}
Expand Down Expand Up @@ -233,7 +236,7 @@ suspend fun showSdJwtPresentmentFlow(
consentFields: List<ConsentField>,
document: ConsentDocument,
relyingParty: ConsentRelyingParty,
credential: SecureAreaBoundCredential,
credential: Credential,
nonce: String,
clientId: String,
): ByteArray {
Expand Down Expand Up @@ -261,9 +264,10 @@ suspend fun showSdJwtPresentmentFlow(
Logger.e(TAG, "No disclosures remaining.")
}

val secureAreaBoundCredential = credential as? SecureAreaBoundCredential
filteredSdJwt.createPresentation(
credential.secureArea,
credential.alias,
secureAreaBoundCredential?.secureArea,
secureAreaBoundCredential?.alias,
keyUnlockData,
Algorithm.ES256,
nonce!!,
Expand Down

0 comments on commit 57e2dd3

Please sign in to comment.