Skip to content

Commit

Permalink
Abstract attestation support
Browse files Browse the repository at this point in the history
  • Loading branch information
ia0 committed Jul 5, 2022
1 parent aee7d7c commit 80a6b82
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 165 deletions.
110 changes: 110 additions & 0 deletions src/api/attestation_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use alloc::string::String;
use alloc::vec::Vec;
use persistent_store::{StoreError, StoreUpdate};

use crate::env::Env;

/// Identifies an attestation.
pub enum Id {
Batch,
Enterprise { rp_id: String },
}

#[cfg_attr(feature = "std", derive(Debug, PartialEq, Eq))]
pub struct Attestation {
/// ECDSA private key (big-endian).
pub private_key: [u8; 32],
pub certificate: Vec<u8>,
}

/// Stores enterprise or batch attestations.
///
/// Implementations don't need to distinguish different attestations. In particular, setting one
/// attestation may set other ones.
pub trait AttestationStore {
/// Returns an attestation given its id, if it exists.
///
/// This should always return the attestation. Checking whether it is ok to use the attestation
/// is done in the CTAP library.
fn get(&mut self, id: &Id) -> Result<Option<Attestation>, Error>;

/// Sets the attestation for a given id.
///
/// This function may not be supported.
fn set(&mut self, id: &Id, attestation: Option<&Attestation>) -> Result<(), Error>;
}

/// Attestation store errors.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
Storage,
Internal,
NoSupport,
}

/// Keys of the environment store reserved for the attestation store.
pub const STORAGE_KEYS: &[usize] = &[1, 2];

/// Implements a default attestation store using the environment store.
///
/// The same attestation is used for batch and enterprise.
pub trait Helper: Env {}

impl<T: Helper> AttestationStore for T {
fn get(&mut self, _: &Id) -> Result<Option<Attestation>, Error> {
let private_key = self.store().find(PRIVATE_KEY_STORAGE_KEY)?;
let certificate = self.store().find(CERTIFICATE_STORAGE_KEY)?;
let (private_key, certificate) = match (private_key, certificate) {
(Some(x), Some(y)) => (x, y),
(None, None) => return Ok(None),
_ => return Err(Error::Internal),
};
if private_key.len() != 32 {
return Err(Error::Internal);
}
Ok(Some(Attestation {
private_key: *array_ref![private_key, 0, 32],
certificate,
}))
}

fn set(&mut self, _: &Id, attestation: Option<&Attestation>) -> Result<(), Error> {
let updates = match attestation {
None => [
StoreUpdate::Remove {
key: PRIVATE_KEY_STORAGE_KEY,
},
StoreUpdate::Remove {
key: CERTIFICATE_STORAGE_KEY,
},
],
Some(attestation) => [
StoreUpdate::Insert {
key: PRIVATE_KEY_STORAGE_KEY,
value: &attestation.private_key[..],
},
StoreUpdate::Insert {
key: CERTIFICATE_STORAGE_KEY,
value: &attestation.certificate[..],
},
],
};
Ok(self.store().transaction(&updates)?)
}
}

const PRIVATE_KEY_STORAGE_KEY: usize = STORAGE_KEYS[0];
const CERTIFICATE_STORAGE_KEY: usize = STORAGE_KEYS[1];

impl From<StoreError> for Error {
fn from(error: StoreError) -> Self {
match error {
StoreError::InvalidArgument
| StoreError::NoCapacity
| StoreError::NoLifetime
| StoreError::InvalidStorage => Error::Internal,
StoreError::StorageError => Error::Storage,
}
}
}
1 change: 1 addition & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! The [environment](crate::env::Env) is split into components. Each component has an API described
//! by a trait. This module gathers the API of those components.

pub mod attestation_store;
pub mod connection;
pub mod customization;
pub mod firmware_protection;
Expand Down
40 changes: 17 additions & 23 deletions src/ctap/ctap1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use super::super::clock::CtapInstant;
use super::apdu::{Apdu, ApduStatusCode};
use super::crypto_wrapper::{decrypt_credential_source, encrypt_key_handle, PrivateKey};
use super::CtapState;
use crate::ctap::storage;
use crate::api::attestation_store::{self, Attestation, AttestationStore};
use crate::env::Env;
use alloc::vec::Vec;
use arrayref::array_ref;
Expand Down Expand Up @@ -257,12 +257,14 @@ impl Ctap1Command {
return Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION);
}

let certificate = storage::attestation_certificate(env)
let Attestation {
private_key,
certificate,
} = env
.attestation_store()
.get(&attestation_store::Id::Batch)
.map_err(|_| Ctap1StatusCode::SW_MEMERR)?
.ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;
let private_key = storage::attestation_private_key(env)
.map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?
.ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?;

let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len());
response.push(Ctap1Command::LEGACY_BYTE);
Expand Down Expand Up @@ -345,10 +347,10 @@ impl Ctap1Command {
mod test {
use super::super::crypto_wrapper::ECDSA_CREDENTIAL_ID_SIZE;
use super::super::data_formats::SignatureAlgorithm;
use super::super::key_material;
use super::*;
use crate::api::customization::Customization;
use crate::clock::TEST_CLOCK_FREQUENCY_HZ;
use crate::ctap::storage;
use crate::env::test::TestEnv;
use crypto::Hash256;

Expand Down Expand Up @@ -423,21 +425,13 @@ mod test {
// Certificate and private key are missing
assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));

let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH];
assert!(storage::set_attestation_private_key(&mut env, &fake_key).is_ok());
ctap_state.u2f_up_state.consume_up(CtapInstant::new(0));
ctap_state.u2f_up_state.grant_up(CtapInstant::new(0));
let response = Ctap1Command::process_command(
&mut env,
&message,
&mut ctap_state,
CtapInstant::new(0_u64),
);
// Certificate is still missing
assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION));

let fake_cert = [0x99u8; 100]; // Arbitrary length
assert!(storage::set_attestation_certificate(&mut env, &fake_cert[..]).is_ok());
let attestation = Attestation {
private_key: [0x41; 32],
certificate: vec![0x99; 100],
};
env.attestation_store()
.set(&attestation_store::Id::Batch, Some(&attestation))
.unwrap();
ctap_state.u2f_up_state.consume_up(CtapInstant::new(0));
ctap_state.u2f_up_state.grant_up(CtapInstant::new(0));
let response =
Expand All @@ -454,8 +448,8 @@ mod test {
.is_some());
const CERT_START: usize = 67 + ECDSA_CREDENTIAL_ID_SIZE;
assert_eq!(
&response[CERT_START..CERT_START + fake_cert.len()],
&fake_cert[..]
&response[CERT_START..][..attestation.certificate.len()],
&attestation.certificate
);
}

Expand Down
106 changes: 50 additions & 56 deletions src/ctap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ use self::status_code::Ctap2StatusCode;
use self::timed_permission::TimedPermission;
#[cfg(feature = "with_ctap1")]
use self::timed_permission::U2fUserPresenceState;
use crate::api::attestation_store::{self, Attestation, AttestationStore};
use crate::api::connection::{HidConnection, SendOrRecvStatus};
use crate::api::customization::Customization;
use crate::api::firmware_protection::FirmwareProtection;
Expand Down Expand Up @@ -860,7 +861,7 @@ impl CtapState {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: random_id.clone(),
private_key: private_key.clone(),
rp_id,
rp_id: rp_id.clone(),
user_handle: user.user_id,
// This input is user provided, so we crop it to 64 byte for storage.
// The UTF8 encoding is always preserved, so the string might end up shorter.
Expand Down Expand Up @@ -918,21 +919,31 @@ impl CtapState {
let mut signature_data = auth_data.clone();
signature_data.extend(client_data_hash);

let (signature, x5c) = if env.customization().use_batch_attestation() || ep_att {
let attestation_private_key = storage::attestation_private_key(env)?
.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?;
let attestation_key =
crypto::ecdsa::SecKey::from_bytes(&attestation_private_key).unwrap();
let attestation_certificate = storage::attestation_certificate(env)?
.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?;
(
attestation_key
.sign_rfc6979::<Sha256>(&signature_data)
.to_asn1_der(),
Some(vec![attestation_certificate]),
)
let attestation_id = if env.customization().use_batch_attestation() {
Some(attestation_store::Id::Batch)
} else if ep_att {
Some(attestation_store::Id::Enterprise { rp_id })
} else {
(private_key.sign_and_encode(env, &signature_data)?, None)
None
};
let (signature, x5c) = match attestation_id {
Some(id) => {
let Attestation {
private_key,
certificate,
} = env
.attestation_store()
.get(&id)?
.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?;
let attestation_key = crypto::ecdsa::SecKey::from_bytes(&private_key).unwrap();
(
attestation_key
.sign_rfc6979::<Sha256>(&signature_data)
.to_asn1_der(),
Some(vec![certificate]),
)
}
None => (private_key.sign_and_encode(env, &signature_data)?, None),
};
let attestation_statement = PackedAttestationStatement {
alg: SignatureAlgorithm::ES256 as i64,
Expand Down Expand Up @@ -1338,41 +1349,26 @@ impl CtapState {
if params.attestation_material.is_some() || params.lockdown {
check_user_presence(env, channel)?;
}
// This command is for U2F support and we use the batch attestation there.
let attestation_id = attestation_store::Id::Batch;

// Sanity checks
let current_priv_key = storage::attestation_private_key(env)?;
let current_cert = storage::attestation_certificate(env)?;

let current_attestation = env.attestation_store().get(&attestation_id)?;
let response = match params.attestation_material {
// Only reading values.
None => AuthenticatorVendorConfigureResponse {
cert_programmed: current_cert.is_some(),
pkey_programmed: current_priv_key.is_some(),
cert_programmed: current_attestation.is_some(),
pkey_programmed: current_attestation.is_some(),
},
// Device is already fully programmed. We don't leak information.
Some(_) if current_cert.is_some() && current_priv_key.is_some() => {
AuthenticatorVendorConfigureResponse {
cert_programmed: true,
pkey_programmed: true,
}
}
// Device is partially or not programmed. We complete the process.
Some(data) => {
if let Some(current_cert) = &current_cert {
if current_cert != &data.certificate {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
}
if let Some(current_priv_key) = &current_priv_key {
if current_priv_key != &data.private_key {
return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER);
}
}
if current_cert.is_none() {
storage::set_attestation_certificate(env, &data.certificate)?;
}
if current_priv_key.is_none() {
storage::set_attestation_private_key(env, &data.private_key)?;
// We don't overwrite the attestation if it's already set. We don't return any error
// to not leak information.
if current_attestation.is_none() {
let attestation = Attestation {
private_key: data.private_key,
certificate: data.certificate,
};
env.attestation_store()
.set(&attestation_id, Some(&attestation))?;
}
AuthenticatorVendorConfigureResponse {
cert_programmed: true,
Expand Down Expand Up @@ -3145,12 +3141,11 @@ mod test {
))
);
assert_eq!(
storage::attestation_certificate(&mut env).unwrap().unwrap(),
dummy_cert
);
assert_eq!(
storage::attestation_private_key(&mut env).unwrap().unwrap(),
dummy_key
env.attestation_store().get(&attestation_store::Id::Batch),
Ok(Some(Attestation {
private_key: dummy_key,
certificate: dummy_cert.to_vec(),
}))
);

// Try to inject other dummy values and check that initial values are retained.
Expand All @@ -3176,12 +3171,11 @@ mod test {
))
);
assert_eq!(
storage::attestation_certificate(&mut env).unwrap().unwrap(),
dummy_cert
);
assert_eq!(
storage::attestation_private_key(&mut env).unwrap().unwrap(),
dummy_key
env.attestation_store().get(&attestation_store::Id::Batch),
Ok(Some(Attestation {
private_key: dummy_key,
certificate: dummy_cert.to_vec(),
}))
);

// Now try to lock the device
Expand Down
13 changes: 12 additions & 1 deletion src/ctap/status_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::api::key_store;
use crate::api::user_presence::UserPresenceError;
use crate::api::{attestation_store, key_store};

// CTAP specification (version 20190130) section 6.3
// For now, only the CTAP2 codes are here, the CTAP1 are not included.
Expand Down Expand Up @@ -100,3 +100,14 @@ impl From<key_store::Error> for Ctap2StatusCode {
Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR
}
}

impl From<attestation_store::Error> for Ctap2StatusCode {
fn from(error: attestation_store::Error) -> Self {
use attestation_store::Error;
match error {
Error::Storage => Self::CTAP2_ERR_VENDOR_HARDWARE_FAILURE,
Error::Internal => Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
Error::NoSupport => Self::CTAP2_ERR_VENDOR_INTERNAL_ERROR,
}
}
}
Loading

0 comments on commit 80a6b82

Please sign in to comment.