use core::fmt::Debug;
use core::{convert::Infallible, error::Error};
use alloc::borrow::ToOwned;
use rand::CryptoRng;
use super::{ApplicationVerifier, picc_key};
use crate::TagTamperStatusReadout;
use crate::sdm::SdmUrlConfig;
use crate::types::file_settings::Sdm;
use crate::{
Access, AccessRights, AuthenticatedSession, CommMode, Configuration, EncryptedSession, File,
FileSettingsUpdate, KeyNumber, NonMasterKeyNumber, Session, SessionError, Transport, Version,
key_diversification::diversify_ntag424,
sdm::{SdmUrlOptions, Verifier, sdm_url_config},
types::file_settings::CryptoMode,
};
const SYSTEM_IDENTIFIER: &[u8] = b"NTAG424DNA";
const MODE: CryptoMode = CryptoMode::Lrp;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProvisioningError<E: Error + Debug, K: Error + Debug> {
#[error("PICC data does not contain a UID")]
NoUid,
#[error("verification error: {0}")]
SdmError(#[from] crate::sdm::SdmError),
#[error("sdm url error: {0}")]
SdmUrlError(#[from] crate::sdm::SdmUrlError),
#[error("session error: {0}")]
SessionError(#[from] SessionError<E>),
#[error("tag version mismatch: expected NTAG 424 DNA, got hardware type {hw_type:#04x}")]
VersionMismatch { hw_type: u8 },
#[error("tag is tampered ({0:?})")]
Tampered(TagTamperStatusReadout),
#[error("SDM verifier offset adjustment out of range; check URL length")]
Offset,
#[error("SDM URL must include UID mirroring (e.g. {{picc}} or {{uid}}) for provisioning")]
NoUidMirroring,
#[error("key generation failed: {0}")]
KeyGenerationError(K),
}
pub async fn provision<T: Transport>(
transport: &mut T,
url_template: &str,
master_key: &[u8; 16],
rng: &mut impl CryptoRng,
) -> Result<(ApplicationVerifier, [u8; 7]), ProvisioningError<T::Error, Infallible>> {
let key_fn = |uid| {
let new_keys = derive_keys_for_uid(master_key, &uid);
core::future::ready(Ok(new_keys))
};
provision_with_fn(transport, url_template, key_fn, rng).await
}
pub fn derive_keys_for_uid(master_key: &[u8; 16], uid: &[u8; 7]) -> [[u8; 16]; 5] {
let key1 = picc_key(master_key);
let new_master_key = diversify_ntag424(master_key, uid, KeyNumber::Key0, SYSTEM_IDENTIFIER);
[
new_master_key,
key1,
diversify_ntag424(master_key, uid, KeyNumber::Key2, SYSTEM_IDENTIFIER),
diversify_ntag424(master_key, uid, KeyNumber::Key3, SYSTEM_IDENTIFIER),
diversify_ntag424(master_key, uid, KeyNumber::Key4, SYSTEM_IDENTIFIER),
]
}
pub async fn provision_with_keys<T: Transport>(
transport: &mut T,
url_template: &str,
keys: &[[u8; 16]; 5],
rng: &mut impl CryptoRng,
) -> Result<(ApplicationVerifier, [u8; 7]), ProvisioningError<T::Error, Infallible>> {
provision_with_fn(
transport,
url_template,
|_| core::future::ready(Ok(*keys)),
rng,
)
.await
}
pub async fn provision_with_fn<T: Transport, F, Fut, K>(
transport: &mut T,
url_template: &str,
keys: F,
rng: &mut impl CryptoRng,
) -> Result<(ApplicationVerifier, [u8; 7]), ProvisioningError<T::Error, K>>
where
K: Error + Debug,
F: FnOnce([u8; 7]) -> Fut,
Fut: core::future::Future<Output = Result<[[u8; 16]; 5], K>>,
{
let (sdm_conf, verifier) = create_sdm_url_config(url_template, MODE)?;
if !sdm_conf.mirrors_uid() {
return Err(ProvisioningError::NoUidMirroring);
}
let prefix = sdm_conf.prefix();
let version = check_version(transport).await?;
let session = authenticate_using_factory_defaults(transport, rng).await?;
let (uid, session) = verify_originality(transport, session).await?;
let new_keys = keys(uid)
.await
.map_err(ProvisioningError::KeyGenerationError)?;
let session = configure(
transport,
session,
&version,
sdm_conf.sdm_settings.tamper_status().is_some(),
)
.await?;
let session = configure_ndef(transport, session, sdm_conf).await?;
provision_keys(transport, session, &new_keys).await?;
Ok((
ApplicationVerifier {
verifier,
url_template: url_template.to_owned(),
prefix: prefix.map(|p| p.to_owned()),
system_identifier: SYSTEM_IDENTIFIER.to_vec(),
},
uid,
))
}
fn create_sdm_url_config<E, K>(
url_template: &str,
mode: CryptoMode,
) -> Result<(SdmUrlConfig, Verifier), ProvisioningError<E, K>>
where
E: Error + Debug,
K: Error + Debug,
{
let opts = SdmUrlOptions {
picc_key: KeyNumber::Key1,
mac_key: KeyNumber::Key2,
..Default::default()
};
let sdm_conf = sdm_url_config(url_template, mode, opts)?;
let verifier = Verifier::try_new(&sdm_conf.sdm_settings, mode)?
.with_offset(sdm_conf.prefix_len as i32 - sdm_conf.offset as i32)
.ok_or_else(|| ProvisioningError::Offset)?;
Ok((sdm_conf, verifier))
}
pub fn create_app_verifier(
url_template: &str,
) -> Result<super::ApplicationVerifier, ProvisioningError<Infallible, Infallible>> {
let (sdm_conf, verifier) = create_sdm_url_config(url_template, MODE)?;
Ok(super::ApplicationVerifier {
verifier,
url_template: url_template.to_owned(),
prefix: sdm_conf.prefix().map(|p| p.to_owned()),
system_identifier: SYSTEM_IDENTIFIER.to_vec(),
})
}
async fn configure_ndef<T: Transport>(
transport: &mut T,
mut session: EncryptedSession,
config: crate::sdm::SdmUrlConfig,
) -> Result<EncryptedSession, SessionError<T::Error>> {
let sdm = config.sdm_settings;
session = session
.write_file(transport, File::Ndef, 0, &config.ndef_bytes)
.await?;
session = session
.change_file_settings(
transport,
File::Ndef,
&get_file_settings_update_for_sdm(sdm),
)
.await?;
let cc_bytes = crate::types::cc::CapabilityContainer::default()
.with_ndef_write_access(crate::types::cc::AccessCondition::Denied)
.to_bytes();
session = session
.write_file_with_mode(
transport,
File::CapabilityContainer,
0,
&cc_bytes,
CommMode::Plain,
)
.await?;
Ok(session)
}
fn get_file_settings_update_for_sdm(sdm: Sdm) -> FileSettingsUpdate {
FileSettingsUpdate::new(
CommMode::Plain,
AccessRights {
read: Access::Free,
write: Access::NoAccess,
read_write: Access::Key(KeyNumber::Key0),
change: Access::Key(KeyNumber::Key0),
},
)
.with_sdm(sdm)
}
async fn provision_keys<T: Transport>(
transport: &mut T,
mut session: EncryptedSession,
keys: &[[u8; 16]; 5],
) -> Result<(), SessionError<T::Error>> {
for (new_key, key_number) in keys[1..].iter().zip([
NonMasterKeyNumber::Key1,
NonMasterKeyNumber::Key2,
NonMasterKeyNumber::Key3,
NonMasterKeyNumber::Key4,
]) {
let old_key = [0u8; 16]; session = session
.change_key(transport, key_number, new_key, 0x01, &old_key)
.await?;
}
session.change_master_key(transport, &keys[0], 0x01).await?;
Ok(())
}
async fn configure<T: Transport, K: Error + Debug>(
transport: &mut T,
session: EncryptedSession,
version: &Version,
enable_tag_tamper: bool,
) -> Result<EncryptedSession, ProvisioningError<T::Error, K>> {
let mut config = Configuration::new().with_random_uid_enabled();
let tag_tamper_enabled = if version.has_tag_tamper_support() && enable_tag_tamper {
config = config.with_tag_tamper_enabled(Access::Free);
true
} else {
false
};
let mut session = session.set_configuration(transport, &config).await?;
if tag_tamper_enabled {
let (tt_status, new_session) = session.get_tt_status(transport).await?;
if tt_status.is_tampered() {
return Err(ProvisioningError::Tampered(tt_status));
}
session = new_session;
}
Ok(session)
}
async fn verify_originality<T: Transport>(
transport: &mut T,
session: EncryptedSession,
) -> Result<([u8; 7], EncryptedSession), SessionError<T::Error>> {
let (uid, session) = session.get_uid(transport).await?;
Ok((uid, session.verify_originality(transport, &uid).await?))
}
async fn check_version<T: Transport, K: Error + Debug>(
transport: &mut T,
) -> Result<Version, ProvisioningError<T::Error, K>> {
let version = Session::new().get_version(transport).await?;
if version.hw_type() != 0x04 {
return Err(ProvisioningError::VersionMismatch {
hw_type: version.hw_type(),
});
}
Ok(version)
}
async fn authenticate_using_factory_defaults<T: Transport>(
transport: &mut T,
rng: &mut impl CryptoRng,
) -> Result<EncryptedSession, SessionError<T::Error>> {
use rand::RngExt as _;
if let Ok(s) = Session::new()
.authenticate_aes(transport, KeyNumber::Key0, &[0; 16], rng.random())
.await
{
if MODE == CryptoMode::Lrp {
s.enable_lrp(transport).await?;
} else {
return Ok(s.into());
}
}
Session::new()
.authenticate_lrp(transport, KeyNumber::Key0, &[0; 16], rng.random())
.await
.map(EncryptedSession::from)
}