Which Device(s) Support Ed25519 TLS Certificates?

Are there any Nitrokey products that support storing/using Ed25519 keys for use in a TLS certificate authority? Alternately, if OpenPGP supports Ed25519 keys, how can I use that to sign a CSR in order to create a self-signed certificate?

Hi!

The Nitrokey 3 supports Ed25519 through the OpenPGP interface. However OpenPGP is not really designed for that use case, so there is not much tooling available even though it is possible to use it to sign a CSR.

You might be able to get it working through the OpenSC PKCS#11 engine using openssl, which should support the OpenPGP card implementation from the NK3.
However support for ECC and openpgp in OpenSC is not fully implemented.

If you only need to perform such a signature you would also be able to create a manual script that performs the signature directly by talking to the card over the CCID interface. You could use the opensc-tool command line to perform the signature. But you would have to build and assemble the certificate manually.

@sosthene-nitrokey thank you so much for your response.

I’m incidentally looking into this actually :slight_smile: Implementing PKCS#11 is a bit of a nightmare I have found, but if I can extract the binary data to sign and look into how OpenSSL does things internally, I should be able to sign the data.

For anyone that finds this in the future and wants to do this, here are some breadcrumbs:

  • GnuPG has two things related to interacting with smart cards: GnuPG agent, and scdaemon.
  • Both of these have a text protocol which I assume is simply read/write from the Unix socket files.
  • You can connect to the GnuPG agent using gpg-connect-agent. Run HELP to see a list of supported commands for the GnuPG agent.
  • From the GnuPG agent console, you can send commands to scdaemon by using the SCD command. Run SCD HELP in the GnuPG agent console to see a list of supported commands.
  • Presumably you can build a scdaemon console similarly to how GnuPG agent works, but I haven’t done enough of a deep dive to confirm this.
  • Also an assumption, but you may or may not need to explicitly unlock the card with SCD UNLOCK to enter your PIN before performing any operations.
  • Next, you will need to run SCD SETDATA [--append] <hexstring> to write data to the card in order to perform a signing operation. You may need to do this in blocks using the --append flag, and obviously the data needs to be in hexadecimal. I’m not sure what limits exist for this and how much data can/should be written.
  • Finally, you will need to call SCD PKSIGN --hash=sha512 <hexified_id>. I’m also not sure what the hexified ID is, it may be the serial number of the card which can be obtained with SCD SERIALNO. It may be something else, it may be the id of the key on the device, but I’m not sure how to get this value. Play around inside of the console to try different things. The output of the signing operation hopefully conforms to EdDSA. I’m not sure whether this command will return binary output or if it will return hex-encoded output. Signatures should be 64 bytes (512 bits) in length. It could be that you need to pass --hash=none, but I’m not sure, it could be that scdaemon just understands EdDSA and will do things for you automatically.

Verification

To be sure about whether this will even work, I have started work on trying the following:

  1. Write some Rust, use the sequoia-openpgp crate, generate an Ed25519 public/private keypair on your machine (not on the card), build a PGP identity with it, write it to a file, and import it into your GnuPG keyring with gpg --import mybundle.asc
  2. Now that your key is in your GnuPG keyring, use gpg --edit-key $KEY_ID, and then use the keytocard command to import the private key to your device.
  3. Possibly using the openssl crate, and specifically openssl::pkey::PKey::private_key_from_raw_bytes, import the private key data for the Ed25519 private key you generated.
  4. Generate arbitrary data to sign.
  5. Using the openssl crate, sign the arbitrary data and stash the signature somewhere.
  6. Using gpg-connect-agent and the SCD commands to load the data you’d like to sign using SCD SETDATA into the card.
  7. Within GnuPG agent, use PKSIGN to sign the data that you wrote.
  8. Stash the signature somewhere.
  9. Compare the signature from OpenSSL to the signature your card generated.

Unlike RSA and ECDSA, EdDSA signatures are deterministic, meaning that the same signature input will produce the same signature output. RSA/ECDSA use RNG as part of their signature schemes, EdDSA does not. Therefore, if you can produce the same signature from the same data, you have verified that you can, in fact, use the OpenPGP applet to sign your certificate.

The next steps are to determine what exactly is signed in your X509 certificate, and generate a signature over that data. My research hasn’t gotten me very far on this yet. There are two options at this point depending on how X509 signatures work:

  1. It may be that you need to hash the data you want to sign before signing it, i.e. in pseudocode: scdaemon.sign(sha256(x509_binary_data)). I don’t know what hash function should be used but I imagine it is either SHA-1 or SHA-256, but it may be that you need to specify the digest when you build the certificate.
  2. It may be that you don’t need to hash the data you want to sign before signing it, i.e. in pseudocode: scdaemon.sign(x509_binary_data).

I hope this is helpful to someone else, and I hope to be able to try this out at some point.

Upon further investigation, the OpenSSL X509_sign function is what is used to sign a certificate, and accepts the X509 data, the private key, and a message digest: https://github.com/openssl/openssl/blob/6d552a532754f6ee66d6cc604655deaeb5425b16/crypto/x509/x_all.c#L60-L81

X509_sign takes the following parameters:

  1. X509 *x - the certificate
  2. EVP_PKEY *pkey - the private key
  3. const EVP_MD *md - the message digest function

This ultimately calls ASN1_item_sign_ctx and passes:

  1. parameter type: const ASN1_ITEM *it, parameter value: ASN1_ITEM_PTR(X509_CINF), I think this is the kind of ASN.1 value to be signed
  2. parameter type: X509_ALGOR *algor1, parameter value: the X509 struct’s cert_info.signature field, which is the signature algorithm to be used
  3. parameter type: X509_ALGOR *algor2, parameter value: the X509 struct’s sig_alg field, which is a struct with an ASN1_OBJECT *algorithm and `ASN1_TYPE *parameter.
  4. ASN1_BIT_STRING *signature, parameter value: the X509 struct’s signature field to store the signature in
  5. parameter type: const void *data, parameter value: the X509 struct’s cert_info field, which is a X509_CINF, which consists of data I will explain shortly
  6. parameter type: EVP_MD_CTX *ctx, parameter value is NULL`, ignore this field basically.

pkey->ameth->item_sign is a function pointer which signs the data and knows how to arrange it. I’ll see if I can keep digging to determine how this serialization occurs.