Initial commit
This commit is contained in:
commit
640023e0f8
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
|
src/bin
|
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "ctap"
|
||||||
|
description = "A Rust implementation of the FIDO2 CTAP protocol"
|
||||||
|
version = "0.1.0"
|
||||||
|
license = "Apache-2.0/MIT"
|
||||||
|
homepage = "https://github.com/ArdaXi/ctap"
|
||||||
|
repository = "https://github.com/ArdaXi/ctap"
|
||||||
|
authors = ["Arda Xi <arda@ardaxi.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.6"
|
||||||
|
failure = "0.1"
|
||||||
|
failure_derive = "0.1"
|
||||||
|
num-traits = "0.2"
|
||||||
|
num-derive = "0.2"
|
||||||
|
byteorder = "1"
|
||||||
|
cbor-codec = "0.7"
|
||||||
|
ring = "0.13"
|
||||||
|
untrusted = "0.6"
|
||||||
|
rust-crypto = "0.2"
|
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
# ctap
|
||||||
|
|
||||||
|
ctap is a library implementing the [FIDO2 CTAP](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html) protocol.
|
||||||
|
|
||||||
|
## Usage example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let devices = ctap::get_devices()?;
|
||||||
|
let device_info = &devices[0];
|
||||||
|
let mut device = ctap::FidoDevice::new(device_info)?;
|
||||||
|
|
||||||
|
// This can be omitted if the FIDO device is not configured with a PIN.
|
||||||
|
let pin = "test";
|
||||||
|
device.unlock(pin)?;
|
||||||
|
|
||||||
|
// In a real application these values would come from the requesting app.
|
||||||
|
let rp_id = "rp_id";
|
||||||
|
let user_id = [0];
|
||||||
|
let user_name = "user_name";
|
||||||
|
let client_data_hash = [0; 32];
|
||||||
|
let cred = device.make_credential(
|
||||||
|
rp_id,
|
||||||
|
&user_id,
|
||||||
|
user_name,
|
||||||
|
&client_data_hash
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// In a real application the credential would be stored and used later.
|
||||||
|
let result = device.get_assertion(&cred, &client_data_hash);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
Currently, this library only supports Linux. Testing and contributions for
|
||||||
|
other platforms is welcome.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
|
||||||
|
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
at your option.
|
||||||
|
|
||||||
|
### Contribution
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally
|
||||||
|
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||||
|
license, shall be dual licensed as above, without any additional terms or
|
||||||
|
conditions.
|
201
src/LICENSE-APACHE
Normal file
201
src/LICENSE-APACHE
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
19
src/LICENSE-MIT
Normal file
19
src/LICENSE-MIT
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
707
src/cbor.rs
Normal file
707
src/cbor.rs
Normal file
@ -0,0 +1,707 @@
|
|||||||
|
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
|
||||||
|
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
|
||||||
|
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||||
|
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
|
// copied, modified, or distributed except according to those terms.
|
||||||
|
use cbor_codec::{Config, Encoder, Decoder, GenericDecoder, GenericEncoder};
|
||||||
|
use cbor_codec::value::Value;
|
||||||
|
use cbor_codec::value;
|
||||||
|
|
||||||
|
use byteorder::{WriteBytesExt, ReadBytesExt, BigEndian, ByteOrder};
|
||||||
|
use failure::ResultExt;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::error::*;
|
||||||
|
|
||||||
|
pub enum Request<'a> {
|
||||||
|
MakeCredential(MakeCredentialRequest<'a>),
|
||||||
|
GetAssertion(GetAssertionRequest<'a>),
|
||||||
|
GetInfo,
|
||||||
|
ClientPin(ClientPinRequest<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Request<'a> {
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, writer: &mut W) -> FidoResult<()> {
|
||||||
|
let mut encoder = Encoder::new(writer);
|
||||||
|
match self {
|
||||||
|
Request::MakeCredential(req) => req.encode(&mut encoder),
|
||||||
|
Request::GetAssertion(req) => req.encode(&mut encoder),
|
||||||
|
Request::GetInfo => {
|
||||||
|
encoder
|
||||||
|
.writer()
|
||||||
|
.write_u8(0x04)
|
||||||
|
.context(FidoErrorKind::CborEncode)
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
Request::ClientPin(req) => req.encode(&mut encoder),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode<R: ReadBytesExt>(&self, reader: R) -> FidoResult<Response> {
|
||||||
|
Ok(match self {
|
||||||
|
Request::MakeCredential(_) => Response::MakeCredential(
|
||||||
|
MakeCredentialResponse::decode(reader)?,
|
||||||
|
),
|
||||||
|
Request::GetAssertion(_) => Response::GetAssertion(
|
||||||
|
GetAssertionResponse::decode(reader)?,
|
||||||
|
),
|
||||||
|
Request::GetInfo => Response::GetInfo(GetInfoResponse::decode(reader)?),
|
||||||
|
Request::ClientPin(_) => Response::ClientPin(ClientPinResponse::decode(reader)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
MakeCredential(MakeCredentialResponse),
|
||||||
|
GetAssertion(GetAssertionResponse),
|
||||||
|
GetInfo(GetInfoResponse),
|
||||||
|
ClientPin(ClientPinResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct MakeCredentialRequest<'a> {
|
||||||
|
pub client_data_hash: &'a [u8],
|
||||||
|
pub rp: PublicKeyCredentialRpEntity<'a>,
|
||||||
|
pub user: PublicKeyCredentialUserEntity<'a>,
|
||||||
|
pub pub_key_cred_params: &'a [(&'a str, i32)],
|
||||||
|
pub exclude_list: &'a [PublicKeyCredentialDescriptor],
|
||||||
|
pub extensions: &'a [(&'a str, &'a Value)],
|
||||||
|
pub options: Option<AuthenticatorOptions>,
|
||||||
|
pub pin_auth: Option<[u8; 16]>,
|
||||||
|
pub pin_protocol: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MakeCredentialRequest<'a> {
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, mut encoder: &mut Encoder<W>) -> FidoResult<()> {
|
||||||
|
encoder.writer().write_u8(0x01).context(
|
||||||
|
FidoErrorKind::CborEncode,
|
||||||
|
)?; // authenticatorMakeCredential
|
||||||
|
let mut length = 4;
|
||||||
|
length += !self.exclude_list.is_empty() as usize;
|
||||||
|
length += !self.extensions.is_empty() as usize;
|
||||||
|
length += self.options.is_some() as usize;
|
||||||
|
length += self.pin_auth.is_some() as usize;
|
||||||
|
length += self.pin_protocol.is_some() as usize;
|
||||||
|
encoder.object(length)?;
|
||||||
|
encoder.u8(0x01)?; // clientDataHash
|
||||||
|
encoder.bytes(&self.client_data_hash)?;
|
||||||
|
encoder.u8(0x02)?; // rp
|
||||||
|
self.rp.encode(&mut encoder)?;
|
||||||
|
encoder.u8(0x03)?; // user
|
||||||
|
self.user.encode(&mut encoder)?;
|
||||||
|
encoder.u8(0x04)?; // pubKeyCredParams
|
||||||
|
encoder.array(self.pub_key_cred_params.len())?;
|
||||||
|
for (cred_type, alg) in self.pub_key_cred_params {
|
||||||
|
encoder.object(2)?;
|
||||||
|
encoder.text("alg")?;
|
||||||
|
encoder.i32(*alg)?;
|
||||||
|
encoder.text("type")?;
|
||||||
|
encoder.text(&cred_type)?;
|
||||||
|
}
|
||||||
|
if self.exclude_list.len() > 0 {
|
||||||
|
encoder.u8(0x05)?; // excludeList
|
||||||
|
encoder.array(self.exclude_list.len())?;
|
||||||
|
for item in self.exclude_list {
|
||||||
|
item.encode(&mut encoder)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.extensions.len() > 0 {
|
||||||
|
encoder.u8(0x06)?; // extensions
|
||||||
|
encoder.object(self.extensions.len())?;
|
||||||
|
for (key, value) in self.extensions {
|
||||||
|
encoder.text(key)?;
|
||||||
|
let mut generic = GenericEncoder::new(encoder.writer());
|
||||||
|
generic.value(value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(options) = &self.options {
|
||||||
|
if options.encoded() {
|
||||||
|
encoder.u8(0x07)?; // options
|
||||||
|
options.encode(&mut encoder)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(pin_auth) = &self.pin_auth {
|
||||||
|
encoder.u8(0x08)?; // pinAuth
|
||||||
|
encoder.bytes(pin_auth)?;
|
||||||
|
}
|
||||||
|
if let Some(pin_protocol) = &self.pin_protocol {
|
||||||
|
encoder.u8(0x09)?; // pinProtocol
|
||||||
|
encoder.u8(*pin_protocol)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct MakeCredentialResponse {
|
||||||
|
pub format: String,
|
||||||
|
pub auth_data: AuthenticatorData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MakeCredentialResponse {
|
||||||
|
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
||||||
|
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
||||||
|
if status != 0 {
|
||||||
|
Err(FidoErrorKind::CborError(status))?
|
||||||
|
}
|
||||||
|
let mut decoder = Decoder::new(Config::default(), reader);
|
||||||
|
let mut response = MakeCredentialResponse::default();
|
||||||
|
for _ in 0..decoder.object()? {
|
||||||
|
let key = decoder.u8()?;
|
||||||
|
match key {
|
||||||
|
0x01 => response.format = decoder.text()?,
|
||||||
|
0x02 => response.auth_data = AuthenticatorData::from_bytes(&decoder.bytes()?)?,
|
||||||
|
0x03 => break, // TODO: parse attestation
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct GetAssertionRequest<'a> {
|
||||||
|
pub rp_id: &'a str,
|
||||||
|
pub client_data_hash: &'a [u8],
|
||||||
|
pub allow_list: &'a [PublicKeyCredentialDescriptor],
|
||||||
|
pub extensions: &'a [(&'a str, &'a Value)],
|
||||||
|
pub options: Option<AuthenticatorOptions>,
|
||||||
|
pub pin_auth: Option<[u8; 16]>,
|
||||||
|
pub pin_protocol: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GetAssertionRequest<'a> {
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, mut encoder: &mut Encoder<W>) -> FidoResult<()> {
|
||||||
|
encoder.writer().write_u8(0x02).context(
|
||||||
|
FidoErrorKind::CborEncode,
|
||||||
|
)?; // authenticatorGetAssertion
|
||||||
|
let mut length = 2;
|
||||||
|
length += !self.allow_list.is_empty() as usize;
|
||||||
|
length += !self.extensions.is_empty() as usize;
|
||||||
|
length += self.options.is_some() as usize;
|
||||||
|
length += self.pin_auth.is_some() as usize;
|
||||||
|
length += self.pin_protocol.is_some() as usize;
|
||||||
|
encoder.object(length)?;
|
||||||
|
encoder.u8(0x01)?; // rpId
|
||||||
|
encoder.text(&self.rp_id)?;
|
||||||
|
encoder.u8(0x02)?; // clientDataHash
|
||||||
|
encoder.bytes(self.client_data_hash)?;
|
||||||
|
if !self.allow_list.is_empty() {
|
||||||
|
encoder.u8(0x03)?; // allowList
|
||||||
|
encoder.array(self.allow_list.len())?;
|
||||||
|
for item in self.allow_list {
|
||||||
|
item.encode(&mut encoder)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.extensions.len() > 0 {
|
||||||
|
encoder.u8(0x04)?; // extensions
|
||||||
|
encoder.object(self.extensions.len())?;
|
||||||
|
for (key, value) in self.extensions {
|
||||||
|
encoder.text(key)?;
|
||||||
|
let mut generic = GenericEncoder::new(encoder.writer());
|
||||||
|
generic.value(value)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(options) = &self.options {
|
||||||
|
if options.encoded() {
|
||||||
|
encoder.u8(0x05)?; // options
|
||||||
|
options.encode(&mut encoder)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(pin_auth) = &self.pin_auth {
|
||||||
|
encoder.u8(0x06)?; // pinAuth
|
||||||
|
encoder.bytes(pin_auth)?;
|
||||||
|
}
|
||||||
|
if let Some(pin_protocol) = &self.pin_protocol {
|
||||||
|
encoder.u8(0x07)?; // pinProtocol
|
||||||
|
encoder.u8(*pin_protocol)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct GetAssertionResponse {
|
||||||
|
pub credential: Option<PublicKeyCredentialDescriptor>,
|
||||||
|
pub auth_data_bytes: Vec<u8>,
|
||||||
|
pub auth_data: AuthenticatorData,
|
||||||
|
pub signature: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetAssertionResponse {
|
||||||
|
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
||||||
|
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
||||||
|
if status != 0 {
|
||||||
|
Err(FidoErrorKind::CborError(status))?
|
||||||
|
}
|
||||||
|
let mut decoder = Decoder::new(Config::default(), reader);
|
||||||
|
let mut response = GetAssertionResponse::default();
|
||||||
|
for _ in 0..decoder.object()? {
|
||||||
|
let key = decoder.u8()?;
|
||||||
|
match key {
|
||||||
|
0x01 => {
|
||||||
|
response.credential = Some(PublicKeyCredentialDescriptor::decode(&mut decoder)?)
|
||||||
|
}
|
||||||
|
0x02 => {
|
||||||
|
response.auth_data_bytes = decoder.bytes()?;
|
||||||
|
response.auth_data = AuthenticatorData::from_bytes(&response.auth_data_bytes)?;
|
||||||
|
}
|
||||||
|
0x03 => response.signature = decoder.bytes()?,
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct GetInfoResponse {
|
||||||
|
pub versions: Vec<String>,
|
||||||
|
pub extensions: Vec<String>,
|
||||||
|
pub aaguid: [u8; 16],
|
||||||
|
pub options: OptionsInfo,
|
||||||
|
pub max_msg_size: u16,
|
||||||
|
pub pin_protocols: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetInfoResponse {
|
||||||
|
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
||||||
|
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
||||||
|
if status != 0 {
|
||||||
|
Err(FidoErrorKind::CborError(status))?
|
||||||
|
}
|
||||||
|
let mut decoder = Decoder::new(Config::default(), reader);
|
||||||
|
let mut response = GetInfoResponse::default();
|
||||||
|
for _ in 0..decoder.object()? {
|
||||||
|
match decoder.u8()? {
|
||||||
|
0x01 => {
|
||||||
|
for _ in 0..decoder.array()? {
|
||||||
|
response.versions.push(decoder.text()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x02 => {
|
||||||
|
for _ in 0..decoder.array()? {
|
||||||
|
response.extensions.push(decoder.text()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x03 => response.aaguid.copy_from_slice(&decoder.bytes()?[..]),
|
||||||
|
0x04 => response.options = OptionsInfo::decode(&mut decoder)?,
|
||||||
|
0x05 => response.max_msg_size = decoder.u16()?,
|
||||||
|
0x06 => {
|
||||||
|
for _ in 0..decoder.array()? {
|
||||||
|
response.pin_protocols.push(decoder.u8()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ClientPinRequest<'a> {
|
||||||
|
pub pin_protocol: u8,
|
||||||
|
pub sub_command: u8,
|
||||||
|
pub key_agreement: Option<&'a CoseKey>,
|
||||||
|
pub pin_auth: Option<[u8; 16]>,
|
||||||
|
pub new_pin_enc: Option<Vec<u8>>,
|
||||||
|
pub pin_hash_enc: Option<[u8; 16]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ClientPinRequest<'a> {
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
|
||||||
|
encoder.writer().write_u8(0x06).context(
|
||||||
|
FidoErrorKind::CborEncode,
|
||||||
|
)?; // authenticatorClientPIN
|
||||||
|
let mut length = 2;
|
||||||
|
length += self.key_agreement.is_some() as usize;
|
||||||
|
length += self.pin_auth.is_some() as usize;
|
||||||
|
length += self.new_pin_enc.is_some() as usize;
|
||||||
|
length += self.pin_hash_enc.is_some() as usize;
|
||||||
|
encoder.object(length)?;
|
||||||
|
encoder.u8(0x01)?; // pinProtocol
|
||||||
|
encoder.u8(self.pin_protocol)?;
|
||||||
|
encoder.u8(0x02)?; // subCommand
|
||||||
|
encoder.u8(self.sub_command)?;
|
||||||
|
if let Some(key_agreement) = self.key_agreement {
|
||||||
|
encoder.u8(0x03)?; // keyAgreement
|
||||||
|
key_agreement.encode(encoder)?;
|
||||||
|
}
|
||||||
|
if let Some(pin_auth) = &self.pin_auth {
|
||||||
|
encoder.u8(0x04)?; // pinAuth
|
||||||
|
encoder.bytes(pin_auth)?;
|
||||||
|
}
|
||||||
|
if let Some(new_pin_enc) = &self.new_pin_enc {
|
||||||
|
encoder.u8(0x05)?; // newPinEnc
|
||||||
|
encoder.bytes(&new_pin_enc)?;
|
||||||
|
}
|
||||||
|
if let Some(pin_hash_enc) = &self.pin_hash_enc {
|
||||||
|
encoder.u8(0x06)?; // pinHashEnc
|
||||||
|
encoder.bytes(pin_hash_enc)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ClientPinResponse {
|
||||||
|
pub key_agreement: Option<CoseKey>,
|
||||||
|
pub pin_token: Option<[u8; 16]>,
|
||||||
|
pub retries: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientPinResponse {
|
||||||
|
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
||||||
|
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
||||||
|
if status != 0 {
|
||||||
|
Err(FidoErrorKind::CborError(status))?
|
||||||
|
}
|
||||||
|
let mut decoder = Decoder::new(Config::default(), reader);
|
||||||
|
let mut response = ClientPinResponse::default();
|
||||||
|
for _ in 0..decoder.object()? {
|
||||||
|
match decoder.u8()? {
|
||||||
|
0x01 => {
|
||||||
|
let mut generic = GenericDecoder::from_decoder(decoder);
|
||||||
|
response.key_agreement = Some(CoseKey::decode(&mut generic)?);
|
||||||
|
decoder = generic.into_inner();
|
||||||
|
}
|
||||||
|
0x02 => {
|
||||||
|
let mut pin_token = [0; 16];
|
||||||
|
pin_token.copy_from_slice(&decoder.bytes()?[..]);
|
||||||
|
response.pin_token = Some(pin_token)
|
||||||
|
}
|
||||||
|
0x03 => response.retries = Some(decoder.u8()?),
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OptionsInfo {
|
||||||
|
pub plat: bool,
|
||||||
|
pub rk: bool,
|
||||||
|
pub client_pin: Option<bool>,
|
||||||
|
pub up: bool,
|
||||||
|
pub uv: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OptionsInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
OptionsInfo {
|
||||||
|
plat: false,
|
||||||
|
rk: false,
|
||||||
|
client_pin: None,
|
||||||
|
up: true,
|
||||||
|
uv: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionsInfo {
|
||||||
|
pub fn decode<R: ReadBytesExt>(decoder: &mut Decoder<R>) -> FidoResult<Self> {
|
||||||
|
let mut options = OptionsInfo::default();
|
||||||
|
for _ in 0..decoder.object()? {
|
||||||
|
match decoder.text()?.as_ref() {
|
||||||
|
"plat" => options.plat = decoder.bool()?,
|
||||||
|
"rk" => options.rk = decoder.bool()?,
|
||||||
|
"clientPin" => options.client_pin = Some(decoder.bool()?),
|
||||||
|
"up" => options.up = decoder.bool()?,
|
||||||
|
"uv" => options.uv = Some(decoder.bool()?),
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct AuthenticatorData {
|
||||||
|
pub rp_id_hash: [u8; 32],
|
||||||
|
pub up: bool,
|
||||||
|
pub uv: bool,
|
||||||
|
pub sign_count: u32,
|
||||||
|
pub attested_credential_data: AttestedCredentialData,
|
||||||
|
pub extensions: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthenticatorData {
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> FidoResult<Self> {
|
||||||
|
let mut data = AuthenticatorData::default();
|
||||||
|
data.rp_id_hash.copy_from_slice(&bytes[0..32]);
|
||||||
|
let flags = bytes[32];
|
||||||
|
data.up = (flags & 0x01) == 0x01;
|
||||||
|
data.uv = (flags & 0x02) == 0x02;
|
||||||
|
data.sign_count = BigEndian::read_u32(&bytes[33..37]);
|
||||||
|
if bytes.len() < 38 {
|
||||||
|
return Ok(data);
|
||||||
|
}
|
||||||
|
let mut cur = Cursor::new(&bytes[37..]);
|
||||||
|
let attested_credential_data = AttestedCredentialData::from_bytes(&mut cur)?;
|
||||||
|
data.attested_credential_data = attested_credential_data;
|
||||||
|
if cur.position() >= (bytes.len() - 37) as u64 {
|
||||||
|
return Ok(data);
|
||||||
|
}
|
||||||
|
let mut decoder = GenericDecoder::new(Config::default(), cur);
|
||||||
|
for _ in 0..decoder.borrow_mut().object()? {
|
||||||
|
let key = decoder.borrow_mut().text()?;
|
||||||
|
let value = decoder.value()?;
|
||||||
|
data.extensions.insert(key.to_string(), value);
|
||||||
|
}
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct AttestedCredentialData {
|
||||||
|
pub aaguid: [u8; 16],
|
||||||
|
pub credential_id: Vec<u8>,
|
||||||
|
pub credential_public_key: CoseKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttestedCredentialData {
|
||||||
|
pub fn from_bytes(cur: &mut Cursor<&[u8]>) -> FidoResult<Self> {
|
||||||
|
let mut response = AttestedCredentialData::default();
|
||||||
|
let bytes = cur.get_ref();
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
response.aaguid.copy_from_slice(&bytes[0..16]);
|
||||||
|
let id_length = BigEndian::read_u16(&bytes[16..18]) as usize;
|
||||||
|
response.credential_id = Vec::from(&bytes[18..(18 + id_length)]);
|
||||||
|
cur.set_position(18 + id_length as u64);
|
||||||
|
let mut decoder = GenericDecoder::new(Config::default(), cur);
|
||||||
|
response.credential_public_key = CoseKey::decode(&mut decoder)?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct P256Key {
|
||||||
|
x: [u8; 32],
|
||||||
|
y: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl P256Key {
|
||||||
|
pub fn from_cose(cose: &CoseKey) -> FidoResult<Self> {
|
||||||
|
if cose.key_type != 2 || cose.algorithm != -7 {
|
||||||
|
Err(FidoErrorKind::KeyType)?
|
||||||
|
}
|
||||||
|
if let (Some(Value::U8(curve)),
|
||||||
|
Some(Value::Bytes(value::Bytes::Bytes(x))),
|
||||||
|
Some(Value::Bytes(value::Bytes::Bytes(y)))) =
|
||||||
|
(
|
||||||
|
cose.parameters.get(&-1),
|
||||||
|
cose.parameters.get(&-2),
|
||||||
|
cose.parameters.get(&-3),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if *curve != 1 {
|
||||||
|
Err(FidoErrorKind::KeyType)?
|
||||||
|
}
|
||||||
|
let mut key = P256Key::default();
|
||||||
|
key.x.copy_from_slice(&x);
|
||||||
|
key.y.copy_from_slice(&y);
|
||||||
|
return Ok(key);
|
||||||
|
}
|
||||||
|
Err(FidoErrorKind::KeyType)?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> FidoResult<Self> {
|
||||||
|
if bytes.len() != 65 || bytes[0] != 0x04 {
|
||||||
|
Err(FidoErrorKind::CborDecode)?
|
||||||
|
}
|
||||||
|
let mut res = P256Key::default();
|
||||||
|
res.x.copy_from_slice(&bytes[1..33]);
|
||||||
|
res.y.copy_from_slice(&bytes[33..65]);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_cose(&self) -> CoseKey {
|
||||||
|
CoseKey {
|
||||||
|
key_type: 2,
|
||||||
|
algorithm: -7,
|
||||||
|
parameters: [
|
||||||
|
(-1, Value::U8(1)),
|
||||||
|
(-2, Value::Bytes(value::Bytes::Bytes(self.x.to_vec()))),
|
||||||
|
(-3, Value::Bytes(value::Bytes::Bytes(self.y.to_vec()))),
|
||||||
|
].iter()
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bytes(&self) -> [u8; 65] {
|
||||||
|
let mut bytes = [0; 65];
|
||||||
|
bytes[0] = 0x04;
|
||||||
|
bytes[1..33].copy_from_slice(&self.x);
|
||||||
|
bytes[33..65].copy_from_slice(&self.y);
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct CoseKey {
|
||||||
|
key_type: u16,
|
||||||
|
algorithm: i32,
|
||||||
|
parameters: HashMap<i16, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoseKey {
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
|
||||||
|
let size = 1 + self.parameters.len();
|
||||||
|
encoder.object(size)?;
|
||||||
|
encoder.i16(0x01)?; // keyType
|
||||||
|
encoder.u16(self.key_type)?;
|
||||||
|
//encoder.i16(0x02)?; // algorithm
|
||||||
|
//encoder.i32(self.algorithm)?;
|
||||||
|
for (key, value) in self.parameters.iter() {
|
||||||
|
encoder.i16(*key)?;
|
||||||
|
let mut generic = GenericEncoder::new(encoder.writer());
|
||||||
|
generic.value(value)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode<R: ReadBytesExt>(generic: &mut GenericDecoder<R>) -> FidoResult<Self> {
|
||||||
|
let items;
|
||||||
|
{
|
||||||
|
let decoder = generic.borrow_mut();
|
||||||
|
items = decoder.object()?;
|
||||||
|
}
|
||||||
|
let mut cose_key = CoseKey::default();
|
||||||
|
cose_key.algorithm = -7;
|
||||||
|
for _ in 0..items {
|
||||||
|
match generic.borrow_mut().i16()? {
|
||||||
|
0x01 => cose_key.key_type = generic.borrow_mut().u16()?,
|
||||||
|
0x02 => cose_key.algorithm = generic.borrow_mut().i32()?,
|
||||||
|
key if key < 0 => {
|
||||||
|
cose_key.parameters.insert(key, generic.value()?);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
generic.value()?; // skip unknown parameter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(cose_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct PublicKeyCredentialRpEntity<'a> {
|
||||||
|
pub id: &'a str,
|
||||||
|
pub name: Option<&'a str>,
|
||||||
|
pub icon: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PublicKeyCredentialRpEntity<'a> {
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
|
||||||
|
let mut length = 1;
|
||||||
|
length += self.name.is_some() as usize;
|
||||||
|
length += self.icon.is_some() as usize;
|
||||||
|
encoder.object(length)?;
|
||||||
|
encoder.text("id")?;
|
||||||
|
encoder.text(&self.id)?;
|
||||||
|
if let Some(icon) = &self.icon {
|
||||||
|
encoder.text("icon")?;
|
||||||
|
encoder.text(&icon)?;
|
||||||
|
}
|
||||||
|
if let Some(name) = &self.name {
|
||||||
|
encoder.text("name")?;
|
||||||
|
encoder.text(&name)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct PublicKeyCredentialUserEntity<'a> {
|
||||||
|
pub id: &'a [u8],
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: Option<&'a str>,
|
||||||
|
pub display_name: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PublicKeyCredentialUserEntity<'a> {
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
|
||||||
|
let mut length = 2;
|
||||||
|
length += self.icon.is_some() as usize;
|
||||||
|
length += self.display_name.is_some() as usize;
|
||||||
|
encoder.object(length)?;
|
||||||
|
encoder.text("id")?;
|
||||||
|
encoder.bytes(&self.id)?;
|
||||||
|
if let Some(icon) = &self.icon {
|
||||||
|
encoder.text("icon")?;
|
||||||
|
encoder.text(&icon)?;
|
||||||
|
}
|
||||||
|
encoder.text("name")?;
|
||||||
|
encoder.text(&self.name)?;
|
||||||
|
if let Some(display_name) = &self.display_name {
|
||||||
|
encoder.text("displayName")?;
|
||||||
|
encoder.text(&display_name)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct PublicKeyCredentialDescriptor {
|
||||||
|
pub cred_type: String,
|
||||||
|
pub id: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PublicKeyCredentialDescriptor {
|
||||||
|
pub fn decode<R: ReadBytesExt>(decoder: &mut Decoder<R>) -> FidoResult<Self> {
|
||||||
|
let mut response = PublicKeyCredentialDescriptor::default();
|
||||||
|
for _ in 0..decoder.object()? {
|
||||||
|
match decoder.text()?.as_ref() {
|
||||||
|
"id" => response.id = decoder.bytes()?,
|
||||||
|
"type" => response.cred_type = decoder.text()?,
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
|
||||||
|
encoder.object(2)?;
|
||||||
|
encoder.text("id")?;
|
||||||
|
encoder.bytes(&self.id)?;
|
||||||
|
encoder.text("type")?;
|
||||||
|
encoder.text(&self.cred_type)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AuthenticatorOptions {
|
||||||
|
pub rk: bool,
|
||||||
|
pub uv: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthenticatorOptions {
|
||||||
|
pub fn encoded(&self) -> bool {
|
||||||
|
self.rk || self.uv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
|
||||||
|
let length = (self.rk as usize) + (self.uv as usize);
|
||||||
|
encoder.object(length)?;
|
||||||
|
if self.rk {
|
||||||
|
encoder.text("rk")?;
|
||||||
|
encoder.bool(true)?;
|
||||||
|
}
|
||||||
|
if self.uv {
|
||||||
|
encoder.text("uv")?;
|
||||||
|
encoder.bool(true)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
123
src/crypto.rs
Normal file
123
src/crypto.rs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
|
||||||
|
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
|
||||||
|
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||||
|
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
|
// copied, modified, or distributed except according to those terms.
|
||||||
|
use ring::{agreement, rand, digest, hmac, signature};
|
||||||
|
use ring::error::Unspecified;
|
||||||
|
use untrusted::Input;
|
||||||
|
use rust_crypto::blockmodes::NoPadding;
|
||||||
|
use rust_crypto::aes;
|
||||||
|
use rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer};
|
||||||
|
use failure::ResultExt;
|
||||||
|
use super::cbor::{CoseKey, P256Key};
|
||||||
|
use super::error::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SharedSecret {
|
||||||
|
pub public_key: CoseKey,
|
||||||
|
pub shared_secret: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedSecret {
|
||||||
|
pub fn new(peer_key: &CoseKey) -> FidoResult<Self> {
|
||||||
|
let rng = rand::SystemRandom::new();
|
||||||
|
let private = agreement::EphemeralPrivateKey::generate(&agreement::ECDH_P256, &rng)
|
||||||
|
.context(FidoErrorKind::GenerateKey)?;
|
||||||
|
let public = &mut [0u8; agreement::PUBLIC_KEY_MAX_LEN][..private.public_key_len()];
|
||||||
|
private.compute_public_key(public).context(
|
||||||
|
FidoErrorKind::GenerateKey,
|
||||||
|
)?;
|
||||||
|
let peer = P256Key::from_cose(peer_key)
|
||||||
|
.context(FidoErrorKind::ParsePublic)?
|
||||||
|
.bytes();
|
||||||
|
let peer = Input::from(&peer);
|
||||||
|
let shared_secret = agreement::agree_ephemeral(
|
||||||
|
private,
|
||||||
|
&agreement::ECDH_P256,
|
||||||
|
peer,
|
||||||
|
Unspecified,
|
||||||
|
|material| Ok(digest::digest(&digest::SHA256, material)),
|
||||||
|
).context(FidoErrorKind::GenerateSecret)?;
|
||||||
|
let mut res = SharedSecret {
|
||||||
|
public_key: P256Key::from_bytes(&public)
|
||||||
|
.context(FidoErrorKind::ParsePublic)?
|
||||||
|
.to_cose(),
|
||||||
|
shared_secret: [0; 32],
|
||||||
|
};
|
||||||
|
res.shared_secret.copy_from_slice(shared_secret.as_ref());
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_pin(&self, pin: &str) -> FidoResult<[u8; 16]> {
|
||||||
|
let mut encryptor = aes::cbc_encryptor(
|
||||||
|
aes::KeySize::KeySize256,
|
||||||
|
&self.shared_secret,
|
||||||
|
&[0u8; 16],
|
||||||
|
NoPadding,
|
||||||
|
);
|
||||||
|
let pin_bytes = pin.as_bytes();
|
||||||
|
let hash = digest::digest(&digest::SHA256, &pin_bytes);
|
||||||
|
let in_bytes = &hash.as_ref()[0..16];
|
||||||
|
let mut input = RefReadBuffer::new(&in_bytes);
|
||||||
|
let mut out_bytes = [0; 16];
|
||||||
|
let mut output = RefWriteBuffer::new(&mut out_bytes);
|
||||||
|
encryptor.encrypt(&mut input, &mut output, true).map_err(
|
||||||
|
|_| {
|
||||||
|
FidoErrorKind::EncryptPin
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(out_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_token(&self, data: &mut [u8]) -> FidoResult<PinToken> {
|
||||||
|
let mut decryptor = aes::cbc_decryptor(
|
||||||
|
aes::KeySize::KeySize256,
|
||||||
|
&self.shared_secret,
|
||||||
|
&[0u8; 16],
|
||||||
|
NoPadding,
|
||||||
|
);
|
||||||
|
let mut input = RefReadBuffer::new(data);
|
||||||
|
let mut out_bytes = [0; 16];
|
||||||
|
let mut output = RefWriteBuffer::new(&mut out_bytes);
|
||||||
|
decryptor.decrypt(&mut input, &mut output, true).map_err(
|
||||||
|
|_| {
|
||||||
|
FidoErrorKind::DecryptPin
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(PinToken(hmac::SigningKey::new(&digest::SHA256, &out_bytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PinToken(hmac::SigningKey);
|
||||||
|
|
||||||
|
impl PinToken {
|
||||||
|
pub fn auth(&self, data: &[u8]) -> [u8; 16] {
|
||||||
|
let signature = hmac::sign(&self.0, &data);
|
||||||
|
let mut out = [0; 16];
|
||||||
|
out.copy_from_slice(&signature.as_ref()[0..16]);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_signature(
|
||||||
|
public_key: &[u8],
|
||||||
|
client_data: &[u8],
|
||||||
|
auth_data: &[u8],
|
||||||
|
signature: &[u8],
|
||||||
|
) -> bool {
|
||||||
|
let public_key = Input::from(&public_key);
|
||||||
|
let msg_len = client_data.len() + auth_data.len();
|
||||||
|
let mut msg = Vec::with_capacity(msg_len);
|
||||||
|
msg.extend_from_slice(auth_data);
|
||||||
|
msg.extend_from_slice(client_data);
|
||||||
|
let msg = Input::from(&msg);
|
||||||
|
let signature = Input::from(signature);
|
||||||
|
signature::verify(
|
||||||
|
&signature::ECDSA_P256_SHA256_ASN1,
|
||||||
|
public_key,
|
||||||
|
msg,
|
||||||
|
signature,
|
||||||
|
).is_ok()
|
||||||
|
}
|
101
src/error.rs
Normal file
101
src/error.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
|
||||||
|
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
|
||||||
|
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||||
|
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
|
// copied, modified, or distributed except according to those terms.
|
||||||
|
use cbor_codec::{EncodeError, DecodeError};
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use failure::{Context, Backtrace, Fail};
|
||||||
|
|
||||||
|
pub type FidoResult<T> = Result<T, FidoError>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FidoError(Context<FidoErrorKind>);
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
|
||||||
|
pub enum FidoErrorKind {
|
||||||
|
#[fail(display = "Read/write error with device.")]
|
||||||
|
Io,
|
||||||
|
#[fail(display = "Error while reading packet from device.")]
|
||||||
|
ReadPacket,
|
||||||
|
#[fail(display = "Error while writing packet to device.")]
|
||||||
|
WritePacket,
|
||||||
|
#[fail(display = "Error while parsing CTAP from device.")]
|
||||||
|
ParseCtap,
|
||||||
|
#[fail(display = "Error while encoding CBOR for device.")]
|
||||||
|
CborEncode,
|
||||||
|
#[fail(display = "Error while decoding CBOR from device.")]
|
||||||
|
CborDecode,
|
||||||
|
#[fail(display = "Packets received from device in the wrong order.")]
|
||||||
|
InvalidSequence,
|
||||||
|
#[fail(display = "Failed to generate private keypair.")]
|
||||||
|
GenerateKey,
|
||||||
|
#[fail(display = "Failed to generate shared secret.")]
|
||||||
|
GenerateSecret,
|
||||||
|
#[fail(display = "Failed to parse public key.")]
|
||||||
|
ParsePublic,
|
||||||
|
#[fail(display = "Failed to encrypt PIN.")]
|
||||||
|
EncryptPin,
|
||||||
|
#[fail(display = "Failed to decrypt PIN.")]
|
||||||
|
DecryptPin,
|
||||||
|
#[fail(display = "Supplied key has incorrect type.")]
|
||||||
|
KeyType,
|
||||||
|
#[fail(display = "Device returned error: 0x{:x}", _0)]
|
||||||
|
CborError(u8),
|
||||||
|
#[fail(display = "Device does not support FIDO2")]
|
||||||
|
DeviceUnsupported,
|
||||||
|
#[fail(display = "This operating requires a PIN but none was provided.")]
|
||||||
|
PinRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fail for FidoError {
|
||||||
|
fn cause(&self) -> Option<&Fail> {
|
||||||
|
self.0.cause()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backtrace(&self) -> Option<&Backtrace> {
|
||||||
|
self.0.backtrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FidoError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Display::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FidoError {
|
||||||
|
pub fn kind(&self) -> FidoErrorKind {
|
||||||
|
*self.0.get_context()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FidoErrorKind> for FidoError {
|
||||||
|
#[inline(always)]
|
||||||
|
fn from(kind: FidoErrorKind) -> FidoError {
|
||||||
|
FidoError(Context::new(kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Context<FidoErrorKind>> for FidoError {
|
||||||
|
fn from(inner: Context<FidoErrorKind>) -> FidoError {
|
||||||
|
FidoError(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EncodeError> for FidoError {
|
||||||
|
#[inline(always)]
|
||||||
|
fn from(err: EncodeError) -> FidoError {
|
||||||
|
FidoError(err.context(FidoErrorKind::CborEncode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DecodeError> for FidoError {
|
||||||
|
#[inline(always)]
|
||||||
|
fn from(err: DecodeError) -> FidoError {
|
||||||
|
FidoError(err.context(FidoErrorKind::CborDecode))
|
||||||
|
}
|
||||||
|
}
|
15
src/hid_common.rs
Normal file
15
src/hid_common.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
|
||||||
|
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
|
||||||
|
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||||
|
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
|
// copied, modified, or distributed except according to those terms.
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
/// Storage for device related information
|
||||||
|
pub struct DeviceInfo {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub usage_page: u16,
|
||||||
|
pub usage: u16,
|
||||||
|
}
|
86
src/hid_linux.rs
Normal file
86
src/hid_linux.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
|
||||||
|
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
|
||||||
|
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||||
|
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
|
// copied, modified, or distributed except according to those terms.
|
||||||
|
use std::io;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use byteorder::{ByteOrder, LittleEndian};
|
||||||
|
pub use super::hid_common::*;
|
||||||
|
|
||||||
|
static REPORT_DESCRIPTOR_KEY_MASK: u8 = 0xfc;
|
||||||
|
static LONG_ITEM_ENCODING: u8 = 0xfe;
|
||||||
|
static USAGE_PAGE: u8 = 0x04;
|
||||||
|
static USAGE: u8 = 0x08;
|
||||||
|
static REPORT_SIZE: u8 = 0x74;
|
||||||
|
|
||||||
|
pub fn enumerate() -> io::Result<Vec<DeviceInfo>> {
|
||||||
|
fs::read_dir("/sys/class/hidraw")?
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.map(|entry| path_to_device(&entry.path()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_to_device(path: &PathBuf) -> io::Result<DeviceInfo> {
|
||||||
|
let mut rd_path = path.clone();
|
||||||
|
rd_path.push("device/report_descriptor");
|
||||||
|
let rd = fs::read(rd_path)?;
|
||||||
|
let mut usage_page: u16 = 0;
|
||||||
|
let mut usage: u16 = 0;
|
||||||
|
let mut report_size: u16 = 0;
|
||||||
|
let mut pos: usize = 0;
|
||||||
|
|
||||||
|
while pos < rd.len() {
|
||||||
|
let key = rd[pos];
|
||||||
|
let mut key_size: usize = 1;
|
||||||
|
let mut size: u8;
|
||||||
|
|
||||||
|
if key == LONG_ITEM_ENCODING {
|
||||||
|
key_size = 3;
|
||||||
|
size = rd[pos + 1];
|
||||||
|
} else {
|
||||||
|
size = key & 0x03;
|
||||||
|
|
||||||
|
if size == 0x03 {
|
||||||
|
size = 0x04
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key & REPORT_DESCRIPTOR_KEY_MASK == USAGE_PAGE {
|
||||||
|
if size != 2 {
|
||||||
|
usage_page = u16::from(rd[pos + 1])
|
||||||
|
} else {
|
||||||
|
usage_page = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key & REPORT_DESCRIPTOR_KEY_MASK == USAGE {
|
||||||
|
if size != 2 {
|
||||||
|
usage = u16::from(rd[pos + 1])
|
||||||
|
} else {
|
||||||
|
usage = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key & REPORT_DESCRIPTOR_KEY_MASK == REPORT_SIZE {
|
||||||
|
if size != 2 {
|
||||||
|
report_size = u16::from(rd[pos + 1])
|
||||||
|
} else {
|
||||||
|
report_size = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = pos + key_size + size as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut device_path = PathBuf::from("/dev");
|
||||||
|
device_path.push(path.file_name().unwrap());
|
||||||
|
|
||||||
|
Ok(DeviceInfo {
|
||||||
|
path: device_path,
|
||||||
|
usage_page,
|
||||||
|
usage,
|
||||||
|
})
|
||||||
|
}
|
410
src/lib.rs
Normal file
410
src/lib.rs
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
|
||||||
|
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
|
||||||
|
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||||
|
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
|
// copied, modified, or distributed except according to those terms.
|
||||||
|
//! An implementation of the CTAP2 protocol over USB.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # fn do_fido() -> ctap::FidoResult<()> {
|
||||||
|
//! let devices = ctap::get_devices()?;
|
||||||
|
//! let device_info = &devices[0];
|
||||||
|
//! let mut device = ctap::FidoDevice::new(device_info)?;
|
||||||
|
//!
|
||||||
|
//! // This can be omitted if the FIDO device is not configured with a PIN.
|
||||||
|
//! let pin = "test";
|
||||||
|
//! device.unlock(pin)?;
|
||||||
|
//!
|
||||||
|
//! // In a real application these values would come from the requesting app.
|
||||||
|
//! let rp_id = "rp_id";
|
||||||
|
//! let user_id = [0];
|
||||||
|
//! let user_name = "user_name";
|
||||||
|
//! let client_data_hash = [0; 32];
|
||||||
|
//! let cred = device.make_credential(
|
||||||
|
//! rp_id,
|
||||||
|
//! &user_id,
|
||||||
|
//! user_name,
|
||||||
|
//! &client_data_hash
|
||||||
|
//! )?;
|
||||||
|
//!
|
||||||
|
//! // In a real application the credential would be stored and used later.
|
||||||
|
//! let result = device.get_assertion(&cred, &client_data_hash);
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
extern crate rand;
|
||||||
|
extern crate failure;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate failure_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate num_derive;
|
||||||
|
extern crate num_traits;
|
||||||
|
extern crate byteorder;
|
||||||
|
extern crate cbor as cbor_codec;
|
||||||
|
extern crate ring;
|
||||||
|
extern crate untrusted;
|
||||||
|
extern crate crypto as rust_crypto;
|
||||||
|
|
||||||
|
mod packet;
|
||||||
|
mod hid_common;
|
||||||
|
mod hid_linux;
|
||||||
|
mod error;
|
||||||
|
mod crypto;
|
||||||
|
mod cbor;
|
||||||
|
|
||||||
|
use std::cmp;
|
||||||
|
use std::u8;
|
||||||
|
use std::u16;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{Read, Write, Cursor};
|
||||||
|
|
||||||
|
use failure::{Fail, ResultExt};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use num_traits::FromPrimitive;
|
||||||
|
use self::hid_linux as hid;
|
||||||
|
use self::packet::CtapCommand;
|
||||||
|
use self::packet::Packet;
|
||||||
|
pub use self::error::*;
|
||||||
|
|
||||||
|
static BROADCAST_CID: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
|
||||||
|
|
||||||
|
/// Looks for any connected HID devices and returns those that support FIDO.
|
||||||
|
pub fn get_devices() -> error::FidoResult<Vec<hid::DeviceInfo>> {
|
||||||
|
Ok(
|
||||||
|
hid::enumerate()
|
||||||
|
.context(FidoErrorKind::Io)?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|dev| dev.usage_page == 0xf1d0 && dev.usage == 0x21)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A credential created by a FIDO2 authenticator.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FidoCredential {
|
||||||
|
/// The ID provided by the authenticator.
|
||||||
|
pub id: Vec<u8>,
|
||||||
|
/// The public key provided by the authenticator, in uncompressed form.
|
||||||
|
pub public_key: Vec<u8>,
|
||||||
|
/// The Relying Party ID provided by the platform when this key was generated.
|
||||||
|
pub rp_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An opened FIDO authenticator.
|
||||||
|
pub struct FidoDevice {
|
||||||
|
device: fs::File,
|
||||||
|
packet_size: u16,
|
||||||
|
channel_id: [u8; 4],
|
||||||
|
needs_pin: bool,
|
||||||
|
shared_secret: Option<crypto::SharedSecret>,
|
||||||
|
pin_token: Option<crypto::PinToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FidoDevice {
|
||||||
|
/// Open and initialize a given device. DeviceInfo is provided by the `get_devices`
|
||||||
|
/// function. This method will allocate a channel for this application, verify that
|
||||||
|
/// it supports FIDO2, and checks if a PIN is set.
|
||||||
|
///
|
||||||
|
/// This method will fail if the device can't be opened, if the device returns
|
||||||
|
/// malformed data or if the device is not supported.
|
||||||
|
pub fn new(device: &hid::DeviceInfo) -> error::FidoResult<Self> {
|
||||||
|
let mut options = fs::OpenOptions::new();
|
||||||
|
options.read(true).write(true);
|
||||||
|
let mut dev = FidoDevice {
|
||||||
|
device: options.open(&device.path).context(FidoErrorKind::Io)?,
|
||||||
|
packet_size: 64,
|
||||||
|
channel_id: BROADCAST_CID,
|
||||||
|
needs_pin: false,
|
||||||
|
shared_secret: None,
|
||||||
|
pin_token: None,
|
||||||
|
};
|
||||||
|
dev.init()?;
|
||||||
|
Ok(dev)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&mut self) -> FidoResult<()> {
|
||||||
|
let mut nonce = [0u8; 8];
|
||||||
|
thread_rng().fill_bytes(&mut nonce);
|
||||||
|
let response = self.exchange(CtapCommand::Init, &nonce)?;
|
||||||
|
if response.len() < 17 || response[0..8] != nonce {
|
||||||
|
Err(FidoErrorKind::ParseCtap)?
|
||||||
|
}
|
||||||
|
self.channel_id.copy_from_slice(&response[8..12]);
|
||||||
|
let response = match self.cbor(cbor::Request::GetInfo)? {
|
||||||
|
cbor::Response::GetInfo(resp) => resp,
|
||||||
|
_ => Err(FidoErrorKind::CborDecode)?,
|
||||||
|
};
|
||||||
|
if !response.versions.iter().any(|ver| ver == "FIDO_2_0") {
|
||||||
|
Err(FidoErrorKind::DeviceUnsupported)?
|
||||||
|
}
|
||||||
|
if !response.pin_protocols.iter().any(|ver| *ver == 1) {
|
||||||
|
Err(FidoErrorKind::DeviceUnsupported)?
|
||||||
|
}
|
||||||
|
self.needs_pin = response.options.client_pin == Some(true);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_shared_secret(&mut self) -> FidoResult<()> {
|
||||||
|
let mut request = cbor::ClientPinRequest::default();
|
||||||
|
request.pin_protocol = 1;
|
||||||
|
request.sub_command = 0x02; // getKeyAgreement
|
||||||
|
let response = match self.cbor(cbor::Request::ClientPin(request))? {
|
||||||
|
cbor::Response::ClientPin(resp) => resp,
|
||||||
|
_ => Err(FidoErrorKind::CborDecode)?,
|
||||||
|
};
|
||||||
|
if let Some(key_agreement) = response.key_agreement {
|
||||||
|
self.shared_secret = Some(crypto::SharedSecret::new(&key_agreement)?);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(FidoErrorKind::CborDecode)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unlock the device with the provided PIN. Internally this will generate
|
||||||
|
/// an ECDH keypair, send the encrypted PIN to the device and store the PIN
|
||||||
|
/// token that the device generates on every power cycle. The PIN itself is
|
||||||
|
/// not stored.
|
||||||
|
///
|
||||||
|
/// This method will fail if the device returns malformed data or the PIN is
|
||||||
|
/// incorrect.
|
||||||
|
pub fn unlock(&mut self, pin: &str) -> FidoResult<()> {
|
||||||
|
while self.shared_secret.is_none() {
|
||||||
|
self.init_shared_secret()?;
|
||||||
|
}
|
||||||
|
// If the PIN is invalid the device should create a new agreementKey,
|
||||||
|
// so we only replace shared_secret on success.
|
||||||
|
let shared_secret = self.shared_secret.take().unwrap();
|
||||||
|
let mut request = cbor::ClientPinRequest::default();
|
||||||
|
request.pin_protocol = 1;
|
||||||
|
request.sub_command = 0x05; // getPINToken
|
||||||
|
request.key_agreement = Some(&shared_secret.public_key);
|
||||||
|
request.pin_hash_enc = Some(shared_secret.encrypt_pin(pin)?);
|
||||||
|
let response = match self.cbor(cbor::Request::ClientPin(request))? {
|
||||||
|
cbor::Response::ClientPin(resp) => resp,
|
||||||
|
_ => Err(FidoErrorKind::CborDecode)?,
|
||||||
|
};
|
||||||
|
if let Some(mut pin_token) = response.pin_token {
|
||||||
|
self.pin_token = Some(shared_secret.decrypt_token(&mut pin_token)?);
|
||||||
|
self.shared_secret = Some(shared_secret);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(FidoErrorKind::CborDecode)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request a new credential from the authenticator. The `rp_id` should be
|
||||||
|
/// a stable string used to identify the party for whom the credential is
|
||||||
|
/// created, for convenience it will be returned with the credential.
|
||||||
|
/// `user_id` and `user_name` are not required when requesting attestations
|
||||||
|
/// but they MAY be displayed to the user and MAY be stored on the device
|
||||||
|
/// to be returned with an attestation if the device supports this.
|
||||||
|
/// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`,
|
||||||
|
/// this is only used to verify the attestation provided by the
|
||||||
|
/// authenticator. When not implementing WebAuthN this can be any random
|
||||||
|
/// 32-byte array.
|
||||||
|
///
|
||||||
|
/// This method will fail if a PIN is required but the device is not
|
||||||
|
/// unlocked or if the device returns malformed data.
|
||||||
|
pub fn make_credential(
|
||||||
|
&mut self,
|
||||||
|
rp_id: &str,
|
||||||
|
user_id: &[u8],
|
||||||
|
user_name: &str,
|
||||||
|
client_data_hash: &[u8],
|
||||||
|
) -> FidoResult<FidoCredential> {
|
||||||
|
if self.needs_pin && self.pin_token.is_none() {
|
||||||
|
Err(FidoErrorKind::PinRequired)?
|
||||||
|
}
|
||||||
|
if client_data_hash.len() != 32 {
|
||||||
|
Err(FidoErrorKind::CborEncode)?
|
||||||
|
}
|
||||||
|
let pin_auth = self.pin_token.as_ref().map(
|
||||||
|
|token| token.auth(&client_data_hash),
|
||||||
|
);
|
||||||
|
let rp = cbor::PublicKeyCredentialRpEntity {
|
||||||
|
id: rp_id,
|
||||||
|
name: None,
|
||||||
|
icon: None,
|
||||||
|
};
|
||||||
|
let user = cbor::PublicKeyCredentialUserEntity {
|
||||||
|
id: user_id,
|
||||||
|
name: user_name,
|
||||||
|
icon: None,
|
||||||
|
display_name: None,
|
||||||
|
};
|
||||||
|
let pub_key_cred_params = [("public-key", -7)];
|
||||||
|
let request = cbor::MakeCredentialRequest {
|
||||||
|
client_data_hash,
|
||||||
|
rp,
|
||||||
|
user,
|
||||||
|
pub_key_cred_params: &pub_key_cred_params,
|
||||||
|
exclude_list: Default::default(),
|
||||||
|
extensions: Default::default(),
|
||||||
|
options: Some(cbor::AuthenticatorOptions {
|
||||||
|
rk: false,
|
||||||
|
uv: true,
|
||||||
|
}),
|
||||||
|
pin_auth,
|
||||||
|
pin_protocol: pin_auth.and(Some(0x01)),
|
||||||
|
};
|
||||||
|
let response = match self.cbor(cbor::Request::MakeCredential(request))? {
|
||||||
|
cbor::Response::MakeCredential(resp) => resp,
|
||||||
|
_ => Err(FidoErrorKind::CborDecode)?,
|
||||||
|
};
|
||||||
|
let public_key = cbor::P256Key::from_cose(
|
||||||
|
&response
|
||||||
|
.auth_data
|
||||||
|
.attested_credential_data
|
||||||
|
.credential_public_key,
|
||||||
|
)?
|
||||||
|
.bytes();
|
||||||
|
Ok(FidoCredential {
|
||||||
|
id: response.auth_data.attested_credential_data.credential_id,
|
||||||
|
rp_id: String::from(rp_id),
|
||||||
|
public_key: Vec::from(&public_key[..]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request an assertion from the authenticator for a given credential.
|
||||||
|
/// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`,
|
||||||
|
/// this is signed and verified as part of the attestation. When not
|
||||||
|
/// implementing WebAuthN this can be any random 32-byte array.
|
||||||
|
///
|
||||||
|
/// This method will return whether the assertion matches the credential
|
||||||
|
/// provided, and will fail if a PIN is required but not provided or if the
|
||||||
|
/// device returns malformed data.
|
||||||
|
pub fn get_assertion(
|
||||||
|
&mut self,
|
||||||
|
credential: &FidoCredential,
|
||||||
|
client_data_hash: &[u8],
|
||||||
|
) -> FidoResult<bool> {
|
||||||
|
if self.needs_pin && self.pin_token.is_none() {
|
||||||
|
Err(FidoErrorKind::PinRequired)?
|
||||||
|
}
|
||||||
|
if client_data_hash.len() != 32 {
|
||||||
|
Err(FidoErrorKind::CborEncode)?
|
||||||
|
}
|
||||||
|
let pin_auth = self.pin_token.as_ref().map(
|
||||||
|
|token| token.auth(&client_data_hash),
|
||||||
|
);
|
||||||
|
let allow_list = [
|
||||||
|
cbor::PublicKeyCredentialDescriptor {
|
||||||
|
cred_type: String::from("public-key"),
|
||||||
|
id: credential.id.clone(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let request = cbor::GetAssertionRequest {
|
||||||
|
rp_id: &credential.rp_id,
|
||||||
|
client_data_hash: client_data_hash,
|
||||||
|
allow_list: &allow_list,
|
||||||
|
extensions: Default::default(),
|
||||||
|
options: Some(cbor::AuthenticatorOptions {
|
||||||
|
rk: false,
|
||||||
|
uv: true,
|
||||||
|
}),
|
||||||
|
pin_auth,
|
||||||
|
pin_protocol: pin_auth.and(Some(0x01)),
|
||||||
|
};
|
||||||
|
let response = match self.cbor(cbor::Request::GetAssertion(request))? {
|
||||||
|
cbor::Response::GetAssertion(resp) => resp,
|
||||||
|
_ => Err(FidoErrorKind::CborDecode)?,
|
||||||
|
};
|
||||||
|
Ok(crypto::verify_signature(
|
||||||
|
&credential.public_key,
|
||||||
|
&client_data_hash,
|
||||||
|
&response.auth_data_bytes,
|
||||||
|
&response.signature,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cbor(&mut self, request: cbor::Request) -> FidoResult<cbor::Response> {
|
||||||
|
let mut buf = Cursor::new(Vec::new());
|
||||||
|
request.encode(&mut buf).context(FidoErrorKind::CborEncode)?;
|
||||||
|
let response = self.exchange(CtapCommand::Cbor, &buf.into_inner())?;
|
||||||
|
request
|
||||||
|
.decode(Cursor::new(response))
|
||||||
|
.context(FidoErrorKind::CborDecode)
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exchange(&mut self, cmd: CtapCommand, payload: &[u8]) -> FidoResult<Vec<u8>> {
|
||||||
|
self.send(&cmd, payload)?;
|
||||||
|
self.receive(&cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(&mut self, cmd: &CtapCommand, payload: &[u8]) -> FidoResult<()> {
|
||||||
|
if payload.is_empty() || payload.len() > u16::MAX as usize {
|
||||||
|
Err(FidoErrorKind::WritePacket)?
|
||||||
|
}
|
||||||
|
let to_send = payload.len() as u16;
|
||||||
|
let max_payload = (self.packet_size - 7) as usize;
|
||||||
|
let (frame, payload) = payload.split_at(cmp::min(payload.len(), max_payload));
|
||||||
|
{
|
||||||
|
let packet = packet::InitPacket::new(&self.channel_id, cmd, to_send, frame);
|
||||||
|
self.device.write(packet.to_wire_format()).context(
|
||||||
|
FidoErrorKind::WritePacket,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if payload.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let max_payload = (self.packet_size - 5) as usize;
|
||||||
|
for (seq, frame) in (0..u8::MAX).zip(payload.chunks(max_payload)) {
|
||||||
|
let packet = packet::ContPacket::new(&self.channel_id, seq, frame);
|
||||||
|
self.device.write(packet.to_wire_format()).context(
|
||||||
|
FidoErrorKind::WritePacket,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
self.device.flush().context(FidoErrorKind::WritePacket)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive(&mut self, cmd: &CtapCommand) -> FidoResult<Vec<u8>> {
|
||||||
|
let mut first_packet: Option<packet::InitPacket> = None;
|
||||||
|
let mut packet_size = 0;
|
||||||
|
while first_packet.is_none() {
|
||||||
|
let mut buf = [0; 64];
|
||||||
|
packet_size = self.device.read(&mut buf).context(
|
||||||
|
FidoErrorKind::ReadPacket,
|
||||||
|
)?;
|
||||||
|
let packet = packet::InitPacket::from_wire_format(&buf[0..packet_size]);
|
||||||
|
if packet.cmd() == CtapCommand::Error {
|
||||||
|
Err(
|
||||||
|
packet::CtapError::from_u8(packet.payload()[0])
|
||||||
|
.unwrap_or(packet::CtapError::Other)
|
||||||
|
.context(FidoErrorKind::ParseCtap),
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
if packet.cid() == self.channel_id && &packet.cmd() == cmd {
|
||||||
|
first_packet = Some(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let first_packet = first_packet.unwrap();
|
||||||
|
let mut data = first_packet.payload()[0..(packet_size - 7)].to_vec();
|
||||||
|
let mut to_read = (first_packet.size() as isize) - data.len() as isize;
|
||||||
|
let mut seq = 0;
|
||||||
|
while to_read > 0 {
|
||||||
|
let mut buf = [0; 64];
|
||||||
|
let packet_size = self.device.read(&mut buf).context(
|
||||||
|
FidoErrorKind::ReadPacket,
|
||||||
|
)?;
|
||||||
|
let packet = packet::ContPacket::from_wire_format(&buf[0..packet_size]);
|
||||||
|
if packet.cid() != self.channel_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if packet.seq() != seq {
|
||||||
|
Err(FidoErrorKind::InvalidSequence)?
|
||||||
|
}
|
||||||
|
let payload_size = packet_size - 5;
|
||||||
|
to_read -= payload_size as isize;
|
||||||
|
data.extend(&packet.payload()[0..payload_size]);
|
||||||
|
seq += 1;
|
||||||
|
}
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
}
|
143
src/packet.rs
Normal file
143
src/packet.rs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
|
||||||
|
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
|
||||||
|
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||||
|
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||||
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
|
// copied, modified, or distributed except according to those terms.
|
||||||
|
use num_traits::{FromPrimitive, ToPrimitive};
|
||||||
|
|
||||||
|
static FRAME_INIT: u8 = 0x80;
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(FromPrimitive, ToPrimitive, PartialEq)]
|
||||||
|
pub enum CtapCommand {
|
||||||
|
Invalid = 0x00,
|
||||||
|
Ping = 0x01,
|
||||||
|
Msg = 0x03,
|
||||||
|
Lock = 0x04,
|
||||||
|
Init = 0x06,
|
||||||
|
Wink = 0x08,
|
||||||
|
Cbor = 0x10,
|
||||||
|
Cancel = 0x11,
|
||||||
|
Keepalive = 0x3b,
|
||||||
|
Error = 0x3f,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CtapCommand {
|
||||||
|
pub fn to_wire_format(&self) -> u8 {
|
||||||
|
match self.to_u8() {
|
||||||
|
Some(x) => x,
|
||||||
|
None => 0x00,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(FromPrimitive, Fail, Debug)]
|
||||||
|
pub enum CtapError {
|
||||||
|
#[fail(display = "The command in the request is invalid")]
|
||||||
|
InvalidCmd = 0x01,
|
||||||
|
#[fail(display = "The parameter(s) in the request is invalid")]
|
||||||
|
InvalidPar = 0x02,
|
||||||
|
#[fail(display = "The length field (BCNT) is invalid for the request ")]
|
||||||
|
InvalidLen = 0x03,
|
||||||
|
#[fail(display = "The sequence does not match expected value ")]
|
||||||
|
InvalidSeq = 0x04,
|
||||||
|
#[fail(display = "The message has timed out ")]
|
||||||
|
MsgTimeout = 0x05,
|
||||||
|
#[fail(display = "The device is busy for the requesting channel ")]
|
||||||
|
ChannelBusy = 0x06,
|
||||||
|
#[fail(display = "Command requires channel lock ")]
|
||||||
|
LockRequired = 0x0A,
|
||||||
|
#[fail(display = "Reserved error")]
|
||||||
|
NA = 0x0B,
|
||||||
|
#[fail(display = "Unspecified error")]
|
||||||
|
Other = 0x7F,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Packet {
|
||||||
|
fn from_wire_format(data: &[u8]) -> Self;
|
||||||
|
|
||||||
|
fn to_wire_format(&self) -> &[u8];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InitPacket(pub [u8; 65]);
|
||||||
|
|
||||||
|
impl InitPacket {
|
||||||
|
pub fn new(cid: &[u8], cmd: &CtapCommand, size: u16, payload: &[u8]) -> InitPacket {
|
||||||
|
let mut packet = InitPacket([0; 65]);
|
||||||
|
packet.0[1..5].copy_from_slice(cid);
|
||||||
|
packet.0[5] = FRAME_INIT | cmd.to_wire_format();
|
||||||
|
packet.0[6] = ((size >> 8) & 0xff) as u8;
|
||||||
|
packet.0[7] = (size & 0xff) as u8;
|
||||||
|
packet.0[8..(payload.len() + 8)].copy_from_slice(payload);
|
||||||
|
packet
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cid(&self) -> &[u8] {
|
||||||
|
&self.0[1..5]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmd(&self) -> CtapCommand {
|
||||||
|
match CtapCommand::from_u8(self.0[5] ^ FRAME_INIT) {
|
||||||
|
Some(cmd) => cmd,
|
||||||
|
None => CtapCommand::Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> u16 {
|
||||||
|
((u16::from(self.0[6])) << 8) | u16::from(self.0[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn payload(&self) -> &[u8] {
|
||||||
|
&self.0[8..65]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Packet for InitPacket {
|
||||||
|
fn from_wire_format(data: &[u8]) -> InitPacket {
|
||||||
|
let mut packet = InitPacket([0; 65]);
|
||||||
|
packet.0[1..65].copy_from_slice(data);
|
||||||
|
packet
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_wire_format(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContPacket(pub [u8; 65]);
|
||||||
|
|
||||||
|
impl ContPacket {
|
||||||
|
pub fn new(cid: &[u8], seq: u8, payload: &[u8]) -> ContPacket {
|
||||||
|
let mut packet = ContPacket([0; 65]);
|
||||||
|
packet.0[1..5].copy_from_slice(cid);
|
||||||
|
packet.0[5] = seq;
|
||||||
|
packet.0[6..(payload.len() + 6)].copy_from_slice(payload);
|
||||||
|
packet
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cid(&self) -> &[u8] {
|
||||||
|
&self.0[1..5]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seq(&self) -> u8 {
|
||||||
|
self.0[5]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn payload(&self) -> &[u8] {
|
||||||
|
&self.0[6..65]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Packet for ContPacket {
|
||||||
|
fn from_wire_format(data: &[u8]) -> ContPacket {
|
||||||
|
let mut packet = ContPacket([0; 65]);
|
||||||
|
packet.0[1..65].copy_from_slice(data);
|
||||||
|
packet
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_wire_format(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user