chore(authenticator): Inline u2f crate (#780)

* chore(authenticator): Inline u2f crate

* update chrono

* update rustix

* clippy

Committed via a GitHub action: https://github.com/tauri-apps/plugins-workspace/actions/runs/7032171903

Co-authored-by: FabianLars <FabianLars@users.noreply.github.com>
This commit is contained in:
Fabian-Lars
2023-11-29 11:57:03 +00:00
committed by tauri-bot
parent 93394e89d3
commit 146493665a
13 changed files with 700 additions and 5 deletions

View File

@@ -19,8 +19,10 @@ authenticator = "0.3.1"
once_cell = "1"
sha2 = "0.10"
base64 = "0.21"
u2f = "0.2"
chrono = "0.4"
bytes = "0.4"
byteorder = "1.3"
openssl = "0.10"
[dev-dependencies]
rand = "0.8"

View File

@@ -7,7 +7,7 @@ pub enum Error {
#[error(transparent)]
JSON(#[from] serde_json::Error),
#[error(transparent)]
U2F(#[from] u2f::u2ferror::U2fError),
U2F(#[from] crate::u2f_crate::u2ferror::U2fError),
#[error(transparent)]
Auth(#[from] authenticator::errors::AuthenticatorError),
}

View File

@@ -5,6 +5,7 @@
mod auth;
mod error;
mod u2f;
mod u2f_crate;
use tauri::{
plugin::{Builder as PluginBuilder, TauriPlugin},

View File

@@ -2,13 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::u2f_crate::messages::*;
use crate::u2f_crate::protocol::*;
use crate::u2f_crate::register::*;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use chrono::prelude::*;
use serde::Serialize;
use std::convert::Into;
use u2f::messages::*;
use u2f::protocol::*;
use u2f::register::*;
static VERSION: &str = "U2F_V2";

8
src/u2f_crate/LICENSE Normal file
View File

@@ -0,0 +1,8 @@
Copyright (c) 2017
Licensed under either of
* Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0)
* MIT license (http://opensource.org/licenses/MIT)
at your option.

View File

@@ -0,0 +1,65 @@
// Copyright 2021 Flavio Oliveira
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use bytes::{Buf, BufMut};
use openssl::sha::sha256;
use serde::Serialize;
use std::io::Cursor;
use crate::u2f_crate::u2ferror::U2fError;
/// The `Result` type used in this crate.
type Result<T> = ::std::result::Result<T, U2fError>;
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Authorization {
pub counter: u32,
pub user_presence: bool,
}
pub fn parse_sign_response(
app_id: String,
client_data: Vec<u8>,
public_key: Vec<u8>,
sign_data: Vec<u8>,
) -> Result<Authorization> {
if sign_data.len() <= 5 {
return Err(U2fError::InvalidSignatureData);
}
let user_presence_flag = &sign_data[0];
let counter = &sign_data[1..=4];
let signature = &sign_data[5..];
// Let's build the msg to verify the signature
let app_id_hash = sha256(&app_id.into_bytes());
let client_data_hash = sha256(&client_data[..]);
let mut msg = vec![];
msg.put(app_id_hash.as_ref());
msg.put(*user_presence_flag);
msg.put(counter);
msg.put(client_data_hash.as_ref());
let public_key = super::crypto::NISTP256Key::from_bytes(&public_key)?;
// The signature is to be verified by the relying party using the public key obtained during registration.
let verified = public_key.verify_signature(signature, msg.as_ref())?;
if !verified {
return Err(U2fError::BadSignature);
}
let authorization = Authorization {
counter: get_counter(counter),
user_presence: true,
};
Ok(authorization)
}
fn get_counter(counter: &[u8]) -> u32 {
let mut buf = Cursor::new(counter);
buf.get_u32_be()
}

156
src/u2f_crate/crypto.rs Normal file
View File

@@ -0,0 +1,156 @@
// Copyright 2021 Flavio Oliveira
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Cryptographic operation wrapper for Webauthn. This module exists to
//! allow ease of auditing, safe operation wrappers for the webauthn library,
//! and cryptographic provider abstraction. This module currently uses OpenSSL
//! as the cryptographic primitive provider.
// Source can be found here: https://github.com/Firstyear/webauthn-rs/blob/master/src/crypto.rs
#![allow(non_camel_case_types)]
use openssl::{bn, ec, hash, nid, sign, x509};
use std::convert::TryFrom;
// use super::constants::*;
use crate::u2f_crate::u2ferror::U2fError;
use openssl::pkey::Public;
// use super::proto::*;
// Why OpenSSL over another rust crate?
// - Well, the openssl crate allows us to reconstruct a public key from the
// x/y group coords, where most others want a pkcs formatted structure. As
// a result, it's easiest to use openssl as it gives us exactly what we need
// for these operations, and despite it's many challenges as a library, it
// has resources and investment into it's maintenance, so we can a least
// assert a higher level of confidence in it that <backyard crypto here>.
// Object({Integer(-3): Bytes([48, 185, 178, 204, 113, 186, 105, 138, 190, 33, 160, 46, 131, 253, 100, 177, 91, 243, 126, 128, 245, 119, 209, 59, 186, 41, 215, 196, 24, 222, 46, 102]), Integer(-2): Bytes([158, 212, 171, 234, 165, 197, 86, 55, 141, 122, 253, 6, 92, 242, 242, 114, 158, 221, 238, 163, 127, 214, 120, 157, 145, 226, 232, 250, 144, 150, 218, 138]), Integer(-1): U64(1), Integer(1): U64(2), Integer(3): I64(-7)})
//
/// An X509PublicKey. This is what is otherwise known as a public certificate
/// which comprises a public key and other signed metadata related to the issuer
/// of the key.
pub struct X509PublicKey {
pubk: x509::X509,
}
impl std::fmt::Debug for X509PublicKey {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "X509PublicKey")
}
}
impl TryFrom<&[u8]> for X509PublicKey {
type Error = U2fError;
// Must be DER bytes. If you have PEM, base64decode first!
fn try_from(d: &[u8]) -> Result<Self, Self::Error> {
let pubk = x509::X509::from_der(d)?;
Ok(X509PublicKey { pubk })
}
}
impl X509PublicKey {
pub(crate) fn common_name(&self) -> Option<String> {
let cert = &self.pubk;
let subject = cert.subject_name();
let common = subject
.entries_by_nid(openssl::nid::Nid::COMMONNAME)
.next()
.map(|b| b.data().as_slice());
if let Some(common) = common {
std::str::from_utf8(common).ok().map(|s| s.to_string())
} else {
None
}
}
pub(crate) fn is_secp256r1(&self) -> Result<bool, U2fError> {
// Can we get the public key?
let pk = self.pubk.public_key()?;
let ec_key = pk.ec_key()?;
ec_key.check_key()?;
let ec_grpref = ec_key.group();
let ec_curve = ec_grpref.curve_name().ok_or(U2fError::OpenSSLNoCurveName)?;
Ok(ec_curve == nid::Nid::X9_62_PRIME256V1)
}
pub(crate) fn verify_signature(
&self,
signature: &[u8],
verification_data: &[u8],
) -> Result<bool, U2fError> {
let pkey = self.pubk.public_key()?;
// TODO: Should this determine the hash type from the x509 cert? Or other?
let mut verifier = sign::Verifier::new(hash::MessageDigest::sha256(), &pkey)?;
verifier.update(verification_data)?;
Ok(verifier.verify(signature)?)
}
}
pub struct NISTP256Key {
/// The key's public X coordinate.
pub x: [u8; 32],
/// The key's public Y coordinate.
pub y: [u8; 32],
}
impl NISTP256Key {
pub fn from_bytes(public_key_bytes: &[u8]) -> Result<Self, U2fError> {
if public_key_bytes.len() != 65 {
return Err(U2fError::InvalidPublicKey);
}
if public_key_bytes[0] != 0x04 {
return Err(U2fError::InvalidPublicKey);
}
let mut x: [u8; 32] = Default::default();
x.copy_from_slice(&public_key_bytes[1..=32]);
let mut y: [u8; 32] = Default::default();
y.copy_from_slice(&public_key_bytes[33..=64]);
Ok(NISTP256Key { x, y })
}
fn get_key(&self) -> Result<ec::EcKey<Public>, U2fError> {
let ec_group = ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)?;
let xbn = bn::BigNum::from_slice(&self.x)?;
let ybn = bn::BigNum::from_slice(&self.y)?;
let ec_key = openssl::ec::EcKey::from_public_key_affine_coordinates(&ec_group, &xbn, &ybn)?;
// Validate the key is sound. IIRC this actually checks the values
// are correctly on the curve as specified
ec_key.check_key()?;
Ok(ec_key)
}
pub fn verify_signature(
&self,
signature: &[u8],
verification_data: &[u8],
) -> Result<bool, U2fError> {
let pkey = self.get_key()?;
let signature = openssl::ecdsa::EcdsaSig::from_der(signature)?;
let hash = openssl::sha::sha256(verification_data);
Ok(signature.verify(hash.as_ref(), &pkey)?)
}
}

54
src/u2f_crate/messages.rs Normal file
View File

@@ -0,0 +1,54 @@
// Copyright 2021 Flavio Oliveira
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// As defined by FIDO U2F Javascript API.
// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-javascript-api.html#registration
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct U2fRegisterRequest {
pub app_id: String,
pub register_requests: Vec<RegisterRequest>,
pub registered_keys: Vec<RegisteredKey>,
}
#[derive(Serialize)]
pub struct RegisterRequest {
pub version: String,
pub challenge: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RegisteredKey {
pub version: String,
pub key_handle: Option<String>,
pub app_id: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegisterResponse {
pub registration_data: String,
pub version: String,
pub client_data: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct U2fSignRequest {
pub app_id: String,
pub challenge: String,
pub registered_keys: Vec<RegisteredKey>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignResponse {
pub key_handle: String,
pub signature_data: String,
pub client_data: String,
}

12
src/u2f_crate/mod.rs Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2021 Flavio Oliveira
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
mod util;
pub mod authorization;
mod crypto;
pub mod messages;
pub mod protocol;
pub mod register;
pub mod u2ferror;

191
src/u2f_crate/protocol.rs Normal file
View File

@@ -0,0 +1,191 @@
// Copyright 2021 Flavio Oliveira
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::u2f_crate::authorization::*;
use crate::u2f_crate::messages::*;
use crate::u2f_crate::register::*;
use crate::u2f_crate::u2ferror::U2fError;
use crate::u2f_crate::util::*;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use chrono::prelude::*;
use chrono::Duration;
use serde::{Deserialize, Serialize};
type Result<T> = ::std::result::Result<T, U2fError>;
#[derive(Clone)]
pub struct U2f {
app_id: String,
}
#[derive(Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Challenge {
pub app_id: String,
pub challenge: String,
pub timestamp: String,
}
impl Challenge {
// Not used in this plugin.
#[allow(dead_code)]
pub fn new() -> Self {
Challenge {
app_id: String::new(),
challenge: String::new(),
timestamp: String::new(),
}
}
}
impl U2f {
// The app ID is a string used to uniquely identify an U2F app
pub fn new(app_id: String) -> Self {
U2f { app_id }
}
// Not used in this plugin.
#[allow(dead_code)]
pub fn generate_challenge(&self) -> Result<Challenge> {
let utc: DateTime<Utc> = Utc::now();
let challenge_bytes = generate_challenge(32)?;
let challenge = Challenge {
challenge: URL_SAFE_NO_PAD.encode(challenge_bytes),
timestamp: format!("{:?}", utc),
app_id: self.app_id.clone(),
};
Ok(challenge.clone())
}
// Not used in this plugin.
#[allow(dead_code)]
pub fn request(
&self,
challenge: Challenge,
registrations: Vec<Registration>,
) -> Result<U2fRegisterRequest> {
let u2f_request = U2fRegisterRequest {
app_id: self.app_id.clone(),
register_requests: self.register_request(challenge),
registered_keys: self.registered_keys(registrations),
};
Ok(u2f_request)
}
fn register_request(&self, challenge: Challenge) -> Vec<RegisterRequest> {
let mut requests: Vec<RegisterRequest> = vec![];
let request = RegisterRequest {
version: U2F_V2.into(),
challenge: challenge.challenge,
};
requests.push(request);
requests
}
pub fn register_response(
&self,
challenge: Challenge,
response: RegisterResponse,
) -> Result<Registration> {
if expiration(challenge.timestamp) > Duration::seconds(300) {
return Err(U2fError::ChallengeExpired);
}
let registration_data: Vec<u8> = URL_SAFE_NO_PAD
.decode(&response.registration_data[..])
.unwrap();
let client_data: Vec<u8> = URL_SAFE_NO_PAD.decode(&response.client_data[..]).unwrap();
parse_registration(challenge.app_id, client_data, registration_data)
}
fn registered_keys(&self, registrations: Vec<Registration>) -> Vec<RegisteredKey> {
let mut keys: Vec<RegisteredKey> = vec![];
for registration in registrations {
keys.push(get_registered_key(
self.app_id.clone(),
registration.key_handle,
));
}
keys
}
// Not used in this plugin.
#[allow(dead_code)]
pub fn sign_request(
&self,
challenge: Challenge,
registrations: Vec<Registration>,
) -> U2fSignRequest {
let mut keys: Vec<RegisteredKey> = vec![];
for registration in registrations {
keys.push(get_registered_key(
self.app_id.clone(),
registration.key_handle,
));
}
let signed_request = U2fSignRequest {
app_id: self.app_id.clone(),
challenge: URL_SAFE_NO_PAD.encode(challenge.challenge.as_bytes()),
registered_keys: keys,
};
signed_request
}
pub fn sign_response(
&self,
challenge: Challenge,
reg: Registration,
sign_resp: SignResponse,
counter: u32,
) -> Result<u32> {
if expiration(challenge.timestamp) > Duration::seconds(300) {
return Err(U2fError::ChallengeExpired);
}
if sign_resp.key_handle != get_encoded(&reg.key_handle[..]) {
return Err(U2fError::WrongKeyHandler);
}
let client_data: Vec<u8> = URL_SAFE_NO_PAD
.decode(&sign_resp.client_data[..])
.map_err(|_e| U2fError::InvalidClientData)?;
let sign_data: Vec<u8> = URL_SAFE_NO_PAD
.decode(&sign_resp.signature_data[..])
.map_err(|_e| U2fError::InvalidSignatureData)?;
let public_key = reg.pub_key;
let auth = parse_sign_response(
self.app_id.clone(),
client_data.clone(),
public_key,
sign_data.clone(),
);
match auth {
Ok(ref res) => {
// CounterTooLow is raised when the counter value received from the device is
// lower than last stored counter value.
if res.counter < counter {
Err(U2fError::CounterTooLow)
} else {
Ok(res.counter)
}
}
Err(e) => Err(e),
}
}
}

101
src/u2f_crate/register.rs Normal file
View File

@@ -0,0 +1,101 @@
// Copyright 2021 Flavio Oliveira
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use byteorder::{BigEndian, ByteOrder};
use bytes::{BufMut, Bytes};
use openssl::sha::sha256;
use serde::Serialize;
use crate::u2f_crate::messages::RegisteredKey;
use crate::u2f_crate::u2ferror::U2fError;
use crate::u2f_crate::util::*;
use std::convert::TryFrom;
/// The `Result` type used in this crate.
type Result<T> = ::std::result::Result<T, U2fError>;
// Single enrolment or pairing between an application and a token.
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Registration {
pub key_handle: Vec<u8>,
pub pub_key: Vec<u8>,
// AttestationCert can be null for Authenticate requests.
pub attestation_cert: Option<Vec<u8>>,
pub device_name: Option<String>,
}
pub fn parse_registration(
app_id: String,
client_data: Vec<u8>,
registration_data: Vec<u8>,
) -> Result<Registration> {
let reserved_byte = registration_data[0];
if reserved_byte != 0x05 {
return Err(U2fError::InvalidReservedByte);
}
let mut mem = Bytes::from(registration_data);
//Start parsing ... advance the reserved byte.
let _ = mem.split_to(1);
// P-256 NIST elliptic curve
let public_key = mem.split_to(65);
// Key Handle
let key_handle_size = mem.split_to(1);
let key_len = BigEndian::read_uint(&key_handle_size[..], 1);
let key_handle = mem.split_to(key_len as usize);
// The certificate length needs to be inferred by parsing.
let cert_len = asn_length(mem.clone()).unwrap();
let attestation_certificate = mem.split_to(cert_len);
// Remaining data corresponds to the signature
let signature = mem;
// Let's build the msg to verify the signature
let app_id_hash = sha256(&app_id.into_bytes());
let client_data_hash = sha256(&client_data[..]);
let mut msg = vec![0x00]; // A byte reserved for future use [1 byte] with the value 0x00
msg.put(app_id_hash.as_ref());
msg.put(client_data_hash.as_ref());
msg.put(key_handle.clone());
msg.put(public_key.clone());
// The signature is to be verified by the relying party using the public key certified
// in the attestation certificate.
let cerificate_public_key =
super::crypto::X509PublicKey::try_from(&attestation_certificate[..])?;
if !(cerificate_public_key.is_secp256r1()?) {
return Err(U2fError::BadCertificate);
}
let verified = cerificate_public_key.verify_signature(&signature[..], &msg[..])?;
if !verified {
return Err(U2fError::BadCertificate);
}
let registration = Registration {
key_handle: key_handle[..].to_vec(),
pub_key: public_key[..].to_vec(),
attestation_cert: Some(attestation_certificate[..].to_vec()),
device_name: cerificate_public_key.common_name(),
};
Ok(registration)
}
pub fn get_registered_key(app_id: String, key_handle: Vec<u8>) -> RegisteredKey {
RegisteredKey {
app_id,
version: U2F_V2.into(),
key_handle: Some(get_encoded(key_handle.as_slice())),
}
}

39
src/u2f_crate/u2ferror.rs Normal file
View File

@@ -0,0 +1,39 @@
// Copyright 2021 Flavio Oliveira
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use thiserror::Error;
#[derive(Debug, Error)]
pub enum U2fError {
#[error("ASM1 Decoder error")]
Asm1DecoderError,
#[error("Not able to verify signature")]
BadSignature,
#[error("Not able to generate random bytes")]
RandomSecureBytesError,
#[error("Invalid Reserved Byte")]
InvalidReservedByte,
#[error("Challenge Expired")]
ChallengeExpired,
#[error("Wrong Key Handler")]
WrongKeyHandler,
#[error("Invalid Client Data")]
InvalidClientData,
#[error("Invalid Signature Data")]
InvalidSignatureData,
#[error("Invalid User Presence Byte")]
InvalidUserPresenceByte,
#[error("Failed to parse certificate")]
BadCertificate,
#[error("Not Trusted Anchor")]
NotTrustedAnchor,
#[error("Counter too low")]
CounterTooLow,
#[error("Invalid public key")]
OpenSSLNoCurveName,
#[error("OpenSSL no curve name")]
InvalidPublicKey,
#[error(transparent)]
OpenSSLError(#[from] openssl::error::ErrorStack),
}

66
src/u2f_crate/util.rs Normal file
View File

@@ -0,0 +1,66 @@
// Copyright 2021 Flavio Oliveira
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::u2f_crate::u2ferror::U2fError;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use bytes::Bytes;
use chrono::prelude::*;
use chrono::Duration;
use openssl::rand;
/// The `Result` type used in this crate.
type Result<T> = ::std::result::Result<T, U2fError>;
pub const U2F_V2: &str = "U2F_V2";
// Generates a challenge from a secure, random source.
pub fn generate_challenge(size: usize) -> Result<Vec<u8>> {
let mut bytes: Vec<u8> = vec![0; size];
rand::rand_bytes(&mut bytes).map_err(|_e| U2fError::RandomSecureBytesError)?;
Ok(bytes)
}
pub fn expiration(timestamp: String) -> Duration {
let now: DateTime<Utc> = Utc::now();
let ts = timestamp.parse::<DateTime<Utc>>();
now.signed_duration_since(ts.unwrap())
}
// Decode initial bytes of buffer as ASN and return the length of the encoded structure.
// http://en.wikipedia.org/wiki/X.690
pub fn asn_length(mem: Bytes) -> Result<usize> {
let buffer: &[u8] = &mem[..];
if mem.len() < 2 || buffer[0] != 0x30 {
// Type
return Err(U2fError::Asm1DecoderError);
}
let len = buffer[1]; // Len
if len & 0x80 == 0 {
return Ok((len & 0x7f) as usize);
}
let numbem_of_bytes = len & 0x7f;
if numbem_of_bytes == 0 {
return Err(U2fError::Asm1DecoderError);
}
let mut length: usize = 0;
for num in 0..numbem_of_bytes {
length = length * 0x100 + (buffer[(2 + num) as usize] as usize);
}
length += numbem_of_bytes as usize;
Ok(length + 2) // Add the 2 initial bytes: type and length.
}
pub fn get_encoded(data: &[u8]) -> String {
let encoded: String = URL_SAFE_NO_PAD.encode(data);
encoded.trim_end_matches('=').to_string()
}