libwebauthn 0.5.1

FIDO2 (WebAuthn) and FIDO U2F platform library for Linux written in Rust
Documentation
use std::convert::TryFrom;
use std::io::{BufRead, Cursor as IOCursor, Error as IOError, ErrorKind as IOErrorKind, Read};
use std::time::Duration;

use byteorder::{BigEndian, ReadBytesExt};
use sha2::{Digest, Sha256};
use x509_parser::prelude::{FromDer, X509Certificate};

use crate::proto::ctap1::apdu::{ApduResponse, ApduResponseStatus};
use crate::proto::ctap2::Ctap2Transport;
use crate::webauthn::CtapError;

#[derive(Debug, Clone, Copy)]
pub enum Ctap1Transport {
    Bt,
    Ble,
    Nfc,
    Usb,
}

impl TryFrom<&Ctap2Transport> for Ctap1Transport {
    type Error = CtapError;
    fn try_from(ctap2: &Ctap2Transport) -> Result<Ctap1Transport, Self::Error> {
        match ctap2 {
            Ctap2Transport::Ble => Ok(Ctap1Transport::Ble),
            Ctap2Transport::Usb => Ok(Ctap1Transport::Usb),
            Ctap2Transport::Nfc => Ok(Ctap1Transport::Nfc),
            Ctap2Transport::Internal => Err(CtapError::UnsupportedOption),
            Ctap2Transport::Hybrid => Err(CtapError::UnsupportedOption),
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Ctap1Version {
    U2fV2,
}

#[derive(Debug, Clone)]
pub struct Ctap1RegisteredKey {
    pub version: Ctap1Version,
    pub key_handle: Vec<u8>,
    pub transports: Option<Vec<Ctap1Transport>>,
    pub app_id: Option<String>,
}

impl Ctap1RegisteredKey {
    pub fn new_u2f_v2(key_handle: &[u8]) -> Ctap1RegisteredKey {
        Ctap1RegisteredKey {
            version: Ctap1Version::U2fV2,
            key_handle: Vec::from(key_handle),
            transports: None,
            app_id: None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct Ctap1RegisterRequest {
    pub version: Ctap1Version,
    pub app_id_hash: Vec<u8>,
    pub challenge: Vec<u8>,
    pub registered_keys: Vec<Ctap1RegisteredKey>,
    pub timeout: Duration,
    pub require_user_presence: bool,
}

impl Ctap1RegisterRequest {
    pub fn new_u2f_v2(
        app_id: &str,
        challenge: &[u8],
        registered_keys: Vec<Ctap1RegisteredKey>,
        timeout: Duration,
        require_user_presence: bool,
    ) -> Ctap1RegisterRequest {
        let mut hasher = Sha256::default();
        hasher.update(app_id);
        let app_id_hash = hasher.finalize().to_vec();

        Ctap1RegisterRequest {
            version: Ctap1Version::U2fV2,
            app_id_hash,
            challenge: Vec::from(challenge),
            registered_keys,
            timeout,
            require_user_presence,
        }
    }

    pub fn dummy(timeout: Duration) -> Self {
        Ctap1RegisterRequest {
            version: Ctap1Version::U2fV2,
            app_id_hash: vec![0; 32],
            challenge: vec![0; 32],
            registered_keys: Vec::new(),
            timeout,
            require_user_presence: true,
        }
    }
}

#[derive(Debug)]
pub struct Ctap1RegisterResponse {
    pub version: Ctap1Version,
    pub public_key: Vec<u8>,
    pub key_handle: Vec<u8>,
    pub attestation: Vec<u8>,
    pub signature: Vec<u8>,
}

impl TryFrom<ApduResponse> for Ctap1RegisterResponse {
    type Error = IOError;

    fn try_from(apdu: ApduResponse) -> Result<Self, Self::Error> {
        if apdu.status()? != ApduResponseStatus::NoError {
            return Err(IOError::new(
                IOErrorKind::InvalidInput,
                "APDU packets need to have status NoError to be converted..",
            ));
        }

        let data = apdu.data.ok_or(IOError::new(
            IOErrorKind::InvalidInput,
            "Emtpy APDU packet.",
        ))?;

        let mut cursor = IOCursor::new(data);
        cursor.consume(1); // Reserved bytes.

        let mut public_key = vec![0u8; 65];
        cursor.read_exact(&mut public_key)?;

        let key_handle_len = cursor.read_u8()? as u64;
        let mut key_handle = vec![0u8; key_handle_len as usize];
        cursor.read_exact(&mut key_handle)?;

        let mut remaining = vec![];
        cursor.read_to_end(&mut remaining)?;

        let (signature, _) = X509Certificate::from_der(&remaining).or(Err(IOError::new(
            IOErrorKind::InvalidData,
            "Failed to parse X509 attestation data",
        )))?;
        let signature = Vec::from(signature);
        // `signature` is a suffix of `remaining` returned by `X509Certificate::from_der`,
        // so the split index is always within bounds in practice; bound it explicitly.
        let split_at = remaining
            .len()
            .checked_sub(signature.len())
            .ok_or_else(|| {
                IOError::new(
                    IOErrorKind::InvalidData,
                    "Signature longer than remaining U2F register response data",
                )
            })?;
        let attestation = Vec::from(remaining.get(..split_at).unwrap_or(&[]));

        Ok(Ctap1RegisterResponse {
            version: Ctap1Version::U2fV2,
            public_key,
            key_handle,
            attestation,
            signature,
        })
    }
}

impl Ctap1RegisterResponse {
    pub fn as_registered_key(&self) -> Result<Ctap1RegisteredKey, IOError> {
        Ok(Ctap1RegisteredKey::new_u2f_v2(&self.key_handle))
    }
}

#[derive(Debug, Clone)]
pub struct Ctap1SignRequest {
    pub app_id_hash: Vec<u8>,
    pub challenge: Vec<u8>,
    pub key_handle: Vec<u8>,
    pub timeout: Duration,
    pub require_user_presence: bool,
}

impl Ctap1SignRequest {
    pub fn new(
        app_id: &str,
        challenge: &[u8],
        key_handle: &[u8],
        timeout: Duration,
        require_user_presence: bool,
    ) -> Ctap1SignRequest {
        let mut hasher = Sha256::default();
        hasher.update(app_id);
        let app_id_hash = hasher.finalize().to_vec();

        Ctap1SignRequest {
            app_id_hash,
            challenge: Vec::from(challenge),
            key_handle: Vec::from(key_handle),
            timeout,
            require_user_presence,
        }
    }

    pub fn new_preflight(
        app_id_hash: &[u8],
        challenge: &[u8],
        key_handle: &[u8],
        timeout: Duration,
    ) -> Ctap1SignRequest {
        Ctap1SignRequest {
            app_id_hash: Vec::from(app_id_hash),
            challenge: Vec::from(challenge),
            key_handle: Vec::from(key_handle),
            timeout,
            require_user_presence: false,
        }
    }
}

#[derive(Debug)]
pub struct Ctap1VersionRequest {}

impl Default for Ctap1VersionRequest {
    fn default() -> Self {
        Self::new()
    }
}

impl Ctap1VersionRequest {
    pub fn new() -> Ctap1VersionRequest {
        Ctap1VersionRequest {}
    }
}

#[derive(Debug)]
pub struct Ctap1VersionResponse {
    pub version: Ctap1Version,
}

impl TryFrom<ApduResponse> for Ctap1VersionResponse {
    type Error = IOError;

    fn try_from(apdu: ApduResponse) -> Result<Self, Self::Error> {
        if apdu.status()? != ApduResponseStatus::NoError {
            return Err(IOError::new(
                IOErrorKind::InvalidInput,
                "APDU packets need to have status NoError to be converted..",
            ));
        }

        let data = apdu.data.ok_or(IOError::new(
            IOErrorKind::InvalidInput,
            "Emtpy APDU packet.",
        ))?;

        let version_string = String::from_utf8(data).or(Err(IOError::new(
            IOErrorKind::InvalidInput,
            "Invalid UTF-8 bytes in CTAP1 version string",
        )))?;

        let version = match version_string.as_str() {
            "U2F_V2" => Ctap1Version::U2fV2,
            _ => {
                return Err(IOError::new(
                    IOErrorKind::InvalidInput,
                    format!("Invalid CTAP1 version string: {:}", version_string),
                ))
            }
        };

        Ok(Ctap1VersionResponse { version })
    }
}

#[derive(Debug, Clone)]
pub struct Ctap1SignResponse {
    pub user_presence_verified: bool,
    pub counter: u32,
    pub signature: Vec<u8>,
}

impl TryFrom<ApduResponse> for Ctap1SignResponse {
    type Error = IOError;

    fn try_from(apdu: ApduResponse) -> Result<Self, Self::Error> {
        if apdu.status()? != ApduResponseStatus::NoError {
            return Err(IOError::new(
                IOErrorKind::InvalidInput,
                "APDU packets need to have status NoError to be converted..",
            ));
        }

        let data = apdu.data.ok_or(IOError::new(
            IOErrorKind::InvalidInput,
            "Emtpy APDU packet.",
        ))?;

        let mut cursor = IOCursor::new(data);
        let user_presence_verified = cursor.read_u8()? == 0x01;
        let counter = cursor.read_u32::<BigEndian>()?;

        let mut signature = vec![];
        cursor.read_to_end(&mut signature)?;

        Ok(Ctap1SignResponse {
            user_presence_verified,
            counter,
            signature,
        })
    }
}

pub trait Preflight<P>: Sized {
    /// Modify request in place, removing items in exclusion list, in favour of generating new pre-fligh requests.
    fn preflight(&self) -> Result<(Self, Vec<P>), CtapError>;
}

impl Preflight<Ctap1SignRequest> for Ctap1RegisterRequest {
    fn preflight(&self) -> Result<(Self, Vec<Ctap1SignRequest>), CtapError> {
        let preflight_requests: Vec<Ctap1SignRequest> = self
            .registered_keys
            .iter()
            .map(|registered_key| {
                Ctap1SignRequest::new_preflight(
                    &self.app_id_hash,
                    &[0u8; 32],
                    &registered_key.key_handle,
                    self.timeout,
                )
            })
            .collect();

        let mut modified = self.to_owned();
        modified.registered_keys = vec![];
        Ok((modified, preflight_requests))
    }
}

#[cfg(test)]
mod tests {
    use crate::proto::ctap1::apdu::ApduResponse;
    use crate::proto::ctap1::Ctap1RegisterResponse;
    use std::convert::TryInto;

    #[test]
    fn register_response_apdu_to_ctap1() {
        let apdu = hex::decode("05046DDBE3C25D974C9A403D6C648ED41C219D44734C43986B4053B325BE01C31E28F146731E5C21BA0E0E1938DA4C1FECAD650A2971A13CF6076BF52B52C19F8D0E40602CFD267868E84D4852BD5B008BC6CE0211D4858C8A647328A13B7D5C0A42B3893D63A58FCA7BD3EBB74F55CE537195DFF0113D4C561BBB7DFAC0C0ECD1AFB53082015930820100A003020102020102300A06082A8648CE3D0403023028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653022180F32303030303130313030303030305A180F32303939313233313233353935395A3028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653059301306072A8648CE3D020106082A8648CE3D030107034200040393AF897BE858E88C1953876A1A538477C4DA6E6EA14ACF0A2FD89A4DCCF95878A8CD2929029CC1D794BFFB9C37547CBBB5BB31AB3A6756ACF74F123CECD45CA31730153013060B2B0601040182E51C020101040403020470300A06082A8648CE3D040302034700304402207F958ABE6CF08CB2E9A03774D52DF8C0EA261E1AC0C283409FEDD8D36DFAF09302204EEB7501C720428D206E1B092D8D26CA8536B70F5F09AEA99562390BEF1BA7EC3044022031413D6E238A5F998B26B3931655C411847D99776B6E5CF15AA2E11BFAF325F00220098745DA82C11BB242934BAC6AE95155EAAD68520D695D46982DA9B2C94F94E3").unwrap();
        let apdu = ApduResponse::new_success(&apdu);
        let decoded: Ctap1RegisterResponse = apdu.try_into().unwrap();

        assert_eq!(decoded.public_key, hex::decode("046DDBE3C25D974C9A403D6C648ED41C219D44734C43986B4053B325BE01C31E28F146731E5C21BA0E0E1938DA4C1FECAD650A2971A13CF6076BF52B52C19F8D0E").unwrap());
        assert_eq!(decoded.key_handle, hex::decode("602CFD267868E84D4852BD5B008BC6CE0211D4858C8A647328A13B7D5C0A42B3893D63A58FCA7BD3EBB74F55CE537195DFF0113D4C561BBB7DFAC0C0ECD1AFB5").unwrap());
        assert_eq!(decoded.attestation, hex::decode("3082015930820100A003020102020102300A06082A8648CE3D0403023028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653022180F32303030303130313030303030305A180F32303939313233313233353935395A3028311530130603550403130C5365637572697479204B6579310F300D060355040A1306476F6F676C653059301306072A8648CE3D020106082A8648CE3D030107034200040393AF897BE858E88C1953876A1A538477C4DA6E6EA14ACF0A2FD89A4DCCF95878A8CD2929029CC1D794BFFB9C37547CBBB5BB31AB3A6756ACF74F123CECD45CA31730153013060B2B0601040182E51C020101040403020470300A06082A8648CE3D040302034700304402207F958ABE6CF08CB2E9A03774D52DF8C0EA261E1AC0C283409FEDD8D36DFAF09302204EEB7501C720428D206E1B092D8D26CA8536B70F5F09AEA99562390BEF1BA7EC").unwrap());
        assert_eq!(decoded.signature, hex::decode("3044022031413D6E238A5F998B26B3931655C411847D99776B6E5CF15AA2E11BFAF325F00220098745DA82C11BB242934BAC6AE95155EAAD68520D695D46982DA9B2C94F94E3").unwrap());
    }
}