Blog by Max Ammann

Verifying a Yubikey for genuity using common tools

I received a free Yubikey from an untrusted source on the CCCamp 2019. Therefore I looked for a way to verify its authenticity. The device appeared physically to be an original and not tampered with. A check whether the key was manufactured by Yubico seems like a sufficient way to make sure that the security key is an original.

I found YubiKey Verification which successfully identified my Key as a Yubikey 5 NFC: Positive verification result

But there is no information on how this check works. As a security interrested person I digged a bit deeper on how to do this verification manually and found two ways.

PIV Attestation Method

As documented here the PIV standart allows to store private keys on the Yubikey. There is a factory-generated attestation key in slot f9. This key is signed by the Yubico PIV root certificate.

The yubico-piv-tool allows to retreive the certificate from the Yubikey.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
> yubico-piv-tool -a read-certificate -s f9 > attestation.pem
> openssl x509 -in attestation.pem -noout -text
Certificate:
    Data:
        ...
        Issuer: CN = Yubico PIV Root CA Serial 263751
        Validity
            Not Before: Mar 14 00:00:00 2016 GMT
            Not After : Apr 17 00:00:00 2052 GMT
        Subject: CN = Yubico PIV Attestation
        ...

Next we will generate a key in slot 9a and write the public key to a file:

1
> yubico-piv-tool -a generate -s 9a -A RSA2048 -k > 9a-key.pem

(The default PIN and management key can be found here)

Finally we can attest the new key using the key in f9:

1
> yubico-piv-tool --action=attest --slot=9a > 9a-attest.pem

Finally we can create a certificate chain and verify it:

1
2
3
4
> curl -O https://developers.yubico.com/PIV/Introduction/piv-attestation-ca.pem
> cat piv-attestation-ca.pem attestation.pem > chain.pem
> openssl verify -verbose -CAfile chain.pem 9a-attest.pem
9a-attest.pem: OK

This proves that the attestation key was generated by Yubico and that the attestation certificate is valid.

The 9a-attest.pem certificate contains the public key we previously stored in 9a-key.pem. The following shows the certificate:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
> openssl x509 -in 9a-attest.pem -noout -text
Certificate:
    Data:
        ...
        Issuer: CN = Yubico PIV Attestation
        ...
        Subject: CN = YubiKey PIV Attestation 9a
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:b3:5d:65:a6:3b:1b:97:5b:97:6a:c9:ec:1a:5b:
                    d9:e5:30:16:56:b0:cd:84:f9:cb:d7:0f:90:e1:68:
                    8d:fb:73:c4:09:fd:97:7c:10:8e:19:71:25:bb:a1:
                    db:52:e7:ce:bc:28:f4:7d:d0:b5:b4:9a:68:e4:8a:
                    ...

whereas the following shows the public key of slot 9a:

1
2
3
4
5
6
7
8
> openssl rsa -inform PEM -pubin -in 9a-key.pem -text
RSA Public-Key: (2048 bit)
Modulus:
    00:b3:5d:65:a6:3b:1b:97:5b:97:6a:c9:ec:1a:5b:
    d9:e5:30:16:56:b0:cd:84:f9:cb:d7:0f:90:e1:68:
    8d:fb:73:c4:09:fd:97:7c:10:8e:19:71:25:bb:a1:
    db:52:e7:ce:bc:28:f4:7d:d0:b5:b4:9a:68:e4:8a:
    ...

WebAuthn/U2F Method

The next method goes a little bit deeper into how WebAuthn works internally. We start by getting a WebAuthn response from our Yubikey. Even tough there are probably cooler ways I just got it from the Yubikey WebAuthn demo site. After registering you key there you can get the response in the technical details:

WebAuthn response

Using the MIT licensed code by Ackermann Yuriy we are able to verify the signature ourselves. Props go to him for reading all those RFCs!

The following functions and imports are needed in order to get the data, signature and certificate used in the attestation in WebAuthn:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
const crypto = require('crypto')
const base64url = require('base64url')
const cbor = require('cbor')
const jsrsasign = require('jsrsasign')
const fs = require('fs')

let hash = (alg, message) => {
  return crypto.createHash(alg).update(message).digest()
}

let base64ToPem = (b64cert) => {
  let pemcert = ''
  for (let i = 0; i < b64cert.length; i += 64)
    pemcert += b64cert.slice(i, i + 64) + '\n'

  return '-----BEGIN CERTIFICATE-----\n' + pemcert + '-----END CERTIFICATE-----'
}

const getCertificateInfo = (certificate) => {
  let subjectCert = new jsrsasign.X509()
  subjectCert.readCertPEM(certificate)

  let subjectString = subjectCert.getSubjectString()
  let subjectParts = subjectString.slice(1).split('/')

  let subject = {}
  for (let field of subjectParts) {
    let kv = field.split('=')
    subject[kv[0]] = kv[1]
  }

  let version = subjectCert.version
  let basicConstraintsCA = !!subjectCert.getExtBasicConstraints().cA

  return {
    subject, version, basicConstraintsCA
  }
}

const parseAuthData = (buffer) => {
  let rpIdHash = buffer.slice(0, 32)
  buffer = buffer.slice(32)
  let flagsBuf = buffer.slice(0, 1)
  buffer = buffer.slice(1)
  let flagsInt = flagsBuf[0]
  let flags = {
    up: !!(flagsInt & 0x01),
    uv: !!(flagsInt & 0x04),
    at: !!(flagsInt & 0x40),
    ed: !!(flagsInt & 0x80),
    flagsInt
  }

  let counterBuf = buffer.slice(0, 4)
  buffer = buffer.slice(4)
  let counter = counterBuf.readUInt32BE(0)

  let aaguid = undefined
  let credID = undefined
  let COSEPublicKey = undefined

  if (flags.at) {
    aaguid = buffer.slice(0, 16)
    buffer = buffer.slice(16)
    let credIDLenBuf = buffer.slice(0, 2)
    buffer = buffer.slice(2)
    let credIDLen = credIDLenBuf.readUInt16BE(0)
    credID = buffer.slice(0, credIDLen)
    buffer = buffer.slice(credIDLen)
    COSEPublicKey = buffer
  }

  return {rpIdHash, flagsBuf, flags, counter, counterBuf, aaguid, credID, COSEPublicKey}
}

let COSEECDHAtoPKCS = (COSEPublicKey) => {
  /*
     +------+-------+-------+---------+----------------------------------+
     | name | key   | label | type    | description                      |
     |      | type  |       |         |                                  |
     +------+-------+-------+---------+----------------------------------+
     | crv  | 2     | -1    | int /   | EC Curve identifier - Taken from |
     |      |       |       | tstr    | the COSE Curves registry         |
     |      |       |       |         |                                  |
     | x    | 2     | -2    | bstr    | X Coordinate                     |
     |      |       |       |         |                                  |
     | y    | 2     | -3    | bstr /  | Y Coordinate                     |
     |      |       |       | bool    |                                  |
     |      |       |       |         |                                  |
     | d    | 2     | -4    | bstr    | Private key                      |
     +------+-------+-------+---------+----------------------------------+
  */

  let coseStruct = cbor.decodeAllSync(COSEPublicKey)[0]
  let tag = Buffer.from([0x04])
  let x = coseStruct.get(-2)
  let y = coseStruct.get(-3)

  return Buffer.concat([tag, x, y])
}

In order to verify this using openssl we first need to extract the data, its signature and the PEM certificate:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let attestationBuffer = base64url.toBuffer(webAuthnResponse.response.attestationObject)
let ctapMakeCredResp = cbor.decodeAllSync(attestationBuffer)[0]
let authrDataStruct = parseAuthData(ctapMakeCredResp.authData)

if (!authrDataStruct.flags.up)
  throw new Error('User was NOT presented durring authentication!')

let clientDataHash = hash('sha256', base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let reservedByte = Buffer.from([0x00])
let publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
let signatureBase = Buffer.concat([reservedByte, authrDataStruct.rpIdHash, clientDataHash, authrDataStruct.credID, publicKey])

let PEMCertificate = base64ToPem(ctapMakeCredResp.attStmt.x5c[0].toString('base64'))

let signature = ctapMakeCredResp.attStmt.sig

const pemStream = fs.createWriteStream('certificate.pem')
pemStream.write(PEMCertificate)
pemStream.end()
const signatureStream = fs.createWriteStream('signature.sig')
signatureStream.write(signature)
signatureStream.end()
const dataStream = fs.createWriteStream('data')
dataStream.write(signatureBase)
dataStream.end()

We also need a root certificate by Yubico again which will be used to verify the chain:

1
2
3
> curl -O https://developers.yubico.com/U2F/yubico-u2f-ca-certs.txt
> openssl verify -verbose -CAfile yubico-u2f-ca-certs.txt certificate.pem
certificate.pem: OK

The last step verifies the signature and whether the certificate.pem is actually used:

1
2
3
> openssl x509 -pubkey -noout -in certificate.pem > certificate-raw.pem
> openssl dgst -sha256 -verify certificate-raw.pem -signature signature.sig data
Verified OK

Summary

I gave some insights on how attestation works on the Yubico and how it can be used to verify certificates in order to hopefully prove its genuity.

Do you have Questions? Send an E-Mail to max@maxammann.org