Continuing to the next step of PIV provisioning with open-source utilities, we need to generate a certificate signing request (CSR) corresponding to the key-pair generated on the card. This is a signed document generated by the person enrolling for a certificate, containing information they would like to have appear on the certificate, including their name, organization affiliation and public-key. The certificate authority verifies this information, makes changes/amendments as necessary and issues a certificate. This ritual for using CSRs is largely a matter of convention. Nothing stops a CA from issuing any certificate to anyone given just the public key. But most CA implementations are designed to accept only valid CSRs as input to the issuance procedure.
Offloading crypto in openssl
That calls for constructing the CSR with proper fields, including the public-key that exists on the card and getting that object signed using the PIV card. openssl provides both these ingredients:
- “req” subcommand for working with CSRs in PKCS #10 format
- Ability to offload cryptographic operations such as signing to external hardware using the concept of “engines.” An engine is the openssl abstraction layer for cryptographic tokens, similar to PKCS #11 or Windows smart-card minidrivers.
Our strategy then is to find a suitable engine implementation that can interop with PIV cards and use it for signing the CSR. Fortunately OpenSC project provides precisely such an engine, building on top of its PKCS #11 library that figured in previous PIV scenarios.
It is possible to script openssl to load an engine first and perform an operation using keys associated with that engine. Here is an example gist for CMS decryption. It issues two commands to openssl. First one loads the engine by specifying the shared library path and assigning it a name for future reference. Second one performs the CMS decryption using a private-key associated with that engine, in this case key ID 01 corresponding to the PIV authentication key. (Somewhat confusingly the PKCS11 layer uses does not use the PIV key-reference identifiers.)
It would appear straightforward to create a similar script to load the engine and invoke CSR generation while referencing a key associated with the card. But this runs into a subtle problem: OpenSC middleware assumes there is no key present in a particular slot unless there is also an associated certificate. This is a reasonable assumption for PIV cards in steady-state: each key-type defined in the standard such as PIV authentication or digital signature has both an X509 certificate and associated private key material. But during provisioning that assumption is violated temporarily: a key-pair must be generated first before we can have a certificate containing the public piece.
That leads to a boot-strapping problem: OpenSC middleware can not use the private-key on card without an associated certificate also present on the same card. But we can not obtain a certificate unless we first sign a CSR using the private-key.
It turns out OpenSC is not to blame for this either. Windows PIV minidriver has the same behavior; it would report no credentials present on a card without certificates even if keys had been generated for all four slots. Root cause is a more subtle problem with the PIV command interface itself: there is no mechanism for retrieving a stand-alone public-key. The assumption is that public-keys are extracted from the certificate. In the absence of the certificate, there is no uniform way to even learn the public-key for the purpose of placing it into the CSR.**
Signing the hard way
The above observation also rules out a kludge that at first sight seems promising: provision a dummy certificate containing an unrelated public-key to make OpenSC happy. That would allow signing to “work”– it turns out the middleware does not check the signature for consistency against public key in the certificate. But the CSR generated this way would still have the wrong public-key drawn from the placeholder certificate.
Boot-strapping requires a more complex sequence involving manual ASN1 manipulation:
- Save the output from key generation step
- Craft an unsigned CSR containing that public-key
- This is easiest to accomplish by taking an existing CSR with all of the other fields correct, removing the signature and “grafting” a new public-key in place of the existing field.
- Get a signature from the card.
- This can be done either by using the placeholder-certificate approach described above or directly issuing a GENERAL AUTHENTICATE command to the PIV application.
- Combine the signature with the modified request from step #2 above to create a valid signed CSR.
As a side-note, generating a new CSR once a valid certificate exists is much easier. In that case the key is not changed and the certificate is consistent with the key. It is much easier to go from a card already set up with self-signed certificates (for example provisioned on Windows via certreq) to issuing a new certificates from a different CA using off-the-shelf open source utilities.
Once CSR is created and exchanged for a valid certificate, the final step is straightforward: load the certificate. This is done using piv-tool and authenticating with the card management key, similar to key generation.
** For ECDSA the public-key can be derived from one valid signature and associated hash. For RSA there is no similar quick fix, but most cards will give an error when asked to operate on an input larger than the modulus. This can be used to binary-search for the correct modulus.