myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

cli_bootstrap.rs (4487B)


      1 //! Fixed, zeroizing bootstrap documents consumed only from standard input.
      2 
      3 use std::io::Read;
      4 
      5 use zeroize::Zeroizing;
      6 
      7 use crate::MycEncryptedIdentityProvisioningMaterial;
      8 
      9 pub(crate) const MYC_IDENTITY_PROVISIONING_DOCUMENT_BYTES: usize = 117;
     10 const MYC_IDENTITY_PROVISIONING_MAGIC: &[u8; 4] = b"MYIP";
     11 const MYC_IDENTITY_PROVISIONING_VERSION: u8 = 1;
     12 
     13 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     14 pub(crate) enum MycBootstrapDocumentError {
     15     Io,
     16     InvalidLength,
     17     InvalidHeader,
     18     InvalidMaterial,
     19 }
     20 
     21 pub(crate) fn read_identity_provisioning_document(
     22     mut reader: impl Read,
     23 ) -> Result<MycEncryptedIdentityProvisioningMaterial, MycBootstrapDocumentError> {
     24     let mut document = Zeroizing::new([0_u8; MYC_IDENTITY_PROVISIONING_DOCUMENT_BYTES + 1]);
     25     let mut length = 0_usize;
     26     while length < document.len() {
     27         match reader.read(&mut document[length..]) {
     28             Ok(0) => break,
     29             Ok(read) => length = length.saturating_add(read),
     30             Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
     31             Err(_) => return Err(MycBootstrapDocumentError::Io),
     32         }
     33     }
     34     if length != MYC_IDENTITY_PROVISIONING_DOCUMENT_BYTES {
     35         return Err(MycBootstrapDocumentError::InvalidLength);
     36     }
     37     if &document[..4] != MYC_IDENTITY_PROVISIONING_MAGIC
     38         || document[4] != MYC_IDENTITY_PROVISIONING_VERSION
     39     {
     40         return Err(MycBootstrapDocumentError::InvalidHeader);
     41     }
     42 
     43     let mut identity_secret = [0_u8; 32];
     44     let mut data_key = [0_u8; 32];
     45     let mut envelope_nonce = [0_u8; 24];
     46     let mut wrapping_nonce = [0_u8; 24];
     47     identity_secret.copy_from_slice(&document[5..37]);
     48     data_key.copy_from_slice(&document[37..69]);
     49     envelope_nonce.copy_from_slice(&document[69..93]);
     50     wrapping_nonce.copy_from_slice(&document[93..117]);
     51     MycEncryptedIdentityProvisioningMaterial::new(
     52         identity_secret,
     53         data_key,
     54         envelope_nonce,
     55         wrapping_nonce,
     56     )
     57     .map_err(|_| MycBootstrapDocumentError::InvalidMaterial)
     58 }
     59 
     60 #[cfg(test)]
     61 mod tests {
     62     use std::io::{self, Cursor};
     63 
     64     use super::*;
     65 
     66     fn valid_document() -> [u8; MYC_IDENTITY_PROVISIONING_DOCUMENT_BYTES] {
     67         let mut document = [0_u8; MYC_IDENTITY_PROVISIONING_DOCUMENT_BYTES];
     68         document[..4].copy_from_slice(MYC_IDENTITY_PROVISIONING_MAGIC);
     69         document[4] = MYC_IDENTITY_PROVISIONING_VERSION;
     70         document[5..37].copy_from_slice(&[1; 32]);
     71         document[37..69].copy_from_slice(&[2; 32]);
     72         document[69..93].copy_from_slice(&[3; 24]);
     73         document[93..117].copy_from_slice(&[4; 24]);
     74         document
     75     }
     76 
     77     #[test]
     78     fn exact_document_is_admitted_and_debug_is_redacted() {
     79         let material = read_identity_provisioning_document(Cursor::new(valid_document()))
     80             .expect("exact provisioning document");
     81         assert_eq!(
     82             format!("{material:?}"),
     83             "MycEncryptedIdentityProvisioningMaterial([redacted])"
     84         );
     85     }
     86 
     87     #[test]
     88     fn short_trailing_wrong_header_and_invalid_material_fail_closed() {
     89         let valid = valid_document();
     90         assert_eq!(
     91             read_identity_provisioning_document(Cursor::new(&valid[..116])).unwrap_err(),
     92             MycBootstrapDocumentError::InvalidLength
     93         );
     94         let mut trailing = valid.to_vec();
     95         trailing.push(0);
     96         assert_eq!(
     97             read_identity_provisioning_document(Cursor::new(trailing)).unwrap_err(),
     98             MycBootstrapDocumentError::InvalidLength
     99         );
    100         for index in [0, 4] {
    101             let mut invalid = valid;
    102             invalid[index] ^= 0xff;
    103             assert_eq!(
    104                 read_identity_provisioning_document(Cursor::new(invalid)).unwrap_err(),
    105                 MycBootstrapDocumentError::InvalidHeader
    106             );
    107         }
    108         let mut invalid = valid;
    109         invalid[37..69].fill(0);
    110         assert_eq!(
    111             read_identity_provisioning_document(Cursor::new(invalid)).unwrap_err(),
    112             MycBootstrapDocumentError::InvalidMaterial
    113         );
    114     }
    115 
    116     struct FailingReader;
    117 
    118     impl Read for FailingReader {
    119         fn read(&mut self, _buffer: &mut [u8]) -> io::Result<usize> {
    120             Err(io::Error::other("sensitive source"))
    121         }
    122     }
    123 
    124     #[test]
    125     fn input_errors_discard_the_raw_source() {
    126         assert_eq!(
    127             read_identity_provisioning_document(FailingReader).unwrap_err(),
    128             MycBootstrapDocumentError::Io
    129         );
    130     }
    131 }