Native YubiKey support

321 views
Skip to first unread message

Jack Grigg

unread,
Dec 1, 2019, 8:00:49 PM12/1/19
to age-dev
Hi all,

I took advantage of the four-day weekend to work on a rather fun project: native age support for YubiKeys! You can play with it here:


Bear in mind that it relies on a work-in-progress YubiKey crate, and while I've tested the current functionality (and the PIV app is separate from any others on the key, like OpenPGP), there is still a high risk that it may eat your kittens. Only use it with a YubiKey for which you don't mind losing your PIV keys (or potentially all of them).

I chatted a bit with Filippo offline about the design, and this is the draft spec I've put together:

---

A YubiKey "identity" is managed locally as a key stub with the following form:

AGE_YUBIKEY_STUB_hex(serial)_hex(algorithm)_hex(slot)

It may exist in files alongside age X25519 secret keys, and is similarly passed to the age / rage binary with the -i flag.

A YubiKey RSA recipient has the following form:

yubikey:rsa:encode(modulus):encode(exponent)

where "encode" is defined in the age spec, and "modulus" and "exponent" are encoded in big endian (only because that's how the SSH formats internally encode them; changing this is dead simple).

A YubiKey EC recipient has the following form:

yubikey:ec:[p256|p384]:encode(public key)

where "public key" is encoded in its compressed canonical format, and its length is verified against the algorithm type.

A YubiKey RSA recipient line is:

-> yubikey rsa\n
encode(RSAES-OAEP[public key, "age-tool.com yubikey-rsa"](file key))\n

The body of this recipient MUST be wrapped at 56 columns.

A YubiKey EC recipient line is of the form:

-> yubikey [p256|p384] encode(ECDH(ephemeral secret, basepoint))\n
encode(encrypt[HKDF[salt, label](ECDH(ephemeral secret, public key))](file key))\n

where "basepoint" is the standard generator of the specified curve, "ephemeral secret" is a random scalar within the scalar field of the specified curve and MUST be new for every file key, "salt" is ECDH(ephemeral secret, basepoint) || public key, and "label" is "age-tool.com yubikey-p256" or "age-tool.com yubikey-p384".

---

Thoughts? Suggestions?

Cheers,
str4d

Filippo Valsorda

unread,
Dec 1, 2019, 10:27:45 PM12/1/19
to age-dev, Jack Grigg
Awesome!

Why support RSA if the YubiKey will do P-256? Likewise, I don't feel the need to support P-384.

Jack Grigg

unread,
Dec 2, 2019, 6:16:24 AM12/2/19
to Filippo Valsorda, age-dev
On Mon, 2 Dec 2019, 3:27 am Filippo Valsorda, <fil...@ml.filippo.io> wrote:
Awesome!

Why support RSA if the YubiKey will do P-256? Likewise, I don't feel the need to support P-384.

Currently: because I'm helping implement the YubiKey backing crate as we go, and I gained access to the RSA primitives first 😝

In practice: I agree that P-256 is the ideal case to support from a UX perspective (shortest pubkeys), and I agree with the one-well-liked-joint philosophy, so I'm very happy to drop native RSA support entirely and use P-256.

I just question whether there might be situations where a user is required to use one of the other algorithms? I notice that NSA's Commercial National Security Algorithm Suite specifically requires 3072-bit RSA or P-384 (the now-historic Suite B is what the YubiKey's current PIV algorithms correspond to), so supporting P-384 might be beneficial to adoption.

Jack Grigg

unread,
Dec 2, 2019, 6:50:40 AM12/2/19
to Filippo Valsorda, age-dev
I'm also going to add a tag to the key stub which is the first 32 bytes of the SHA-2 hash of the age-serialized pubkey. The intent of this is to enable rage to detect if the key in a slot has changed and give a useful error message, while being resilient against certificate expiry and reissuance.

Filippo Valsorda

unread,
Dec 2, 2019, 9:57:54 AM12/2/19
to Jack Grigg, age...@googlegroups.com
2019-12-02 06:16 GMT-05:00 Jack Grigg <m...@jackgrigg.com>:
I just question whether there might be situations where a user is required to use one of the other algorithms? I notice that NSA's Commercial National Security Algorithm Suite specifically requires 3072-bit RSA or P-384 (the now-historic Suite B is what the YubiKey's current PIV algorithms correspond to), so supporting P-384 might be beneficial to adoption.

We already dropped the AEAD marker, and use ChaCha20, so FIPS compliance is not happening in the first iteration. If people make a lot of noise about it, we can always add modes, but not remove them.

Wondering if we should call the recipient just "yubikey" or "yubikey-p256" at this point. Since it's always going to be the default, even if we add variants down the road, I'm thinking the former.

2019-12-02 06:50 GMT-05:00 Jack Grigg <m...@jackgrigg.com>:
I'm also going to add a tag to the key stub which is the first 32 bytes of the SHA-2 hash of the age-serialized pubkey. The intent of this is to enable rage to detect if the key in a slot has changed and give a useful error message, while being resilient against certificate expiry and reissuance.

That's a good idea. It can probably be just the first 4 bytes. (Did you mean first 32 bits?)

Can we just make the keys not expire at all?

Now, should we add the tag to the recipient too?

Jack Grigg

unread,
Dec 2, 2019, 10:15:28 AM12/2/19
to Filippo Valsorda, age-dev


On Mon, 2 Dec 2019, 2:57 pm Filippo Valsorda, <fil...@ml.filippo.io> wrote:
2019-12-02 06:16 GMT-05:00 Jack Grigg <m...@jackgrigg.com>:
I just question whether there might be situations where a user is required to use one of the other algorithms? I notice that NSA's Commercial National Security Algorithm Suite specifically requires 3072-bit RSA or P-384 (the now-historic Suite B is what the YubiKey's current PIV algorithms correspond to), so supporting P-384 might be beneficial to adoption.

We already dropped the AEAD marker, and use ChaCha20, so FIPS compliance is not happening in the first iteration. If people make a lot of noise about it, we can always add modes, but not remove them.

Very good point!


Wondering if we should call the recipient just "yubikey" or "yubikey-p256" at this point. Since it's always going to be the default, even if we add variants down the road, I'm thinking the former.

That sounds reasonable. I'll post an update to the draft later this week that just has a "yubikey" pubkey and recipient line that corresponds to P-256. I'll update my PR to match as soon as I get EC support into the yubikey-piv crate.


2019-12-02 06:50 GMT-05:00 Jack Grigg <m...@jackgrigg.com>:
I'm also going to add a tag to the key stub which is the first 32 bytes of the SHA-2 hash of the age-serialized pubkey. The intent of this is to enable rage to detect if the key in a slot has changed and give a useful error message, while being resilient against certificate expiry and reissuance.

That's a good idea. It can probably be just the first 4 bytes. (Did you mean first 32 bits?)

Yes, that is indeed what I meant 😝


Can we just make the keys not expire at all?

The way it works is that you generate a key, and then create a self-signed certificate for the key and import it. So we can define whatever expressible policy we want on certificates we generate ourselves.

Users could also use an existing compatible-algorithm certificate in an existing slot, and the Yubico tools don't let you set no expiry AFAICT, so there might be some that still expire. We can't prevent this, but we could inhibit it by e.g. requiring the Subject Name to contain some age-specific magic?


Now, should we add the tag to the recipient too?

I'm inclined to say yes, if we draw comparison to why the tag is present for SSH recipients (there may be recipients for which the key is encrypted). The logic would be that we don't want to be asking the user to enter their YubiKey PIN for every single registered slot and key if they aren't going to be a match. But this does prevent recipient privacy, allowing tracking of recipients across multiple encrypted messages. So... Hmm...

Note that when generating keys, we also get to set the PIN and touch policies (which are set on private key generation and cannot be changed) to some combination of "Never", "Once per USB session / One touch per 15 seconds" and "Every decryption".

Filippo Valsorda

unread,
Dec 2, 2019, 10:31:22 AM12/2/19
to Jack Grigg, age-dev
2019-12-02 10:15 GMT-05:00 Jack Grigg <m...@jackgrigg.com>:
2019-12-02 06:50 GMT-05:00 Jack Grigg <m...@jackgrigg.com>:
I'm also going to add a tag to the key stub which is the first 32 bytes of the SHA-2 hash of the age-serialized pubkey. The intent of this is to enable rage to detect if the key in a slot has changed and give a useful error message, while being resilient against certificate expiry and reissuance.

That's a good idea. It can probably be just the first 4 bytes. (Did you mean first 32 bits?)

Yes, that is indeed what I meant 😝



Can we just make the keys not expire at all?

The way it works is that you generate a key, and then create a self-signed certificate for the key and import it. So we can define whatever expressible policy we want on certificates we generate ourselves.

Users could also use an existing compatible-algorithm certificate in an existing slot, and the Yubico tools don't let you set no expiry AFAICT, so there might be some that still expire. We can't prevent this, but we could inhibit it by e.g. requiring the Subject Name to contain some age-specific magic?

Eh, if someone configures their own PIV slot and then manually puts together a stub, I don't care much about handing them sharp edges. The important thing is that the age-driven flow does not have any expiration.


Now, should we add the tag to the recipient too?

I'm inclined to say yes, if we draw comparison to why the tag is present for SSH recipients (there may be recipients for which the key is encrypted). The logic would be that we don't want to be asking the user to enter their YubiKey PIN for every single registered slot and key if they aren't going to be a match. But this does prevent recipient privacy, allowing tracking of recipients across multiple encrypted messages. So... Hmm...

I might be wrong, but I expect YK encryption to be more of a local thing, for pass or backups, than a send-over-the-Internet thing. Let's add the tag and see if anyone complains.

Note that when generating keys, we also get to set the PIN and touch policies (which are set on private key generation and cannot be changed) to some combination of "Never", "Once per USB session / One touch per 15 seconds" and "Every decryption".

Cool! We need to get the age-keygen CLI right for this, but I'm very very excited about making touch policies more widely available. I am thinking the default should be a PIN of once per session (if set), and a touch policy of every decryption.

One cryptography question: is there anything we can do to make the P-256 E-S ECDH footgun safer? We should at least enumerate what the [r]age software should do to protect the private key implementation. (On curve checks for sure, is there a way to check the correctness of the operation?)

Jack Grigg

unread,
Dec 2, 2019, 11:04:22 PM12/2/19
to Filippo Valsorda, age-dev
On Mon, Dec 2, 2019 at 3:31 PM Filippo Valsorda <fil...@ml.filippo.io> wrote:
I might be wrong, but I expect YK encryption to be more of a local thing, for pass or backups, than a send-over-the-Internet thing. Let's add the tag and see if anyone complains.
 
👍

I've updated my PR https://github.com/str4d/rage/pull/25 to only use P-256. I'm using the top post there to track the current draft specification, which now includes the tag. I encoded the tag in the recipient line using Base64 to match the other recipient lines, but in the key stub I encode it in hex (because the parts are underscore-separated).

One thing I've discovered: when generating EC keys, the YubiKey returns the pubkeys in their uncompressed encoding (65 bytes for P-256). Additionally, the ring crate I'm using for key agreement requires pubkeys in uncompressed form. For now, my PR uses uncompressed encodings, as I'll need to implement conversion between compressed and uncompressed encodings manually.
Note that when generating keys, we also get to set the PIN and touch policies (which are set on private key generation and cannot be changed) to some combination of "Never", "Once per USB session / One touch per 15 seconds" and "Every decryption".

Cool! We need to get the age-keygen CLI right for this, but I'm very very excited about making touch policies more widely available. I am thinking the default should be a PIN of once per session (if set), and a touch policy of every decryption.

👍
 

One cryptography question: is there anything we can do to make the P-256 E-S ECDH footgun safer? We should at least enumerate what the [r]age software should do to protect the private key implementation. (On curve checks for sure, is there a way to check the correctness of the operation?)

Do we know what protections the YubiKey itself implements internally? Ideally it already verifies that the provided public key is on the curve, and is not the point at infinity.

Tony Arcieri

unread,
Dec 2, 2019, 11:08:14 PM12/2/19
to Jack Grigg, Filippo Valsorda, age-dev
On Mon, Dec 2, 2019 at 8:04 PM Jack Grigg <m...@jackgrigg.com> wrote:
One thing I've discovered: when generating EC keys, the YubiKey returns the pubkeys in their uncompressed encoding (65 bytes for P-256). Additionally, the ring crate I'm using for key agreement requires pubkeys in uncompressed form. For now, my PR uses uncompressed encodings, as I'll need to implement conversion between compressed and uncompressed encodings manually.

Would love to find somewhere for you to upstream NIST p-curve point compression, if you're interested in implementing it!

One cryptography question: is there anything we can do to make the P-256 E-S ECDH footgun safer? We should at least enumerate what the [r]age software should do to protect the private key implementation. (On curve checks for sure, is there a way to check the correctness of the operation?)

Do we know what protections the YubiKey itself implements internally? Ideally it already verifies that the provided public key is on the curve, and is not the point at infinity.

I haven't looked into what it offers in hardware... is it a raw ECDH function?

If so, it seems like it'd be nice to subject the incoming elliptic curve point to a battery of tests to avoid invalid curve / point attacks.

--
Tony Arcieri

Filippo Valsorda

unread,
Dec 3, 2019, 12:56:05 AM12/3/19
to Tony Arcieri, Jack Grigg, age-dev
2019-12-02 23:08 GMT-05:00 Tony Arcieri <bas...@gmail.com>:
On Mon, Dec 2, 2019 at 8:04 PM Jack Grigg <m...@jackgrigg.com> wrote:
One thing I've discovered: when generating EC keys, the YubiKey returns the pubkeys in their uncompressed encoding (65 bytes for P-256). Additionally, the ring crate I'm using for key agreement requires pubkeys in uncompressed form. For now, my PR uses uncompressed encodings, as I'll need to implement conversion between compressed and uncompressed encodings manually.

Would love to find somewhere for you to upstream NIST p-curve point compression, if you're interested in implementing it!


Why did TLS settle on uncompressed points again? (As opposed to compressed ones, not to negotiation, of course.)

One cryptography question: is there anything we can do to make the P-256 E-S ECDH footgun safer? We should at least enumerate what the [r]age software should do to protect the private key implementation. (On curve checks for sure, is there a way to check the correctness of the operation?)

Do we know what protections the YubiKey itself implements internally? Ideally it already verifies that the provided public key is on the curve, and is not the point at infinity.

I haven't looked into what it offers in hardware... is it a raw ECDH function?

If so, it seems like it'd be nice to subject the incoming elliptic curve point to a battery of tests to avoid invalid curve / point attacks.

Let's both run the Wycheproof tests on the YubiKey, because why not, and still try to come up with a comprehensive set of checks we can run in application-space. I'd very much like us not to become the new target for creative E-S ECDH attacks.

Still, probably nothing we can do against Paris256-like bug attacks?

P.S. has anyone noticed anything new over at https://github.com/google/wycheproof? 👀

Tony Arcieri

unread,
Dec 3, 2019, 8:48:02 AM12/3/19
to Filippo Valsorda, Jack Grigg, age-dev
On Mon, Dec 2, 2019 at 9:56 PM Filippo Valsorda <fil...@ml.filippo.io> wrote:
Why did TLS settle on uncompressed points again? (As opposed to compressed ones, not to negotiation, of course.)

AFAIK: point compression patents 

--
Tony Arcieri

Filippo Valsorda

unread,
Dec 3, 2019, 11:28:13 AM12/3/19
to Tony Arcieri, Jack Grigg, age-dev
That's weird for TLS 1.3, because US 6,252,960 expired in 2018 and US 6,141,420 in 2014, and points are not sent in a Client Hello (before version negotiation) before TLS 1.3, so there is no backwards compatibility lock-in. Oh well.

Let's use SEC 1 point compression for this.

Tony Arcieri

unread,
Dec 3, 2019, 11:33:53 AM12/3/19
to Filippo Valsorda, Jack Grigg, age-dev
On Tue, Dec 3, 2019 at 8:28 AM Filippo Valsorda <fil...@ml.filippo.io> wrote:
AFAIK: point compression patents 

That's weird for TLS 1.3, because US 6,252,960 expired in 2018 and US 6,141,420 in 2014

I should've said: now expired point compression patents.
 
ECC in TLS shipped with uncompressed points due to those patents, and in the intervening time since they lapsed, nobody cared/bothered to switch to compressed ones. I expect they might break a lot of things which assume points are always uncompressed.

--
Tony Arcieri

Jack Grigg

unread,
Dec 7, 2019, 6:22:42 PM12/7/19
to Tony Arcieri, Filippo Valsorda, age-dev
I bit the bullet and implemented point compression and decompression for secp256r1 in Rust.

I've updated my PR (https://github.com/str4d/rage/pull/25) and it should now match the draft spec (which I've clarified regarding where SEC-1 compressed encoding is used).

Cheers,
str4d

n...@stalder.io

unread,
Dec 17, 2019, 4:56:19 AM12/17/19
to age-dev
Hi all,

Awesome to see work towards hardware-backed keys!

I'm not so thrilled that everything is branded as "Yubikey", when actually:
- the underlying standard is PIV
- Yubikey is the carrier medium
- Tony's yubikey-piv is the nice API to it

Admittedly, I'm biased, as I'm working towards a
- fully open-source PIV-compatible firmware in Rust,
- for my company SoloKey's open-source hardware keys
- with eventual client APIs for Rust and Go
- and, as main distinguishing feature, 25519 support from the start (as PIV protocol extension)

Therefore, I would love to see
- the roles of PIV, P256 and Yubikeys disentangled in this pull request
- extendability for keys with support for more algorithms in mind

For instance, in my case I'd assume I can get by with the normal X25519 recipient line.
Here, I'd expect a very similar P256 recipient line.

Whether the recipient has the key stored on a drive or on a key is their private matter,
and I'd expect a flag that is passed to `age` on the command line to specify where the key is.

Thoughts?

I'd be happy to draft a PR against the PR if there is agreement to separate these matters.

Jack Grigg

unread,
Dec 17, 2019, 1:15:50 PM12/17/19
to n...@stalder.io, age-dev
On Tue, Dec 17, 2019 at 3:56 AM <n...@stalder.io> wrote:
Hi all,

Awesome to see work towards hardware-backed keys!

I'm not so thrilled that everything is branded as "Yubikey", when actually:
- the underlying standard is PIV
- Yubikey is the carrier medium
- Tony's yubikey-piv is the nice API to it

Admittedly, I'm biased, as I'm working towards a
- fully open-source PIV-compatible firmware in Rust,
- for my company SoloKey's open-source hardware keys
- with eventual client APIs for Rust and Go
- and, as main distinguishing feature, 25519 support from the start (as PIV protocol extension)

Nice!
 

Therefore, I would love to see
- the roles of PIV, P256 and Yubikeys disentangled in this pull request

Given that the PIV standard is pretty clear about the requirement for supporting P256 (and thus we can assume ubiquitous support for at least slot 9d), I'd be fine with replacing "yubikey" with "piv" in the spec, and making the actual hardware an implementation detail:

- *age-keygen would be the only component that makes the per-hardware distinction visible in the CLI flags, in order for users to perform initial setup.

- *age itself would continue to treat all decryption pathways as "identities" that are passed in opaquely by the user, and only display details about the specific hardware key when asking for authentication (as it already needs to for OpenSSH key decryption).
 
- extendability for keys with support for more algorithms in mind

This goes against the "one well-oiled joint" philosophy, as Filippo correctly pointed out to me up-thread. If we are already paying the complexity cost of supporting PIV-P256, best to just focus on that for the native PIV use-case.
 

For instance, in my case I'd assume I can get by with the normal X25519 recipient line.
Here, I'd expect a very similar P256 recipient line.

They aren't quite interchangeable:

- The current YubiKey draft includes a public key tag, because the overhead of trial-authenticating to every registered hardware key is rather burdensome (see also the existing specification for handling of possibly-encrypted OpenSSH keys).

- The normal X25519 recipient lines do not contain any kind of public key tag, which provides recipient privacy in exchange for recipients needing to try every key to find the correct one. This works well because the secret keys aren't behind any encryption themselves.

Put another way, an age recipient line encodes both a particular algorithm choice, and a UX decision about recipient privacy vs decryption overhead. Trying to split these apart introduces configuration complexity into the age spec, which goes against the stated goals.

My current opinion is that age should focus on a few key-usage setups that can service the large majority of use-cases. That being said:
 

Whether the recipient has the key stored on a drive or on a key is their private matter,
and I'd expect a flag that is passed to `age` on the command line to specify where the key is.

It's not entirely private (see above), but I agree that there's nothing preventing someone from having their own custom age implementation that pretends its hardware keys are actually on-filesystem keys. For the CLI spec, the existing identity flag is entirely sufficient for this; the stub generated by *age-keygen would simply identify the kind of hardware key. This is already the case in my draft YubiKey spec.

Cheers,
str4d
Reply all
Reply to author
Forward
0 new messages