diff --git a/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/MainActivity.kt b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/MainActivity.kt index 305ce167..a2e1b859 100755 --- a/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/MainActivity.kt +++ b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/MainActivity.kt @@ -16,6 +16,7 @@ package com.yubico.yubikit.android.app +import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -42,6 +43,8 @@ import com.yubico.yubikit.core.Logger import java.util.* import kotlin.properties.Delegates +val clientCertificatesSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + class MainActivity : AppCompatActivity() { private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var navController: NavController @@ -75,11 +78,22 @@ class MainActivity : AppCompatActivity() { navController = findNavController(R.id.nav_host_fragment) // Passing each menu ID as a set of Ids because each // menu should be considered as top level destinations. - appBarConfiguration = AppBarConfiguration(setOf( - R.id.nav_management, R.id.nav_yubiotp, R.id.nav_piv, R.id.nav_oath), drawerLayout) + appBarConfiguration = AppBarConfiguration( + setOf( + R.id.nav_management, + R.id.nav_yubiotp, + R.id.nav_piv, + R.id.nav_oath, + R.id.nav_client_certs + ), drawerLayout + ) setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) + if (clientCertificatesSupported) { + navView.menu.findItem(R.id.nav_client_certs)?.isEnabled = true + } + yubikit = YubiKitManager(this) viewModel.handleYubiKey.observe(this) { @@ -127,12 +141,16 @@ class MainActivity : AppCompatActivity() { R.id.action_about -> { val binding = DialogAboutBinding.inflate(LayoutInflater.from(this)) AlertDialog.Builder(this) - .setView(binding.root) - .create().apply { - setOnShowListener { - binding.version.text = String.format(Locale.getDefault(), getString(R.string.version), BuildConfig.VERSION_NAME) - } - }.show() + .setView(binding.root) + .create().apply { + setOnShowListener { + binding.version.text = String.format( + Locale.getDefault(), + getString(R.string.version), + BuildConfig.VERSION_NAME + ) + } + }.show() } } return super.onOptionsItemSelected(item) diff --git a/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/ClientCertificatesFragment.kt b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/ClientCertificatesFragment.kt new file mode 100755 index 00000000..df8fd96a --- /dev/null +++ b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/ClientCertificatesFragment.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2022 Yubico. + * + * 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.yubico.yubikit.android.app.ui.client_certs + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.yubico.yubikit.android.YubiKitManager +import com.yubico.yubikit.android.app.MainViewModel +import com.yubico.yubikit.android.app.R +import com.yubico.yubikit.android.app.databinding.FragmentClientCertsBinding +import com.yubico.yubikit.android.transport.nfc.NfcConfiguration +import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable +import com.yubico.yubikit.android.transport.usb.UsbConfiguration +import com.yubico.yubikit.core.util.Result +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +@RequiresApi(21) +class ClientCertificatesFragment : Fragment() { + + val viewModel: ClientCertificatesViewModel by activityViewModels() + private val appViewModel: MainViewModel by activityViewModels() + private lateinit var yubikit: YubiKitManager + private lateinit var binding: FragmentClientCertsBinding + private lateinit var yubiKeyPrompt: AlertDialog + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + // this fragment has its own yubikey handler + // disable yubiKey listener in main activity + appViewModel.setYubiKeyListenerEnabled(false) + + binding = FragmentClientCertsBinding.inflate(inflater, container, false) + + // Handles YubiKey communication + yubikit = YubiKitManager(requireContext()) + + yubikit.startUsbDiscovery(UsbConfiguration()) { device -> + // usbYubiKey keeps a reference to the currently connected YubiKey over USB + viewModel.usbYubiKey.postValue(device) + device.setOnClosed { viewModel.usbYubiKey.postValue(null) } + + lifecycleScope.launch(Dispatchers.Main) { + viewModel.provideYubiKey(Result.success(device)) + // If we were asking the user to insert a YubiKey, close the dialog. + yubiKeyPrompt.dismiss() + } + } + + // Dialog used to prompt the user to insert/tap a YubiKey + yubiKeyPrompt = AlertDialog.Builder(requireContext()) + .setTitle(resources.getString(R.string.client_certs_dialog_title_insert_key)) + .setMessage(resources.getString(R.string.client_certs_dialog_msg_insert_key)) + .setOnCancelListener { + lifecycleScope.launch { + viewModel.provideYubiKey(Result.failure(CancellationException("Cancelled by user"))) + } + } + .create() + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.pendingYubiKeyAction.observe(viewLifecycleOwner) { action -> + if (action != null) { + lifecycleScope.launch(Dispatchers.Main) { + val usbYubiKey = viewModel.usbYubiKey.value + if (usbYubiKey != null) { + viewModel.provideYubiKey(Result.success(usbYubiKey)) + yubiKeyPrompt.dismiss() + } else { + val useNfc = viewModel.useNfc.value == true + yubiKeyPrompt.setTitle(action.message) + yubiKeyPrompt.setMessage(resources.getString(R.string.client_certs_dialog_msg_insert_key_now)) + yubiKeyPrompt.show() + if (useNfc) { + // Listen on NFC + startNfc() + } + } + } + } + } + + with(binding.webView) { + settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + } + + webViewClient = DemoWebViewClient(viewModel) + } + + binding.go.setOnClickListener { + WebView.clearClientCertPreferences { + viewModel.url.postValue(URL) + } + } + + binding.help.setOnClickListener { + WebView.clearClientCertPreferences { + viewModel.url.postValue("") + } + } + + viewModel.url.observe(viewLifecycleOwner) { + if (it.isEmpty()) { + binding.webView.visibility = View.INVISIBLE + } else { + binding.webView.visibility = View.VISIBLE + binding.webView.loadUrl(it) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + // enable yubiKey listener in main activity + appViewModel.setYubiKeyListenerEnabled(true) + } + + + private fun startNfc() { + try { + yubikit.startNfcDiscovery(NfcConfiguration(), requireActivity()) { nfcYubiKey -> + lifecycleScope.launch(Dispatchers.Main) { + yubiKeyPrompt.setMessage(resources.getString(R.string.client_certs_dialog_msg_nfc_hold)) + viewModel.provideYubiKey(Result.success(nfcYubiKey)) + yubiKeyPrompt.setMessage(resources.getString(R.string.client_certs_dialog_msg_nfc_remove)) + nfcYubiKey.remove { + lifecycleScope.launch(Dispatchers.Main) { + yubiKeyPrompt.dismiss() + } + } + } + } + } catch (e: NfcNotAvailable) { + viewModel.useNfc.value = false + } + } + + companion object { + private const val URL = "https://client.badssl.com" + } +} \ No newline at end of file diff --git a/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/ClientCertificatesViewModel.kt b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/ClientCertificatesViewModel.kt new file mode 100755 index 00000000..6749bd41 --- /dev/null +++ b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/ClientCertificatesViewModel.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2022 Yubico. + * + * 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.yubico.yubikit.android.app.ui.client_certs + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice +import com.yubico.yubikit.core.YubiKeyDevice +import com.yubico.yubikit.core.smartcard.SmartCardConnection +import com.yubico.yubikit.core.util.Result +import com.yubico.yubikit.piv.PivSession +import com.yubico.yubikit.piv.jca.PivProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.security.Security +import kotlin.coroutines.suspendCoroutine + +data class YubiKeyAction( + val message: String, val action: suspend (Result) -> Unit +) + + +class ClientCertificatesViewModel : ViewModel() { + + init { + // Needed for PIV private keys to work + Security.insertProviderAt(PivProvider { callback -> + _pendingYubiKeyAction.postValue(YubiKeyAction("PIV private key required") { result -> + try { + result.value.requestConnection(SmartCardConnection::class.java) { + callback.invoke(Result.of { + PivSession(it.value) + }) + } + } catch (e: Exception) { + callback.invoke(Result.failure(e)) + } + }) + }, 1) + } + + val url = MutableLiveData("") + + val useNfc = MutableLiveData(true) + val usbYubiKey = MutableLiveData() + + private val _pendingYubiKeyAction = MutableLiveData() + val pendingYubiKeyAction: LiveData = _pendingYubiKeyAction + + suspend fun provideYubiKey(result: Result) = + withContext(Dispatchers.IO) { + pendingYubiKeyAction.value?.let { + _pendingYubiKeyAction.postValue(null) + it.action.invoke(result) + } + } + + /** + * Requests a PIV session, and uses it to produce some result + */ + suspend fun usePiv(title: String, action: (PivSession) -> T) = suspendCoroutine { outer -> + _pendingYubiKeyAction.postValue(YubiKeyAction(title) { yubiKey -> + outer.resumeWith(runCatching { + suspendCoroutine { inner -> + yubiKey.value.requestConnection(SmartCardConnection::class.java) { + inner.resumeWith(runCatching { + action.invoke(PivSession(it.value)) + }) + } + } + }) + }) + } +} \ No newline at end of file diff --git a/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/DemoWebViewClient.kt b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/DemoWebViewClient.kt new file mode 100755 index 00000000..23cd1910 --- /dev/null +++ b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/DemoWebViewClient.kt @@ -0,0 +1,131 @@ +package com.yubico.yubikit.android.app.ui.client_certs + +import android.content.res.Resources +import android.graphics.Bitmap +import android.net.http.SslError +import android.util.Log +import android.webkit.ClientCertRequest +import android.webkit.SslErrorHandler +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.annotation.RequiresApi +import androidx.lifecycle.viewModelScope +import com.yubico.yubikit.android.app.R +import com.yubico.yubikit.android.app.ui.getSecret +import com.yubico.yubikit.piv.PinPolicy +import com.yubico.yubikit.piv.PivSession +import com.yubico.yubikit.piv.Slot +import com.yubico.yubikit.piv.jca.PivPrivateKey +import com.yubico.yubikit.piv.jca.PivProvider +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch +import java.security.KeyStore +import java.security.cert.X509Certificate + +@RequiresApi(21) +class DemoWebViewClient(private val viewModel: ClientCertificatesViewModel) : + WebViewClient() { + + companion object { + const val TAG = "WebViewClient" + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + Log.d(TAG, "Browsing to $url") + super.onPageStarted(view, url, favicon) + } + + override fun onPageFinished(view: WebView, url: String) { + Log.d(TAG, "Browsed to $url") + super.onPageFinished(view, url) + } + + private suspend fun getCertificates(resources: Resources) = + viewModel.usePiv(resources.getString(R.string.client_certs_dialog_title_read_certs)) { piv: PivSession -> + // We initialize a second PivProvider here to use with the KeyStore instance. + // Unlike the one in MainViewModel which is registered as a system Provider, + // this one will only be used synchronously with this instance of the PivSession. + val keyStore = KeyStore.getInstance("YKPiv", PivProvider(piv)) + keyStore.load(null) + listOf( + Slot.AUTHENTICATION, + Slot.SIGNATURE, + Slot.KEY_MANAGEMENT, + Slot.CARD_AUTH + ).mapNotNull { slot -> + // We avoid padding the PIN here since we're not sure we need it yet + when (val entry = keyStore.getEntry(slot.stringAlias, null)) { + // All entries returned here should be PrivateKeyEntry's, or null + is KeyStore.PrivateKeyEntry -> + Pair( + entry.privateKey as PivPrivateKey, + entry.certificate as X509Certificate + ) + else -> null + } + } + } + + /** + * Handles client certificate requests using PIV on a YubiKey + */ + override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) { + Log.d(TAG, "Client certificate request from ${request.host}") + viewModel.viewModelScope.launch { + try { + val certificates = getCertificates(view.resources) + // Select which certificate to use + val index = + selectItem( + view.context, + view.resources.getString(R.string.client_certs_dialog_select_cert), + certificates + ) { (key, certificate) -> "${key.slot.stringAlias}: ${certificate.issuerDN.name}" } + + val (privateKey, certificate) = certificates[index] + if (privateKey.pinPolicy != PinPolicy.NEVER) { + // Now that we know we might need the PIN, we ask the user for it + getSecret(view.context, R.string.client_certs_dialog_enter_pin)?.apply { + // ...and give it to the PrivateKey so that it can be used + privateKey.setPin(toCharArray()) + } ?: throw CancellationException() + } + // When the private key is used, it will again require a YubiKey connection + request.proceed(privateKey, arrayOf(certificate)) + } catch (e: Exception) { + Log.e(TAG, "Error getting client certificate auth", e) + request.cancel() + } + } + } + + /** + * Allows bypass of untrusted server certificates + * + * WARNING: For demonstration purposes only! + * Don't allow this in production! + */ + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + Log.d("YKBrowser", "Recoverable SSL error") + val message = when (error.primaryError) { + SslError.SSL_NOTYETVALID -> "The certificate is not yet valid" + SslError.SSL_EXPIRED -> "The certificate is expired" + SslError.SSL_IDMISMATCH -> "Hostname mismatch" + SslError.SSL_UNTRUSTED -> "The certificate authority is not trusted" + SslError.SSL_DATE_INVALID -> "The date of the certificate is invalid" + else -> "A generic error occurred" + } + viewModel.viewModelScope.launch { + if (confirmAction( + view.context, + "Connection may not be secure", + "The website ${error.url} has a certificate problem:\n\n${message}\n\nProceed anyway?" + ) + ) { + handler.proceed() + } else { + handler.cancel() + } + } + } +} \ No newline at end of file diff --git a/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/Dialogs.kt b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/Dialogs.kt new file mode 100755 index 00000000..62d77d35 --- /dev/null +++ b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/client_certs/Dialogs.kt @@ -0,0 +1,63 @@ +package com.yubico.yubikit.android.app.ui.client_certs + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import androidx.appcompat.app.AlertDialog +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +/** + * Asks the user to choose an item from a list. + */ +suspend fun selectItem( + context: Context, + title: String, + items: List, + label: (T) -> String +): Int = + withContext(Dispatchers.Main) { + suspendCoroutine { cont -> + AlertDialog.Builder(context) + .setTitle(title) + .setItems(items.map { label(it) }.toTypedArray()) { _, which -> + cont.resume(which) + } + .setOnCancelListener { + cont.resumeWithException(CancellationException()) + } + .create().apply { + listView.apply { + divider = ColorDrawable(Color.GRAY) + dividerHeight = 2 + } + }.show() + } + } + +/** + * Asks the user to confirm (or cancel) an action. + */ +suspend fun confirmAction(context: Context, title: String, message: String): Boolean = + withContext(Dispatchers.Main) { + suspendCoroutine { cont -> + AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(android.R.string.ok) { _, _ -> + cont.resume(true) + } + .setNeutralButton(android.R.string.cancel) { dialog, _ -> + dialog.cancel() + } + .setOnCancelListener { + cont.resume(false) + } + .create() + .show() + } + } \ No newline at end of file diff --git a/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/piv/PivViewModel.kt b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/piv/PivViewModel.kt index 58ca3af2..c0a9eac5 100755 --- a/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/piv/PivViewModel.kt +++ b/AndroidDemo/src/main/java/com/yubico/yubikit/android/app/ui/piv/PivViewModel.kt @@ -30,7 +30,7 @@ import com.yubico.yubikit.piv.PivSession import com.yubico.yubikit.piv.Slot import java.security.cert.X509Certificate -class PivViewModel : YubiKeyViewModel() { +open class PivViewModel : YubiKeyViewModel() { /** * List of slots that we will show on demo UI */ diff --git a/AndroidDemo/src/main/res/drawable/ic_baseline_client_certs_24.xml b/AndroidDemo/src/main/res/drawable/ic_baseline_client_certs_24.xml new file mode 100644 index 00000000..e920e2ec --- /dev/null +++ b/AndroidDemo/src/main/res/drawable/ic_baseline_client_certs_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/AndroidDemo/src/main/res/drawable/ic_baseline_go_24.xml b/AndroidDemo/src/main/res/drawable/ic_baseline_go_24.xml new file mode 100644 index 00000000..593d1b93 --- /dev/null +++ b/AndroidDemo/src/main/res/drawable/ic_baseline_go_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/AndroidDemo/src/main/res/drawable/ic_baseline_help_24.xml b/AndroidDemo/src/main/res/drawable/ic_baseline_help_24.xml new file mode 100644 index 00000000..d9824ce7 --- /dev/null +++ b/AndroidDemo/src/main/res/drawable/ic_baseline_help_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/AndroidDemo/src/main/res/drawable/ic_baseline_home_24.xml b/AndroidDemo/src/main/res/drawable/ic_baseline_home_24.xml new file mode 100644 index 00000000..537e6cee --- /dev/null +++ b/AndroidDemo/src/main/res/drawable/ic_baseline_home_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/AndroidDemo/src/main/res/drawable/ic_baseline_refresh_24.xml b/AndroidDemo/src/main/res/drawable/ic_baseline_refresh_24.xml new file mode 100644 index 00000000..425f39d2 --- /dev/null +++ b/AndroidDemo/src/main/res/drawable/ic_baseline_refresh_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/AndroidDemo/src/main/res/drawable/ic_refresh_24dp.xml b/AndroidDemo/src/main/res/drawable/ic_refresh_24dp.xml deleted file mode 100644 index 9cc95c4c..00000000 --- a/AndroidDemo/src/main/res/drawable/ic_refresh_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/AndroidDemo/src/main/res/layout/fragment_client_certs.xml b/AndroidDemo/src/main/res/layout/fragment_client_certs.xml new file mode 100755 index 00000000..d7eaa0aa --- /dev/null +++ b/AndroidDemo/src/main/res/layout/fragment_client_certs.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidDemo/src/main/res/layout/fragment_oath.xml b/AndroidDemo/src/main/res/layout/fragment_oath.xml index 61136a98..1b764c33 100755 --- a/AndroidDemo/src/main/res/layout/fragment_oath.xml +++ b/AndroidDemo/src/main/res/layout/fragment_oath.xml @@ -57,7 +57,7 @@ android:layout_height="wrap_content" android:hint="@string/oath_key" app:endIconContentDescription="@string/randomize" - app:endIconDrawable="@drawable/ic_refresh_24dp" + app:endIconDrawable="@drawable/ic_baseline_refresh_24" app:endIconMode="custom"> - - - - - - - - - - - - - - - diff --git a/AndroidDemo/src/main/res/layout/fragment_yubiotp_chalresp.xml b/AndroidDemo/src/main/res/layout/fragment_yubiotp_chalresp.xml index 07dace3b..cbd6bdaa 100755 --- a/AndroidDemo/src/main/res/layout/fragment_yubiotp_chalresp.xml +++ b/AndroidDemo/src/main/res/layout/fragment_yubiotp_chalresp.xml @@ -27,7 +27,7 @@ android:layout_height="wrap_content" android:hint="@string/otp_hmac_key" app:endIconContentDescription="@string/randomize" - app:endIconDrawable="@drawable/ic_refresh_24dp" + app:endIconDrawable="@drawable/ic_baseline_refresh_24" app:endIconMode="custom"> + \ No newline at end of file diff --git a/AndroidDemo/src/main/res/navigation/mobile_navigation.xml b/AndroidDemo/src/main/res/navigation/mobile_navigation.xml index 64ae2176..83307a6a 100755 --- a/AndroidDemo/src/main/res/navigation/mobile_navigation.xml +++ b/AndroidDemo/src/main/res/navigation/mobile_navigation.xml @@ -27,4 +27,10 @@ android:name="com.yubico.yubikit.android.app.ui.oath.OathFragment" android:label="@string/menu_oath" tools:layout="@layout/fragment_oath" /> + + \ No newline at end of file diff --git a/AndroidDemo/src/main/res/values/strings.xml b/AndroidDemo/src/main/res/values/strings.xml index c1a0ae2a..9252cf66 100755 --- a/AndroidDemo/src/main/res/values/strings.xml +++ b/AndroidDemo/src/main/res/values/strings.xml @@ -10,6 +10,7 @@ PIV WebView OATH + Client certificates Plug in you YubiKey or tap over NFC reader Hold your key flat until operation is completed @@ -78,6 +79,38 @@ Credential issuer Add new credential + Client SSL certificate demo + Prepare YubiKey + Test client certificate + + This screen shows how to use PIV JCA implementation of YubiKey to connect to sites which require client SSL certificate, this example uses client.badssl.com. + 1. Download cert from https://badssl.com/download/ +\n2. Import it into a YubiKey PIV slot + click to go to test site + \u2022 test site will start loading +\n\u2022 the app will ask for YubiKey +\n\u2022 tap the NFC key or connect it via USB +\n\u2022 list of PIV certs from the YubiKey is presented +\n\u2022 select correct certificate +\n\u2022 if asked for PIN, provide it +\n\u2022 if asked for YubiKey, provide it +\n\u2022 test site loads successfully + + show this help + + https://client.badssl.com + Screen help + Go to test site + + Insert YubiKey + Insert or tap your YubiKey + Insert or tap your YubiKey now + Hold your YubiKey still + Remove your YubiKey + Enter PIV PIN + Select a client certificate + Read PIV certificates + About YubiKit is a library provided by Yubico for interacting with YubiKeys. This demo app showcases some of the capabilities of the library.