mirror of https://gitee.com/openkylin/openssh.git
338 lines
11 KiB
Plaintext
338 lines
11 KiB
Plaintext
|
This document describes OpenSSH's support for U2F/FIDO security keys.
|
||
|
|
||
|
Background
|
||
|
----------
|
||
|
|
||
|
U2F is an open standard for two-factor authentication hardware, widely
|
||
|
used for user authentication to websites. U2F tokens are ubiquitous,
|
||
|
available from a number of manufacturers and are currently by far the
|
||
|
cheapest way for users to achieve hardware-backed credential storage.
|
||
|
|
||
|
The U2F protocol however cannot be trivially used as an SSH protocol key
|
||
|
type as both the inputs to the signature operation and the resultant
|
||
|
signature differ from those specified for SSH. For similar reasons,
|
||
|
integration of U2F devices cannot be achieved via the PKCS#11 API.
|
||
|
|
||
|
U2F also offers a number of features that are attractive in the context
|
||
|
of SSH authentication. They can be configured to require indication
|
||
|
of "user presence" for each signature operation (typically achieved
|
||
|
by requiring the user touch the key). They also offer an attestation
|
||
|
mechanism at key enrollment time that can be used to prove that a
|
||
|
given key is backed by hardware. Finally the signature format includes
|
||
|
a monotonic signature counter that can be used (at scale) to detect
|
||
|
concurrent use of a private key, should it be extracted from hardware.
|
||
|
|
||
|
U2F private keys are generated through an enrollment operation,
|
||
|
which takes an application ID - a URL-like string, typically "ssh:"
|
||
|
in this case, but a HTTP origin for the case of web authentication,
|
||
|
and a challenge string (typically randomly generated). The enrollment
|
||
|
operation returns a public key, a key handle that must be used to invoke
|
||
|
the hardware-backed private key, some flags and signed attestation
|
||
|
information that may be used to verify that a private key is hosted on a
|
||
|
particular hardware instance.
|
||
|
|
||
|
It is common for U2F hardware to derive private keys from the key handle
|
||
|
in conjunction with a small per-device secret that is unique to the
|
||
|
hardware, thus requiring little on-device storage for an effectively
|
||
|
unlimited number of supported keys. This drives the requirement that
|
||
|
the key handle be supplied for each signature operation. U2F tokens
|
||
|
primarily use ECDSA signatures in the NIST-P256 field, though the FIDO2
|
||
|
standard specifies additional key types, including one based on Ed25519.
|
||
|
|
||
|
SSH U2F Key formats
|
||
|
-------------------
|
||
|
|
||
|
OpenSSH integrates U2F as new key and corresponding certificate types:
|
||
|
|
||
|
sk-ecdsa-sha2-nistp256@openssh.com
|
||
|
sk-ecdsa-sha2-nistp256-cert-v01@openssh.com
|
||
|
sk-ssh-ed25519@openssh.com
|
||
|
sk-ssh-ed25519-cert-v01@openssh.com
|
||
|
|
||
|
While each uses ecdsa-sha256-nistp256 as the underlying signature primitive,
|
||
|
keys require extra information in the public and private keys, and in
|
||
|
the signature object itself. As such they cannot be made compatible with
|
||
|
the existing ecdsa-sha2-nistp* key types.
|
||
|
|
||
|
The format of a sk-ecdsa-sha2-nistp256@openssh.com public key is:
|
||
|
|
||
|
string "sk-ecdsa-sha2-nistp256@openssh.com"
|
||
|
string curve name
|
||
|
ec_point Q
|
||
|
string application (user-specified, but typically "ssh:")
|
||
|
|
||
|
The corresponding private key contains:
|
||
|
|
||
|
string "sk-ecdsa-sha2-nistp256@openssh.com"
|
||
|
string curve name
|
||
|
ec_point Q
|
||
|
string application (user-specified, but typically "ssh:")
|
||
|
uint8 flags
|
||
|
string key_handle
|
||
|
string reserved
|
||
|
|
||
|
The format of a sk-ssh-ed25519@openssh.com public key is:
|
||
|
|
||
|
string "sk-ssh-ed25519@openssh.com"
|
||
|
string public key
|
||
|
string application (user-specified, but typically "ssh:")
|
||
|
|
||
|
With a private half consisting of:
|
||
|
|
||
|
string "sk-ssh-ed25519@openssh.com"
|
||
|
string public key
|
||
|
string application (user-specified, but typically "ssh:")
|
||
|
uint8 flags
|
||
|
string key_handle
|
||
|
string reserved
|
||
|
|
||
|
The certificate form for SSH U2F keys appends the usual certificate
|
||
|
information to the public key:
|
||
|
|
||
|
string "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com"
|
||
|
string nonce
|
||
|
string curve name
|
||
|
ec_point Q
|
||
|
string application
|
||
|
uint64 serial
|
||
|
uint32 type
|
||
|
string key id
|
||
|
string valid principals
|
||
|
uint64 valid after
|
||
|
uint64 valid before
|
||
|
string critical options
|
||
|
string extensions
|
||
|
string reserved
|
||
|
string signature key
|
||
|
string signature
|
||
|
|
||
|
and for security key ed25519 certificates:
|
||
|
|
||
|
string "sk-ssh-ed25519-cert-v01@openssh.com"
|
||
|
string nonce
|
||
|
string public key
|
||
|
string application
|
||
|
uint64 serial
|
||
|
uint32 type
|
||
|
string key id
|
||
|
string valid principals
|
||
|
uint64 valid after
|
||
|
uint64 valid before
|
||
|
string critical options
|
||
|
string extensions
|
||
|
string reserved
|
||
|
string signature key
|
||
|
string signature
|
||
|
|
||
|
Both security key certificates use the following encoding for private keys:
|
||
|
|
||
|
string type (e.g. "sk-ssh-ed25519-cert-v01@openssh.com")
|
||
|
string pubkey (the above key/cert structure)
|
||
|
string application
|
||
|
uint8 flags
|
||
|
string key_handle
|
||
|
string reserved
|
||
|
|
||
|
During key generation, the hardware also returns attestation information
|
||
|
that may be used to cryptographically prove that a given key is
|
||
|
hardware-backed. Unfortunately, the protocol required for this proof is
|
||
|
not privacy-preserving and may be used to identify U2F tokens with at
|
||
|
least manufacturer and batch number granularity. For this reason, we
|
||
|
choose not to include this information in the public key or save it by
|
||
|
default.
|
||
|
|
||
|
Attestation information is useful for out-of-band key and certificate
|
||
|
registration worksflows, e.g. proving to a CA that a key is backed
|
||
|
by trusted hardware before it will issue a certificate. To support this
|
||
|
case, OpenSSH optionally allows retaining the attestation information
|
||
|
at the time of key generation. It will take the following format:
|
||
|
|
||
|
string "ssh-sk-attest-v00"
|
||
|
string attestation certificate
|
||
|
string enrollment signature
|
||
|
uint32 reserved flags
|
||
|
string reserved string
|
||
|
|
||
|
OpenSSH treats the attestation certificate and enrollment signatures as
|
||
|
opaque objects and does no interpretation of them itself.
|
||
|
|
||
|
SSH U2F signatures
|
||
|
------------------
|
||
|
|
||
|
In addition to the message to be signed, the U2F signature operation
|
||
|
requires the key handle and a few additional parameters. The signature
|
||
|
is signed over a blob that consists of:
|
||
|
|
||
|
byte[32] SHA256(application)
|
||
|
byte flags (including "user present", extensions present)
|
||
|
uint32 counter
|
||
|
byte[] extensions
|
||
|
byte[32] SHA256(message)
|
||
|
|
||
|
No extensons are yet defined for SSH use. If any are defined in the future,
|
||
|
it will be possible to infer their presence from the contents of the "flags"
|
||
|
value.
|
||
|
|
||
|
The signature returned from U2F hardware takes the following format:
|
||
|
|
||
|
byte flags (including "user present")
|
||
|
uint32 counter
|
||
|
byte[] ecdsa_signature (in X9.62 format).
|
||
|
|
||
|
For use in the SSH protocol, we wish to avoid server-side parsing of ASN.1
|
||
|
format data in the pre-authentication attack surface. Therefore, the
|
||
|
signature format used on the wire in SSH2_USERAUTH_REQUEST packets will
|
||
|
be reformatted to better match the existing signature encoding:
|
||
|
|
||
|
string "sk-ecdsa-sha2-nistp256@openssh.com"
|
||
|
string ecdsa_signature
|
||
|
byte flags
|
||
|
uint32 counter
|
||
|
|
||
|
Where the "ecdsa_signature" field follows the RFC5656 ECDSA signature
|
||
|
encoding:
|
||
|
|
||
|
mpint r
|
||
|
mpint s
|
||
|
|
||
|
For Ed25519 keys the signature is encoded as:
|
||
|
|
||
|
string "sk-ssh-ed25519@openssh.com"
|
||
|
string signature
|
||
|
byte flags
|
||
|
uint32 counter
|
||
|
|
||
|
ssh-agent protocol extensions
|
||
|
-----------------------------
|
||
|
|
||
|
ssh-agent requires a protocol extension to support U2F keys. At
|
||
|
present the closest analogue to Security Keys in ssh-agent are PKCS#11
|
||
|
tokens, insofar as they require a middleware library to communicate with
|
||
|
the device that holds the keys. Unfortunately, the protocol message used
|
||
|
to add PKCS#11 keys to ssh-agent does not include any way to send the
|
||
|
key handle to the agent as U2F keys require.
|
||
|
|
||
|
To avoid this, without having to add wholly new messages to the agent
|
||
|
protocol, we will use the existing SSH2_AGENTC_ADD_ID_CONSTRAINED message
|
||
|
with a new key constraint extension to encode a path to the middleware
|
||
|
library for the key. The format of this constraint extension would be:
|
||
|
|
||
|
byte SSH_AGENT_CONSTRAIN_EXTENSION
|
||
|
string sk-provider@openssh.com
|
||
|
string middleware path
|
||
|
|
||
|
This constraint-based approach does not present any compatibility
|
||
|
problems.
|
||
|
|
||
|
OpenSSH integration
|
||
|
-------------------
|
||
|
|
||
|
U2F tokens may be attached via a number of means, including USB and NFC.
|
||
|
The USB interface is standardised around a HID protocol, but we want to
|
||
|
be able to support other transports as well as dummy implementations for
|
||
|
regress testing. For this reason, OpenSSH shall support a dynamically-
|
||
|
loaded middleware libraries to communicate with security keys, but offer
|
||
|
support for the common case of USB HID security keys internally.
|
||
|
|
||
|
The middleware library need only expose a handful of functions:
|
||
|
|
||
|
#define SSH_SK_VERSION_MAJOR 0x00040000 /* API version */
|
||
|
#define SSH_SK_VERSION_MAJOR_MASK 0xffff0000
|
||
|
|
||
|
/* Flags */
|
||
|
#define SSH_SK_USER_PRESENCE_REQD 0x01
|
||
|
#define SSH_SK_USER_VERIFICATION_REQD 0x04
|
||
|
#define SSH_SK_RESIDENT_KEY 0x20
|
||
|
|
||
|
/* Algs */
|
||
|
#define SSH_SK_ECDSA 0x00
|
||
|
#define SSH_SK_ED25519 0x01
|
||
|
|
||
|
/* Error codes */
|
||
|
#define SSH_SK_ERR_GENERAL -1
|
||
|
#define SSH_SK_ERR_UNSUPPORTED -2
|
||
|
#define SSH_SK_ERR_PIN_REQUIRED -3
|
||
|
#define SSH_SK_ERR_DEVICE_NOT_FOUND -4
|
||
|
|
||
|
struct sk_enroll_response {
|
||
|
uint8_t *public_key;
|
||
|
size_t public_key_len;
|
||
|
uint8_t *key_handle;
|
||
|
size_t key_handle_len;
|
||
|
uint8_t *signature;
|
||
|
size_t signature_len;
|
||
|
uint8_t *attestation_cert;
|
||
|
size_t attestation_cert_len;
|
||
|
};
|
||
|
|
||
|
struct sk_sign_response {
|
||
|
uint8_t flags;
|
||
|
uint32_t counter;
|
||
|
uint8_t *sig_r;
|
||
|
size_t sig_r_len;
|
||
|
uint8_t *sig_s;
|
||
|
size_t sig_s_len;
|
||
|
};
|
||
|
|
||
|
struct sk_resident_key {
|
||
|
uint32_t alg;
|
||
|
size_t slot;
|
||
|
char *application;
|
||
|
struct sk_enroll_response key;
|
||
|
};
|
||
|
|
||
|
struct sk_option {
|
||
|
char *name;
|
||
|
char *value;
|
||
|
uint8_t important;
|
||
|
};
|
||
|
|
||
|
/* Return the version of the middleware API */
|
||
|
uint32_t sk_api_version(void);
|
||
|
|
||
|
/* Enroll a U2F key (private key generation) */
|
||
|
int sk_enroll(uint32_t alg,
|
||
|
const uint8_t *challenge, size_t challenge_len,
|
||
|
const char *application, uint8_t flags, const char *pin,
|
||
|
struct sk_option **options,
|
||
|
struct sk_enroll_response **enroll_response);
|
||
|
|
||
|
/* Sign a challenge */
|
||
|
int sk_sign(uint32_t alg, const uint8_t *message, size_t message_len,
|
||
|
const char *application,
|
||
|
const uint8_t *key_handle, size_t key_handle_len,
|
||
|
uint8_t flags, const char *pin, struct sk_option **options,
|
||
|
struct sk_sign_response **sign_response);
|
||
|
|
||
|
/* Enumerate all resident keys */
|
||
|
int sk_load_resident_keys(const char *pin, struct sk_option **options,
|
||
|
struct sk_resident_key ***rks, size_t *nrks);
|
||
|
|
||
|
The SSH_SK_VERSION_MAJOR should be incremented for each incompatible
|
||
|
API change.
|
||
|
|
||
|
The options may be used to pass miscellaneous options to the middleware
|
||
|
as a NULL-terminated array of pointers to struct sk_option. The middleware
|
||
|
may ignore unsupported or unknown options unless the "important" flag is
|
||
|
set, in which case it should return failure if an unsupported option is
|
||
|
requested.
|
||
|
|
||
|
At present the following options names are supported:
|
||
|
|
||
|
"device"
|
||
|
|
||
|
Specifies a specific FIDO device on which to perform the
|
||
|
operation. The value in this field is interpreted by the
|
||
|
middleware but it would be typical to specify a path to
|
||
|
a /dev node for the device in question.
|
||
|
|
||
|
"user"
|
||
|
|
||
|
Specifies the FIDO2 username used when enrolling a key,
|
||
|
overriding OpenSSH's default of using an all-zero username.
|
||
|
|
||
|
In OpenSSH, the middleware will be invoked by using a similar mechanism to
|
||
|
ssh-pkcs11-helper to provide address-space containment of the
|
||
|
middleware from ssh-agent.
|
||
|
|