1 //! U2F implementation.
3 use std
::mem
::MaybeUninit
;
6 use anyhow
::{bail, format_err, Error}
;
7 use openssl
::ec
::{EcGroup, EcKey, EcPoint}
;
8 use openssl
::ecdsa
::EcdsaSig
;
9 use openssl
::pkey
::Public
;
11 use openssl
::x509
::X509
;
12 use serde
::{Deserialize, Serialize}
;
14 const CHALLENGE_LEN
: usize = 32;
15 const U2F_VERSION
: &str = "U2F_V2";
17 /// The "key" part of a registration, passed to `u2f.sign` in the registered keys list.
19 /// Part of the U2F API, therefore `camelCase` and base64url without padding.
20 #[derive(Clone, Debug, Deserialize, Serialize)]
21 #[serde(rename_all = "camelCase")]
22 pub struct RegisteredKey
{
23 /// Identifies the key handle on the client side. Used to create authentication challenges, so
24 /// the client knows which key to use. Must be remembered.
25 #[serde(with = "bytes_as_base64url_nopad")]
26 pub key_handle
: Vec
<u8>,
31 /// Data we get when a u2f token responds to a registration challenge.
32 #[derive(Clone, Debug, Deserialize, Serialize)]
33 #[serde(rename_all = "kebab-case")]
34 pub struct Registration
{
35 /// The key consisting of key handle and version, which can be passed to the registered-keys
36 /// list in `u2f.sign` in the browser.
37 pub key
: RegisteredKey
,
39 /// Public part of the client key identified via the `key_handle`. Required to verify future
40 /// authentication responses. Must be remembered.
41 #[serde(with = "bytes_as_base64")]
42 pub public_key
: Vec
<u8>,
44 /// Attestation certificate (in DER format) from which we originally copied the `key_handle`.
45 /// Not necessary for authentication, unless the hardware tokens should be restricted to
46 /// specific provider identities. Optional.
48 with
= "bytes_as_base64",
50 skip_serializing_if
= "Vec::is_empty"
52 pub certificate
: Vec
<u8>,
55 /// Result from a successful authentication. The client's hardware token will inform us about the
56 /// user-presence (it may have been configured to respond automatically instead of requiring user
57 /// interaction), and the number of authentications the key has performed.
58 /// We probably won't make much use of this.
59 #[derive(Clone, Debug)]
60 pub struct Authentication
{
61 /// `true` if the user had to be present.
62 pub user_present
: bool
,
64 /// authentication count
68 /// The hardware replies with a client data json object containing some information - this is the
69 /// subset we actually make use of.
70 #[derive(Deserialize)]
71 #[serde(rename_all = "camelCase")]
73 /// The challenge the device responded to. This should be compared against the server side
77 /// The origin the the browser told the device the challenge was coming from.
81 /// A registration challenge to be sent to the `u2f.register` function in the browser.
83 /// Part of the U2F API, therefore `camelCase`.
84 #[derive(Clone, Debug, Deserialize, Serialize)]
85 #[serde(rename_all = "camelCase")]
86 pub struct RegistrationChallenge
{
87 pub challenge
: String
,
92 /// The response we get from a successful call to the `u2f.register` function in the browser.
94 /// Part of the U2F API, therefore `camelCase`.
95 #[derive(Clone, Debug, Deserialize)]
96 #[serde(rename_all = "camelCase")]
97 pub struct RegistrationResponse
{
98 registration_data
: String
,
103 /// Authentication challenge data to be sent to the `u2f.sign` function in the browser. Does not
104 /// include the registered keys.
106 /// Part of the U2F API, therefore `camelCase`.
107 #[derive(Clone, Debug, Deserialize, Serialize)]
108 #[serde(rename_all = "camelCase")]
109 pub struct AuthChallenge
{
110 pub challenge
: String
,
114 /// The response we get from a successful call to the `u2f.sign` function in the browser.
116 /// Part of the U2F API, therefore `camelCase` and base64url without padding.
117 #[derive(Deserialize)]
118 #[serde(rename_all = "camelCase")]
119 pub struct AuthResponse
{
120 #[serde(with = "bytes_as_base64url_nopad")]
121 pub key_handle
: Vec
<u8>,
122 pub client_data
: String
,
123 pub signature_data
: String
,
127 pub fn key_handle(&self) -> &[u8] {
132 /// A u2f context to create or verify challenges with.
133 #[derive(Deserialize, Serialize)]
140 /// Create a new U2F context consisting of an appid and origin.
141 pub fn new(app_id
: String
, origin
: String
) -> Self {
142 Self { app_id, origin }
145 /// Get a challenge object which can be directly passed to `u2f.register` on the browser side.
146 pub fn registration_challenge(&self) -> Result
<RegistrationChallenge
, Error
> {
147 Ok(RegistrationChallenge
{
148 challenge
: challenge()?
,
149 version
: U2F_VERSION
.to_owned(),
150 app_id
: self.app_id
.clone(),
154 /// Convenience method to verify the json formatted response object string.
155 pub fn registration_verify(
159 ) -> Result
<Option
<Registration
>, Error
> {
160 let response
: RegistrationResponse
= serde_json
::from_str(response
)
161 .map_err(|err
| format_err
!("error parsing response: {}", err
))?
;
162 self.registration_verify_obj(challenge
, response
)
165 /// Verifies the registration response object.
166 pub fn registration_verify_obj(
169 response
: RegistrationResponse
,
170 ) -> Result
<Option
<Registration
>, Error
> {
171 let client_data_decoded
= decode(&response
.client_data
)
172 .map_err(|err
| format_err
!("error decoding client data in response: {}", err
))?
;
174 let client_data
: ClientData
= serde_json
::from_reader(&mut &client_data_decoded
[..])
175 .map_err(|err
| format_err
!("error parsing client data: {}", err
))?
;
177 if client_data
.challenge
!= challenge
{
178 bail
!("registration challenge did not match");
181 if client_data
.origin
!= self.origin
{
183 "origin in client registration did not match: {:?} != {:?}",
189 let registration_data
= decode(&response
.registration_data
)
190 .map_err(|err
| format_err
!("error decoding registration data in response: {}", err
))?
;
192 let registration_data
= RegistrationResponseData
::from_raw(®istration_data
)?
;
194 let mut digest
= sha
::Sha256
::new();
195 digest
.update(&[0u8]);
196 digest
.update(&sha
::sha256(self.app_id
.as_bytes()));
197 digest
.update(&sha
::sha256(&client_data_decoded
));
198 digest
.update(registration_data
.key_handle
);
199 digest
.update(registration_data
.public_key
);
200 let digest
= digest
.finish();
202 let signature
= EcdsaSig
::from_der(registration_data
.signature
)
203 .map_err(|err
| format_err
!("error decoding signature in response: {}", err
))?
;
205 // can we decode the public key?
206 drop(decode_public_key(registration_data
.public_key
)?
);
208 match signature
.verify(&digest
, ®istration_data
.cert_key
) {
209 Ok(true) => Ok(Some(Registration
{
211 key_handle
: registration_data
.key_handle
.to_vec(),
212 version
: response
.version
,
214 public_key
: registration_data
.public_key
.to_vec(),
215 certificate
: registration_data
.certificate
.to_vec(),
217 Ok(false) => Ok(None
),
218 Err(err
) => bail
!("openssl error while verifying signature: {}", err
),
222 /// Get a challenge object which can be directly passwd to `u2f.sign` on the browser side.
223 pub fn auth_challenge(&self) -> Result
<AuthChallenge
, Error
> {
225 challenge
: challenge()?
,
226 app_id
: self.app_id
.clone(),
230 /// Convenience method to verify the json formatted response object string.
236 ) -> Result
<Option
<Authentication
>, Error
> {
237 let response
: AuthResponse
= serde_json
::from_str(response
)
238 .map_err(|err
| format_err
!("error parsing response: {}", err
))?
;
239 self.auth_verify_obj(public_key
, challenge
, response
)
242 /// Verifies the authentication response object.
243 pub fn auth_verify_obj(
247 response
: AuthResponse
,
248 ) -> Result
<Option
<Authentication
>, Error
> {
249 let client_data_decoded
= decode(&response
.client_data
)
250 .map_err(|err
| format_err
!("error decoding client data in response: {}", err
))?
;
252 let client_data
: ClientData
= serde_json
::from_reader(&mut &client_data_decoded
[..])
253 .map_err(|err
| format_err
!("error parsing client data: {}", err
))?
;
255 if client_data
.challenge
!= challenge
{
256 bail
!("authentication challenge did not match");
259 if client_data
.origin
!= self.origin
{
261 "origin in client authentication did not match: {:?} != {:?}",
267 let signature_data
= decode(&response
.signature_data
)
268 .map_err(|err
| format_err
!("error decoding signature data in response: {}", err
))?
;
270 // an ecdsa signature is much longer than 16 bytes but we only need to parse the first 5
272 if signature_data
.len() < 1 + 4 + 16 {
273 bail
!("invalid signature data");
276 let presence_and_counter_bytes
= &signature_data
[0..5];
277 let user_present
= presence_and_counter_bytes
[0] != 0;
278 let counter_bytes
= &presence_and_counter_bytes
[1..];
280 u32::from_be(unsafe { std::ptr::read_unaligned(counter_bytes.as_ptr() as *const u32) }
);
281 let signature
= EcdsaSig
::from_der(&signature_data
[5..])
282 .map_err(|err
| format_err
!("error decoding signature in response: {}", err
))?
;
284 let public_key
= decode_public_key(public_key
)?
;
286 let mut digest
= sha
::Sha256
::new();
287 digest
.update(&sha
::sha256(self.app_id
.as_bytes()));
288 digest
.update(presence_and_counter_bytes
);
289 digest
.update(&sha
::sha256(&client_data_decoded
));
290 let digest
= digest
.finish();
292 match signature
.verify(&digest
, &public_key
) {
293 Ok(true) => Ok(Some(Authentication
{
295 counter
: counter
as usize,
297 Ok(false) => Ok(None
),
298 Err(err
) => bail
!("openssl error while verifying signature: {}", err
),
303 /// base64url encoding
304 fn encode(data
: &[u8]) -> String
{
305 let mut out
= base64
::encode_config(data
, base64
::URL_SAFE_NO_PAD
);
306 while out
.ends_with('
='
) {
312 /// base64url decoding
313 fn decode(data
: &str) -> Result
<Vec
<u8>, Error
> {
314 Ok(base64
::decode_config(data
, base64
::URL_SAFE_NO_PAD
)?
)
317 /// produce a challenge, which is just a bunch of random data
318 fn challenge() -> Result
<String
, Error
> {
319 let mut data
= MaybeUninit
::<[u8; CHALLENGE_LEN
]>::uninit();
321 let buf
: &mut [u8; CHALLENGE_LEN
] = &mut *data
.as_mut_ptr();
322 let rc
= libc
::getrandom(buf
.as_mut_ptr() as *mut libc
::c_void
, buf
.len(), 0);
324 return Err(io
::Error
::last_os_error().into());
326 if rc
as usize != buf
.len() {
327 // `CHALLENGE_LEN` is small, so short reads cannot happen (see `getrandom(2)`)
328 bail
!("short getrandom call");
335 /// Used while parsing the binary registration response. The slices point directly into the
336 /// original response byte data, and the public key is extracted from the contained X509
339 pub struct RegistrationResponseData
<'a
> {
340 public_key
: &'a
[u8],
341 key_handle
: &'a
[u8],
342 certificate
: &'a
[u8],
345 /// The client's public key in decoded and parsed form.
346 cert_key
: EcKey
<Public
>,
349 impl<'a
> RegistrationResponseData
<'a
> {
350 /// Parse the binary registration data into its parts and extract the certificate's public key.
352 /// See https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html
353 pub fn from_raw(data
: &'a
[u8]) -> Result
<Self, Error
> {
354 // [ 0x05 | 65b pubkey | 1b keyhandle len | keyhandle | certificate (1 DER obj) | signature ]
356 if data
.len() <= (1 + 65 + 1 + 71) {
357 bail
!("registration data too short");
362 "invalid registration data, reserved byte is 0x{:02x}, expected 0x05",
367 let public_key
= &data
[1..66];
369 let key_handle_len
= usize::from(data
[66]);
370 let data
= &data
[67..];
372 if data
.len() <= key_handle_len
+ 71 {
373 bail
!("registration data invalid too short");
376 let key_handle
= &data
[..key_handle_len
];
377 let data
= &data
[key_handle_len
..];
379 bail
!("error decoding X509 certificate: not a SEQUENCE tag");
381 let cert_len
= der_length(&data
[1..])?
+ 1; // plus the tag!
382 let certificate
= &data
[..cert_len
];
383 let x509
= X509
::from_der(certificate
)
384 .map_err(|err
| format_err
!("error decoding X509 certificate: {}", err
))?
;
385 let signature
= &data
[cert_len
..];
392 cert_key
: x509
.public_key()?
.ec_key()?
,
397 /// Decode the raw 65 byte ec public key into an `openssl::EcKey<Public>`.
398 fn decode_public_key(data
: &[u8]) -> Result
<EcKey
<Public
>, Error
> {
399 if data
.len() != 65 {
400 bail
!("invalid public key length {}, expected 65", data
.len());
403 let group
= EcGroup
::from_curve_name(openssl
::nid
::Nid
::X9_62_PRIME256V1
)
404 .map_err(|err
| format_err
!("openssl error, failed to instantiate ec curve: {}", err
))?
;
406 let mut bn
= openssl
::bn
::BigNumContext
::new().map_err(|err
| {
408 "openssl error, failed to instantiate bignum context: {}",
413 let point
= EcPoint
::from_bytes(&group
, data
, &mut bn
)
414 .map_err(|err
| format_err
!("failed to decode public key point: {}", err
))?
;
416 let key
= EcKey
::from_public_key(&group
, &point
)
417 .map_err(|err
| format_err
!("failed to instantiate public key: {}", err
))?
;
420 .map_err(|err
| format_err
!("public key failed self check: {}", err
))?
;
425 /// The only DER thing we need: lengths.
427 /// Returns the length *including* the size of the length itself.
428 fn der_length(data
: &[u8]) -> Result
<usize, Error
> {
430 bail
!("error decoding X509 certificate: bad length (0)");
434 return Ok(usize::from(data
[0]) + 1);
437 let count
= usize::from(data
[0] & 0x7F);
439 // X.609; 8.1.3.5, the value `1111111` shall not be used
440 bail
!("error decoding X509 certificate: illegal length value");
444 // "indefinite" form not allowed in DER
445 bail
!("error decoding X509 certificate: illegal length form");
448 if count
> std
::mem
::size_of
::<usize>() {
449 bail
!("error decoding X509 certificate: unsupported length");
452 if count
> (data
.len() - 1) {
453 bail
!("error decoding X509 certificate: truncated length data");
458 len
= (len
<< 8) | usize::from(data
[1 + i
]);
466 // The test data in here is generated with a yubi key...
468 use serde
::Deserialize
;
470 const TEST_APPID
: &str = "https://u2ftest.enonet.errno.eu";
472 const TEST_REGISTRATION_JSON
: &str =
473 "{\"challenge\":\"mZoWLngnAh8p98nPkFOIBXecd0CbmgEx5tEd5jNswgY\",\"response\":{\"client\
474 Data\":\"eyJjaGFsbGVuZ2UiOiJtWm9XTG5nbkFoOHA5OG5Qa0ZPSUJYZWNkMENibWdFeDV0RWQ1ak5zd2dZI\
475 iwib3JpZ2luIjoiaHR0cHM6Ly91MmZ0ZXN0LmVub25ldC5lcnJuby5ldSIsInR5cCI6Im5hdmlnYXRvci5pZC5\
476 maW5pc2hFbnJvbGxtZW50In0\",\"registrationData\":\"BQR_9TmMowVeoAHp3ABljCa90eNG87t76D4W\
477 c9nsmK9ihNhhYNxYIq9tnRUPTBZ2X4kZKSB0LXMm32lOKQlNB56QQHlt81cRBfID7BvHk_XIJZc5ks5D3R1ZV1\
478 1fJudp3F-ii_KSdZaFb4cGaq0rEaVDfNR2ZR0T0ApMMCeTIaDAJRQwggJEMIIBLqADAgECAgRVYr6gMAsGCSqG\
479 SIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MD\
480 EwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTQzMjUz\
481 NDY4ODBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEszH3c9gUS5mVy-RYVRfhdYOqR2I2lcvoWsSCyAGfLJuU\
482 Z64EWw5m8TGy6jJDyR_aYC4xjz_F2NKnq65yvRQwmjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0\
483 ODIuMS41MBMGCysGAQQBguUcAgEBBAQDAgUgMAsGCSqGSIb3DQEBCwOCAQEArBbZs262s6m3bXWUs09Z9Pc-28\
484 n96yk162tFHKv0HSXT5xYU10cmBMpypXjjI-23YARoXwXn0bm-BdtulED6xc_JMqbK-uhSmXcu2wJ4ICA81BQd\
485 PutvaizpnjlXgDJjq6uNbsSAp98IStLLp7fW13yUw-vAsWb5YFfK9f46Yx6iakM3YqNvvs9M9EUJYl_VrxBJqn\
486 yLx2iaZlnpr13o8NcsKIJRdMUOBqt_ageQg3ttsyq_3LyoNcu7CQ7x8NmeCGm_6eVnZMQjDmwFdymwEN4OxfnM\
487 5MkcKCYhjqgIGruWkVHsFnJa8qjZXneVvKoiepuUQyDEJ2GcqvhU2YKY1zBFAiEA2mcfAS2XRcWy1lLJikFHGJ\
488 SbtOrrwswjOKEzwp6EonkCIFBxbLAmwUnblAWOVELASi610ZfPK-7qx2VwkWfHqnll\",\"version\":\"U2F\
491 const TEST_AUTH_JSON
: &str =
492 "{\"challenge\":\"8LE_-7Rd1vB3Otn3vJ7GyiwRQtYPMv-BWliCejH0d4Y\",\"response\":{\"clientD\
493 ata\":\"eyJjaGFsbGVuZ2UiOiI4TEVfLTdSZDF2QjNPdG4zdko3R3lpd1JRdFlQTXYtQldsaUNlakgwZDRZIiw\
494 ib3JpZ2luIjoiaHR0cHM6Ly91MmZ0ZXN0LmVub25ldC5lcnJuby5ldSIsInR5cCI6Im5hdmlnYXRvci5pZC5nZX\
495 RBc3NlcnRpb24ifQ\",\"keyHandle\":\"eW3zVxEF8gPsG8eT9cgllzmSzkPdHVlXXV8m52ncX6KL8pJ1loVv\
496 hwZqrSsRpUN81HZlHRPQCkwwJ5MhoMAlFA\",\"signatureData\":\"AQAAAQEwRAIgKdM9cmCLZDxntY-dT_\
497 OXbcVA1D5ewQunXVC-CYZ65pUCIAIOUBsu-dOmTym0ITZt6x75BFUSGlqYRuH5JKBcyO3M\"},\"user\":{\"c\
498 ertificate\":\"MIICRDCCAS6gAwIBAgIEVWK+oDALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUy\
499 RiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDA\
500 mBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE0MzI1MzQ2ODgwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAA\
501 RLMx93PYFEuZlcvkWFUX4XWDqkdiNpXL6FrEgsgBnyyblGeuBFsOZvExsuoyQ8kf2mAuMY8/xdjSp6uucr0UMJo\
502 zswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNTATBgsrBgEEAYLlHAIBAQQEAwIFIDALBgkq\
503 hkiG9w0BAQsDggEBAKwW2bNutrOpt211lLNPWfT3PtvJ/espNetrRRyr9B0l0+cWFNdHJgTKcqV44yPtt2AEaF8\
504 F59G5vgXbbpRA+sXPyTKmyvroUpl3LtsCeCAgPNQUHT7rb2os6Z45V4AyY6urjW7EgKffCErSy6e31td8lMPrwL\
505 Fm+WBXyvX+OmMeompDN2Kjb77PTPRFCWJf1a8QSap8i8dommZZ6a9d6PDXLCiCUXTFDgarf2oHkIN7bbMqv9y8q\
506 DXLuwkO8fDZnghpv+nlZ2TEIw5sBXcpsBDeDsX5zOTJHCgmIY6oCBq7lpFR7BZyWvKo2V53lbyqInqblEMgxCdh\
507 nKr4VNmCmNc=\",\"key\":{\"keyHandle\":\"eW3zVxEF8gPsG8eT9cgllzmSzkPdHVlXXV8m52ncX6KL8pJ\
508 1loVvhwZqrSsRpUN81HZlHRPQCkwwJ5MhoMAlFA\",\"version\":\"U2F_V2\"},\"public-key\":\"BH/1\
509 OYyjBV6gAencAGWMJr3R40bzu3voPhZz2eyYr2KE2GFg3Fgir22dFQ9MFnZfiRkpIHQtcybfaU4pCU0HnpA=\"}\
513 fn test_registration() {
514 let data
= TEST_REGISTRATION_JSON
;
516 #[derive(Deserialize)]
517 struct TestChallenge
{
519 response
: super::RegistrationResponse
,
522 let ts
: TestChallenge
=
523 serde_json
::from_str(&data
).expect("failed to parse json test data");
525 let context
= super::U2f
::new(TEST_APPID
.to_string(), TEST_APPID
.to_string());
527 .registration_verify_obj(&ts
.challenge
, ts
.response
)
528 .expect("error trying to verify registration");
531 "test registration signature fails verification"
536 fn test_authentication() {
537 let data
= TEST_AUTH_JSON
;
539 #[derive(Deserialize)]
540 struct TestChallenge
{
542 user
: super::Registration
,
543 response
: super::AuthResponse
,
546 let ts
: TestChallenge
=
547 serde_json
::from_str(&data
).expect("failed to parse json test data");
549 let context
= super::U2f
::new(TEST_APPID
.to_string(), TEST_APPID
.to_string());
551 .auth_verify_obj(&ts
.user
.public_key
, &ts
.challenge
, ts
.response
)
552 .expect("error trying to verify authentication");
555 "test authentication signature fails verification"
560 mod bytes_as_base64
{
561 use serde
::{Deserialize, Deserializer, Serializer}
;
563 pub fn serialize
<S
: Serializer
>(data
: &[u8], serializer
: S
) -> Result
<S
::Ok
, S
::Error
> {
564 serializer
.serialize_str(&base64
::encode(&data
))
567 pub fn deserialize
<'de
, D
: Deserializer
<'de
>>(deserializer
: D
) -> Result
<Vec
<u8>, D
::Error
> {
568 use serde
::de
::Error
;
569 String
::deserialize(deserializer
).and_then(|string
| {
570 base64
::decode(&string
).map_err(|err
| Error
::custom(err
.to_string()))
575 mod bytes_as_base64url_nopad
{
576 use serde
::{Deserialize, Deserializer, Serializer}
;
578 pub fn serialize
<S
: Serializer
>(data
: &[u8], serializer
: S
) -> Result
<S
::Ok
, S
::Error
> {
579 serializer
.serialize_str(&base64
::encode_config(
581 base64
::URL_SAFE_NO_PAD
,
585 pub fn deserialize
<'de
, D
: Deserializer
<'de
>>(deserializer
: D
) -> Result
<Vec
<u8>, D
::Error
> {
586 use serde
::de
::Error
;
587 String
::deserialize(deserializer
).and_then(|string
| {
588 base64
::decode_config(&string
, base64
::URL_SAFE_NO_PAD
)
589 .map_err(|err
| Error
::custom(err
.to_string()))