SSH

Keypad provided by the Hardware Security SDK to access the smartcard over NFC.

Animation showing how the smartcard should be held against the smartphone for SSH authentication.

In this guide, you’ll learn how to integrate the Hardware Security SDK in your app to implement SSH authentication with smartcards. The Hardware Security SDK will automatically‚Ķ

  1. retrieve the publickey from the smartcard and use it for the SSH connection.
    (If an OpenSSH certificate has been stored on the smartcard, this will be used instead.)
  2. cryptographically sign the SSH challenge using the smartcard.
Fork sample code on Github: Get Sample on Github

Add the SDK to Your Project

To get a username and password for our Maven repository, please contact us for a license.

Add this to your build.gradle:

repositories {
    google()
    jcenter()
    maven {
        credentials {
            username 'xxx'
            password 'xxx'
        }
        url "https://maven.cotech.de"
    }
}

dependencies {
    // Smartcard OpenPGP protocol
    implementation 'de.cotech:hwsecurity-openpgp:2.2.1'
    // Smartcard dialog with keypad
    implementation 'de.cotech:hwsecurity-smartcard-ui:2.2.1'
    
    // SSH library, here we are using Jsch
    implementation 'com.jcraft:jsch:0.1.55'
    
    // NOTE: The full list of dependencies can be found in our Github sample
}

Initialize the Hardware Security SDK

To use the SDK’s functionality in your app, you need to initialize the SecurityKeyManager first. This is the central class of the SDK, which dispatches incoming NFC and USB connections. It is recommended to perform this initialization in the onCreate method of your Application subclass. This ensures Security Keys are reliably dispatched by your app while in the foreground.

override fun onCreate() {
    super.onCreate()

    val securityKeyManager = SecurityKeyManager.getInstance()
    val config = SecurityKeyManagerConfig.Builder()
        .setEnableDebugLogging(BuildConfig.DEBUG)
        .build()

    securityKeyManager.init(this, config)
}
@Override
public void onCreate() {
    super.onCreate();

    SecurityKeyManager securityKeyManager = SecurityKeyManager.getInstance();
    SecurityKeyManagerConfig config = new SecurityKeyManagerConfig.Builder()
        .setEnableDebugLogging(BuildConfig.DEBUG)
        .build();

    securityKeyManager.init(this, config);
}

Don’t forget to register your Application subclass in your AndroidManifest.xml:

<application 
   android:name=".SecurityKeyApplication"
   android:label="@string/app_name" 
...>

Show Smartcard Dialog and Receive Callbacks

In this guide, we use the SmartcardDialogFragment to show a neat kepad for PIN input. It also handles smartcard errors, for example when a wrong PIN is entered.

private fun showSmartcardDialog() {
    val options = SmartcardDialogOptions.builder()
        //.setPinLength(4) // smartcards with a fixed PIN and PUK length improve the UX
        //.setPukLength(8)
        .setShowReset(true) // show button to reset/unblock of PIN using the PUK
        .build()

    smartcardDialogFragment = SmartcardDialogFactory.newOpenPgpInstance(options)
    smartcardDialogFragment.show(supportFragmentManager)
}

Implement SmartcardDialogCallback<OpenPgpSecurityKey> in your Activity and override onSmartcardDialogSecurityKeyDiscovered to receive callbacks from the SmartcardDialogFragment when a smartcard is discovered over NFC (or Security Keys over USB):

@UiThread
override fun onSmartcardDialogSecurityKeyDiscovered(securityKey: OpenPgpSecurityKey, pinProvider: PinProvider) {
    val loginName = "cotech"
    val loginHost = "ssh.hwsecurity.dev"

    connectToSsh(loginName, loginHost, securityKey, pinProvider)
}

IOExceptions thrown inside onSmartcardDialogSecurityKeyDiscovered are catched by the SmartcardDialogFragment and a proper error UI is shown to the user.

Threading and Exception Handling

The actual SSH connection is deferred to a new thread so that network operations are not blocking the main thread. To properly handle Exceptions, deferred.await() is used. IOExceptions are thrown again to be catched by SmartcardDialogFragment.

private fun connectToSsh(
    loginName: String, loginHost: String, securityKey: OpenPgpSecurityKey, pinProvider: PinProvider
) = runBlocking {
    val deferred = GlobalScope.async(Dispatchers.IO) {
        val securityKeyAuthenticator = securityKey.createSshAuthenticatorFromPublicKey(pinProvider)
        // NOTE: If you are using OpenSSH certificates:
        //val securityKeyAuthenticator = securityKey.createSshAuthenticatorFromCertificate(pinProvider)
        val smartcardIdentity = SmartcardJschIdentity(loginName, securityKeyAuthenticator)
        
        jschConnection(loginHost, smartcardIdentity)
    }

    try {
        deferred.await()
    } catch (e: JSchException) {
        Timber.e(e)
        // unwrap IOExceptions thrown in SshIdentity and throw them again to be catched in SmartcardDialogFragment
        e.cause?.let { throw it }
    } catch (e: IOException) {
        // IOExceptions are catched in SmartcardDialogFragment if thrown in this thread
        throw e
    } catch (e: Exception) {
        Timber.e(e)
    }
}

SSH Identity

Using the SecurityKeySshAuthenticator, we can create a Jsch SSH Identity class that delegates the signature operation to the smartcard.

Special care needs to be taken due to Jsch’s exception handling. Since Jsch swallows other exceptions happening during the authentication, we need to wrap IOExceptions happening during securityKeyAuthenticator.authenticateSshChallenge() into a JschException. These are catched during the SSH connection and unwrapped to properly delegate them to the SmartcardDialogFragment, as shown previously.

class SmartcardJschIdentity(
    val loginName: String,
    val securityKeyAuthenticator: SecurityKeySshAuthenticator
) : Identity {
    override fun getName() = loginName

    override fun getAlgName() = securityKeyAuthenticator.sshPublicKeyAlgorithmName
    override fun getPublicKeyBlob() = securityKeyAuthenticator.sshPublicKeyBlob
    override fun getSignature(data: ByteArray?): ByteArray {
        // wrap IOExceptions thrown by authenticateSshChallenge() into JschExceptions to catch them later
        try {
            return securityKeyAuthenticator.authenticateSshChallenge(data)
        } catch (e: IOException) {
            throw JSchException("hwsecurity IOException", e)
        }
    }

    override fun clear() {}
    override fun isEncrypted() = false
    override fun setPassphrase(passphrase: ByteArray?) = true
    override fun decrypt() = true
}

Jsch SSH Connection

The actual SSH connection can be done according to the Jsch documentation. After successfull authentication, SmartcardDialogFragment must be dismissed manually.

@WorkerThread
private fun jschConnection(loginHost: String, smartcardIdentity: SmartcardJschIdentity) {
    val jsch = JSch()
    // disable strict host key checking for testing purposes
    JSch.setConfig("StrictHostKeyChecking", "no")
    jsch.addIdentity(smartcardIdentity, null)
    val sshSession = jsch.getSession(smartcardIdentity.loginName, loginHost)

    val baos = ByteArrayOutputStream()
    baos.write("Server Output: ".toByteArray(), 0, 15)

    sshSession.connect(10000)

    val channel = sshSession.openChannel("shell")
    channel.outputStream = baos
    channel.connect(10000)

    // close dialog after successfull authentication
    smartcardDialogFragment.dismiss()
    Timber.d("SSH connection successful!")
    Timber.d("")
    Timber.d(baos.toString())
}

ssh.hwsecurity.dev Test Server

To test your implementation, we provide a SSH test server at ssh.hwsecurity.dev. When logging in with the username cotech, it simply returns the used SSH algorithm and public key. If an OpenSSH certificate is used, it is returned instead.

Public Key

Try it with your OpenSSH client first:

user@laptop:~$ ssh cotech@ssh.hwsecurity.dev

Hardware Security SDK - SSH Sample

You are using a publickey (not certificate) with type:
ssh-rsa

Your full key is:
AAAAB3NzaC1yc2EAAAADAQABAAABAQC2mh1oganCTRdymQn864LQHkibEyyeC26I5FF4NLv03QU0OxR
iIS3iLpZXJA+hL4ARBDxMWeYnytcgj2n9PLiOLJijgTyfEqVBAp7HpqnXxcKLj+cl4LpBhs81nfdeN1
osNkpsdb7J2ZprSIh8eweIw1ZLB1s6J3FQcxhOfo1I0VYV4u512ra6+13w6CvFqqbAyx9VgvNfNB9LV
rgHP0QWFs7qhtj+wWIS835R5sOiwwC2ELN2nEZKsOlQrp/Um4uFoD/UUqiqznZVNW3l8yKEWTE4jVUg
txg0iFoBqDBKk5N/2z4jUz4MaKZ0LPhlp3hL/E0sDifMwtLxziawaRJn


Bye!

Connection to ssh.hwsecurity.dev closed.

Certificate

When enforcing the use of OpenSSH certificates, the certificate is returned:

user@laptop:~$ ssh cotech@ssh.hwsecurity.dev -i user -o CertificateFile=user-cert.pub

Hardware Security SDK - SSH Sample

You are using a certificate with type:
ssh-rsa-cert-v01@openssh.com

Your full certificate is:                                                                             
AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgqAYx2y4vpU40SqMdOZ2rMyNpIORMYT2
RJHh7xf7yiygAAAADAQABAAABAQC2mh1oganCTRdymQn864LQHkibEyyeC26I5FF4NLv03QU0OxRiIS
3iLpZXJA+hL4ARBDxMWeYnytcgj2n9PLiOLJijgTyfEqVBAp7HpqnXxcKLj+cl4LpBhs81nfdeN1osN
kpsdb7J2ZprSIh8eweIw1ZLB1s6J3FQcxhOfo1I0VYV4u512ra6+13w6CvFqqbAyx9VgvNfNB9LVrgH
P0QWFs7qhtj+wWIS835R5sOiwwC2ELN2nEZKsOlQrp/Um4uFoD/UUqiqznZVNW3l8yKEWTE4jVUgtxg
0iFoBqDBKk5N/2z4jUz4MaKZ0LPhlp3hL/E0sDifMwtLxziawaRJnAAAAAAAAAAAAAAABAAAADHVzZX
Jfc2NodWVybQAAABgAAAAGZXhjZWV0AAAACnByaW5jaXBhbDIAAAAAXPp6EAAAAABe2lxQAAAAAAAAA
IIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAA
AAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl
0LXVzZXItcmMAAAAAAAAAAAAAARcAAAAHc3NoLXJzYQAAAAMBAAEAAAEBAMHqBORhry24p942rZFm0B
oGi2GyyykmFCphNnW4JbdVrg1fsS0wTPqP1NhCHIj6GHiYmXIFS++ttW/pxkTEqX0IUw62d3mukKFXX
FM1XQyYCDocFN3eWQxmcuShktiNXKbQTjnL793NiKPa3Q13T5CPCSMZYBLs0Jd+xVfMEENfGTDf3I4D
9fhpNvVtoJds21X/5BMYSqPO5RJ8utAHsv5J4pJ2n2BlyfgbgfJQ3AJs9aQk6auO0S1WbklMRZCQ2Dv
n1oi9jSvs+/HycGTmw+NJz+7EQm963MZAqIq1wMBx9wRwEBJsB22pOAVRKBnn6JdoutkXeLqVL75xcg
CJIFMAAAEPAAAAB3NzaC1yc2EAAAEALmco/xfMvJSedpprEFG9W+9KdKq2U+x/yAvo4DgjjzAaioWgY
SDli2Sad1RsEOX8svxkUYSHB2puI9KZpoOfqOq7Rh5qKCXV9tSkl4+MAPlLTLay76ts0yyOj8ZEodHH
WuqP2XN0fKjqmPhsypQ07IVg83cHQaea9ZSEGfVpMYTReGy0x32IVM1o+yN4hplIIJ8bdIKySxfUaby
Q6qZQTDAE+2J46OCs4Pfj2dTVgYc2OCrb3BKEdi90PSd2s0v2Gld5/Xwl/u6QiY4Jue61hiVA3fEzbP
YCeMpI+SwXO8sjNW+psFdl0tm9RDC2mQtg21JnVlC7WtoYNEvkt7dr4w==


Bye!

Connection to ssh.hwsecurity.dev closed. 

Profit

That’s all! If you have any questions, don’t hesitate to contact us: