djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 1 | This document describes OpenSSH's support for U2F/FIDO security keys. |
| 2 | |
| 3 | Background |
| 4 | ---------- |
| 5 | |
| 6 | U2F is an open standard for two-factor authentication hardware, widely |
| 7 | used for user authentication to websites. U2F tokens are ubiquitous, |
| 8 | available from a number of manufacturers and are currently by far the |
| 9 | cheapest way for users to achieve hardware-backed credential storage. |
| 10 | |
| 11 | The U2F protocol however cannot be trivially used as an SSH protocol key |
| 12 | type as both the inputs to the signature operation and the resultant |
| 13 | signature differ from those specified for SSH. For similar reasons, |
| 14 | integration of U2F devices cannot be achieved via the PKCS#11 API. |
| 15 | |
| 16 | U2F also offers a number of features that are attractive in the context |
| 17 | of SSH authentication. They can be configured to require indication |
| 18 | of "user presence" for each signature operation (typically achieved |
| 19 | by requiring the user touch the key). They also offer an attestation |
| 20 | mechanism at key enrollment time that can be used to prove that a |
| 21 | given key is backed by hardware. Finally the signature format includes |
| 22 | a monotonic signature counter that can be used (at scale) to detect |
| 23 | concurrent use of a private key, should it be extracted from hardware. |
| 24 | |
naddy@openbsd.org | ad38406 | 2019-11-01 12:10:43 +0000 | [diff] [blame] | 25 | U2F private keys are generated through an enrollment operation, |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 26 | which takes an application ID - a URL-like string, typically "ssh:" |
| 27 | in this case, but a HTTP origin for the case of web authentication, |
| 28 | and a challenge string (typically randomly generated). The enrollment |
| 29 | operation returns a public key, a key handle that must be used to invoke |
| 30 | the hardware-backed private key, some flags and signed attestation |
naddy@openbsd.org | ad38406 | 2019-11-01 12:10:43 +0000 | [diff] [blame] | 31 | information that may be used to verify that a private key is hosted on a |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 32 | particular hardware instance. |
| 33 | |
| 34 | It is common for U2F hardware to derive private keys from the key handle |
| 35 | in conjunction with a small per-device secret that is unique to the |
| 36 | hardware, thus requiring little on-device storage for an effectively |
| 37 | unlimited number of supported keys. This drives the requirement that |
| 38 | the key handle be supplied for each signature operation. U2F tokens |
djm@openbsd.org | 1306643 | 2019-11-18 04:34:47 +0000 | [diff] [blame] | 39 | primarily use ECDSA signatures in the NIST-P256 field, though the FIDO2 |
naddy@openbsd.org | 416f153 | 2019-12-20 20:28:55 +0000 | [diff] [blame] | 40 | standard specifies additional key types, including one based on Ed25519. |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 41 | |
| 42 | SSH U2F Key formats |
| 43 | ------------------- |
| 44 | |
djm@openbsd.org | 1306643 | 2019-11-18 04:34:47 +0000 | [diff] [blame] | 45 | OpenSSH integrates U2F as new key and corresponding certificate types: |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 46 | |
| 47 | sk-ecdsa-sha2-nistp256@openssh.com |
| 48 | sk-ecdsa-sha2-nistp256-cert-v01@openssh.com |
djm@openbsd.org | 1306643 | 2019-11-18 04:34:47 +0000 | [diff] [blame] | 49 | sk-ssh-ed25519@openssh.com |
| 50 | sk-ssh-ed25519-cert-v01@openssh.com |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 51 | |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 52 | While each uses ecdsa-sha256-nistp256 as the underlying signature primitive, |
| 53 | keys require extra information in the public and private keys, and in |
| 54 | the signature object itself. As such they cannot be made compatible with |
| 55 | the existing ecdsa-sha2-nistp* key types. |
| 56 | |
| 57 | The format of a sk-ecdsa-sha2-nistp256@openssh.com public key is: |
| 58 | |
| 59 | string "sk-ecdsa-sha2-nistp256@openssh.com" |
djm@openbsd.org | 93fa2a6 | 2019-11-18 04:16:53 +0000 | [diff] [blame] | 60 | string curve name |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 61 | ec_point Q |
| 62 | string application (user-specified, but typically "ssh:") |
| 63 | |
| 64 | The corresponding private key contains: |
| 65 | |
| 66 | string "sk-ecdsa-sha2-nistp256@openssh.com" |
djm@openbsd.org | 93fa2a6 | 2019-11-18 04:16:53 +0000 | [diff] [blame] | 67 | string curve name |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 68 | ec_point Q |
| 69 | string application (user-specified, but typically "ssh:") |
djm@openbsd.org | 71856e1 | 2019-11-18 04:29:50 +0000 | [diff] [blame] | 70 | uint8 flags |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 71 | string key_handle |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 72 | string reserved |
| 73 | |
djm@openbsd.org | 1306643 | 2019-11-18 04:34:47 +0000 | [diff] [blame] | 74 | The format of a sk-ssh-ed25519@openssh.com public key is: |
| 75 | |
| 76 | string "sk-ssh-ed25519@openssh.com" |
| 77 | string public key |
| 78 | string application (user-specified, but typically "ssh:") |
| 79 | |
| 80 | With a private half consisting of: |
| 81 | |
| 82 | string "sk-ssh-ed25519@openssh.com" |
| 83 | string public key |
| 84 | string application (user-specified, but typically "ssh:") |
djm@openbsd.org | a62f4e1 | 2019-12-10 23:37:31 +0000 | [diff] [blame] | 85 | uint8 flags |
djm@openbsd.org | 1306643 | 2019-11-18 04:34:47 +0000 | [diff] [blame] | 86 | string key_handle |
| 87 | string reserved |
| 88 | |
| 89 | The certificate form for SSH U2F keys appends the usual certificate |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 90 | information to the public key: |
| 91 | |
naddy@openbsd.org | ad38406 | 2019-11-01 12:10:43 +0000 | [diff] [blame] | 92 | string "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com" |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 93 | string nonce |
djm@openbsd.org | 93fa2a6 | 2019-11-18 04:16:53 +0000 | [diff] [blame] | 94 | string curve name |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 95 | ec_point Q |
| 96 | string application |
| 97 | uint64 serial |
| 98 | uint32 type |
| 99 | string key id |
| 100 | string valid principals |
| 101 | uint64 valid after |
| 102 | uint64 valid before |
| 103 | string critical options |
| 104 | string extensions |
| 105 | string reserved |
| 106 | string signature key |
| 107 | string signature |
| 108 | |
djm@openbsd.org | a62f4e1 | 2019-12-10 23:37:31 +0000 | [diff] [blame] | 109 | and for security key ed25519 certificates: |
| 110 | |
djm@openbsd.org | 1306643 | 2019-11-18 04:34:47 +0000 | [diff] [blame] | 111 | string "sk-ssh-ed25519-cert-v01@openssh.com" |
| 112 | string nonce |
| 113 | string public key |
| 114 | string application |
| 115 | uint64 serial |
| 116 | uint32 type |
| 117 | string key id |
| 118 | string valid principals |
| 119 | uint64 valid after |
| 120 | uint64 valid before |
| 121 | string critical options |
| 122 | string extensions |
| 123 | string reserved |
| 124 | string signature key |
| 125 | string signature |
| 126 | |
djm@openbsd.org | a62f4e1 | 2019-12-10 23:37:31 +0000 | [diff] [blame] | 127 | Both security key certificates use the following encoding for private keys: |
| 128 | |
| 129 | string type (e.g. "sk-ssh-ed25519-cert-v01@openssh.com") |
| 130 | string pubkey (the above key/cert structure) |
| 131 | string application |
| 132 | uint8 flags |
| 133 | string key_handle |
| 134 | string reserved |
| 135 | |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 136 | During key generation, the hardware also returns attestation information |
| 137 | that may be used to cryptographically prove that a given key is |
| 138 | hardware-backed. Unfortunately, the protocol required for this proof is |
| 139 | not privacy-preserving and may be used to identify U2F tokens with at |
| 140 | least manufacturer and batch number granularity. For this reason, we |
| 141 | choose not to include this information in the public key or save it by |
| 142 | default. |
| 143 | |
djm@openbsd.org | 24c0f75 | 2020-01-28 08:01:34 +0000 | [diff] [blame] | 144 | Attestation information is useful for out-of-band key and certificate |
| 145 | registration worksflows, e.g. proving to a CA that a key is backed |
| 146 | by trusted hardware before it will issue a certificate. To support this |
| 147 | case, OpenSSH optionally allows retaining the attestation information |
| 148 | at the time of key generation. It will take the following format: |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 149 | |
djm@openbsd.org | 24c0f75 | 2020-01-28 08:01:34 +0000 | [diff] [blame] | 150 | string "ssh-sk-attest-v00" |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 151 | string attestation certificate |
| 152 | string enrollment signature |
djm@openbsd.org | 24c0f75 | 2020-01-28 08:01:34 +0000 | [diff] [blame] | 153 | uint32 reserved flags |
| 154 | string reserved string |
| 155 | |
| 156 | OpenSSH treats the attestation certificate and enrollment signatures as |
| 157 | opaque objects and does no interpretation of them itself. |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 158 | |
| 159 | SSH U2F signatures |
| 160 | ------------------ |
| 161 | |
djm@openbsd.org | 18e84bf | 2019-11-28 05:20:54 +0000 | [diff] [blame] | 162 | In addition to the message to be signed, the U2F signature operation |
djm@openbsd.org | c4036fe | 2019-12-10 22:36:08 +0000 | [diff] [blame] | 163 | requires the key handle and a few additional parameters. The signature |
| 164 | is signed over a blob that consists of: |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 165 | |
| 166 | byte[32] SHA256(application) |
| 167 | byte flags (including "user present", extensions present) |
| 168 | uint32 counter |
| 169 | byte[] extensions |
| 170 | byte[32] SHA256(message) |
| 171 | |
djm@openbsd.org | c33d468 | 2019-12-11 22:19:47 +0000 | [diff] [blame] | 172 | No extensons are yet defined for SSH use. If any are defined in the future, |
| 173 | it will be possible to infer their presence from the contents of the "flags" |
| 174 | value. |
| 175 | |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 176 | The signature returned from U2F hardware takes the following format: |
| 177 | |
| 178 | byte flags (including "user present") |
| 179 | uint32 counter |
djm@openbsd.org | c4036fe | 2019-12-10 22:36:08 +0000 | [diff] [blame] | 180 | byte[] ecdsa_signature (in X9.62 format). |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 181 | |
| 182 | For use in the SSH protocol, we wish to avoid server-side parsing of ASN.1 |
| 183 | format data in the pre-authentication attack surface. Therefore, the |
| 184 | signature format used on the wire in SSH2_USERAUTH_REQUEST packets will |
djm@openbsd.org | a70d92f | 2019-11-19 22:23:19 +0000 | [diff] [blame] | 185 | be reformatted to better match the existing signature encoding: |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 186 | |
djm@openbsd.org | a70d92f | 2019-11-19 22:23:19 +0000 | [diff] [blame] | 187 | string "sk-ecdsa-sha2-nistp256@openssh.com" |
| 188 | string ecdsa_signature |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 189 | byte flags |
| 190 | uint32 counter |
| 191 | |
djm@openbsd.org | a70d92f | 2019-11-19 22:23:19 +0000 | [diff] [blame] | 192 | Where the "ecdsa_signature" field follows the RFC5656 ECDSA signature |
| 193 | encoding: |
| 194 | |
| 195 | mpint r |
| 196 | mpint s |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 197 | |
markus@openbsd.org | b556cc3 | 2019-11-12 19:34:40 +0000 | [diff] [blame] | 198 | For Ed25519 keys the signature is encoded as: |
| 199 | |
| 200 | string "sk-ssh-ed25519@openssh.com" |
| 201 | string signature |
| 202 | byte flags |
| 203 | uint32 counter |
| 204 | |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 205 | ssh-agent protocol extensions |
| 206 | ----------------------------- |
| 207 | |
naddy@openbsd.org | ad38406 | 2019-11-01 12:10:43 +0000 | [diff] [blame] | 208 | ssh-agent requires a protocol extension to support U2F keys. At |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 209 | present the closest analogue to Security Keys in ssh-agent are PKCS#11 |
| 210 | tokens, insofar as they require a middleware library to communicate with |
| 211 | the device that holds the keys. Unfortunately, the protocol message used |
| 212 | to add PKCS#11 keys to ssh-agent does not include any way to send the |
| 213 | key handle to the agent as U2F keys require. |
| 214 | |
naddy@openbsd.org | ad38406 | 2019-11-01 12:10:43 +0000 | [diff] [blame] | 215 | To avoid this, without having to add wholly new messages to the agent |
| 216 | protocol, we will use the existing SSH2_AGENTC_ADD_ID_CONSTRAINED message |
| 217 | with a new key constraint extension to encode a path to the middleware |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 218 | library for the key. The format of this constraint extension would be: |
| 219 | |
| 220 | byte SSH_AGENT_CONSTRAIN_EXTENSION |
djm@openbsd.org | 22d4beb | 2019-12-10 23:21:56 +0000 | [diff] [blame] | 221 | string sk-provider@openssh.com |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 222 | string middleware path |
| 223 | |
| 224 | This constraint-based approach does not present any compatibility |
| 225 | problems. |
| 226 | |
| 227 | OpenSSH integration |
| 228 | ------------------- |
| 229 | |
| 230 | U2F tokens may be attached via a number of means, including USB and NFC. |
| 231 | The USB interface is standardised around a HID protocol, but we want to |
| 232 | be able to support other transports as well as dummy implementations for |
djm@openbsd.org | 1306643 | 2019-11-18 04:34:47 +0000 | [diff] [blame] | 233 | regress testing. For this reason, OpenSSH shall support a dynamically- |
| 234 | loaded middleware libraries to communicate with security keys, but offer |
| 235 | support for the common case of USB HID security keys internally. |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 236 | |
| 237 | The middleware library need only expose a handful of functions: |
| 238 | |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 239 | #define SSH_SK_VERSION_MAJOR 0x00040000 /* API version */ |
djm@openbsd.org | ef65e7d | 2019-12-30 09:25:29 +0000 | [diff] [blame] | 240 | #define SSH_SK_VERSION_MAJOR_MASK 0xffff0000 |
| 241 | |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 242 | /* Flags */ |
| 243 | #define SSH_SK_USER_PRESENCE_REQD 0x01 |
djm@openbsd.org | 4532bd0 | 2019-12-30 09:19:52 +0000 | [diff] [blame] | 244 | #define SSH_SK_USER_VERIFICATION_REQD 0x04 |
| 245 | #define SSH_SK_RESIDENT_KEY 0x20 |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 246 | |
markus@openbsd.org | fd1a3b5 | 2019-11-12 19:32:30 +0000 | [diff] [blame] | 247 | /* Algs */ |
| 248 | #define SSH_SK_ECDSA 0x00 |
| 249 | #define SSH_SK_ED25519 0x01 |
| 250 | |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 251 | /* Error codes */ |
| 252 | #define SSH_SK_ERR_GENERAL -1 |
| 253 | #define SSH_SK_ERR_UNSUPPORTED -2 |
| 254 | #define SSH_SK_ERR_PIN_REQUIRED -3 |
djm@openbsd.org | 59d01f1 | 2020-01-25 23:13:09 +0000 | [diff] [blame] | 255 | #define SSH_SK_ERR_DEVICE_NOT_FOUND -4 |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 256 | |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 257 | struct sk_enroll_response { |
| 258 | uint8_t *public_key; |
| 259 | size_t public_key_len; |
| 260 | uint8_t *key_handle; |
| 261 | size_t key_handle_len; |
| 262 | uint8_t *signature; |
| 263 | size_t signature_len; |
| 264 | uint8_t *attestation_cert; |
| 265 | size_t attestation_cert_len; |
| 266 | }; |
| 267 | |
| 268 | struct sk_sign_response { |
| 269 | uint8_t flags; |
| 270 | uint32_t counter; |
| 271 | uint8_t *sig_r; |
| 272 | size_t sig_r_len; |
| 273 | uint8_t *sig_s; |
| 274 | size_t sig_s_len; |
| 275 | }; |
| 276 | |
djm@openbsd.org | ef65e7d | 2019-12-30 09:25:29 +0000 | [diff] [blame] | 277 | struct sk_resident_key { |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 278 | uint32_t alg; |
djm@openbsd.org | ef65e7d | 2019-12-30 09:25:29 +0000 | [diff] [blame] | 279 | size_t slot; |
| 280 | char *application; |
| 281 | struct sk_enroll_response key; |
| 282 | }; |
| 283 | |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 284 | struct sk_option { |
| 285 | char *name; |
| 286 | char *value; |
| 287 | uint8_t important; |
| 288 | }; |
| 289 | |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 290 | /* Return the version of the middleware API */ |
| 291 | uint32_t sk_api_version(void); |
| 292 | |
| 293 | /* Enroll a U2F key (private key generation) */ |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 294 | int sk_enroll(uint32_t alg, |
| 295 | const uint8_t *challenge, size_t challenge_len, |
djm@openbsd.org | ef65e7d | 2019-12-30 09:25:29 +0000 | [diff] [blame] | 296 | const char *application, uint8_t flags, const char *pin, |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 297 | struct sk_option **options, |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 298 | struct sk_enroll_response **enroll_response); |
| 299 | |
| 300 | /* Sign a challenge */ |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 301 | int sk_sign(uint32_t alg, const uint8_t *message, size_t message_len, |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 302 | const char *application, |
| 303 | const uint8_t *key_handle, size_t key_handle_len, |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 304 | uint8_t flags, const char *pin, struct sk_option **options, |
djm@openbsd.org | ef65e7d | 2019-12-30 09:25:29 +0000 | [diff] [blame] | 305 | struct sk_sign_response **sign_response); |
| 306 | |
| 307 | /* Enumerate all resident keys */ |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 308 | int sk_load_resident_keys(const char *pin, struct sk_option **options, |
djm@openbsd.org | ef65e7d | 2019-12-30 09:25:29 +0000 | [diff] [blame] | 309 | struct sk_resident_key ***rks, size_t *nrks); |
| 310 | |
| 311 | The SSH_SK_VERSION_MAJOR should be incremented for each incompatible |
| 312 | API change. |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 313 | |
djm@openbsd.org | c312ca0 | 2020-01-06 02:00:46 +0000 | [diff] [blame] | 314 | The options may be used to pass miscellaneous options to the middleware |
| 315 | as a NULL-terminated array of pointers to struct sk_option. The middleware |
| 316 | may ignore unsupported or unknown options unless the "important" flag is |
| 317 | set, in which case it should return failure if an unsupported option is |
| 318 | requested. |
| 319 | |
| 320 | At present the following options names are supported: |
| 321 | |
| 322 | "device" |
| 323 | |
| 324 | Specifies a specific FIDO device on which to perform the |
| 325 | operation. The value in this field is interpreted by the |
| 326 | middleware but it would be typical to specify a path to |
| 327 | a /dev node for the device in question. |
| 328 | |
| 329 | "user" |
| 330 | |
| 331 | Specifies the FIDO2 username used when enrolling a key, |
| 332 | overriding OpenSSH's default of using an all-zero username. |
| 333 | |
| 334 | In OpenSSH, the middleware will be invoked by using a similar mechanism to |
djm@openbsd.org | 18e84bf | 2019-11-28 05:20:54 +0000 | [diff] [blame] | 335 | ssh-pkcs11-helper to provide address-space containment of the |
| 336 | middleware from ssh-agent. |
djm@openbsd.org | 57ecc10 | 2019-10-31 21:14:17 +0000 | [diff] [blame] | 337 | |