diff --git a/Cargo.toml b/Cargo.toml index ac576d7..668a1d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ talktosc = "0.1.3" sshkeys = "0.3.2" [dependencies.pyo3] -version = "0.17.2" +version = "0.17.3" [package.metadata.maturin] diff --git a/changelog.md b/changelog.md index 159614c..0ac2537 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,13 @@ - Type annotation for the rust part of the codebase. - `can_primary_expire` new argument to `create_key` function call. - Updated `pyo3` dependency to `0.17.2`. +- Adds `get_card_version` in rjce. +- Adds `TouchMode` enum in rjce. +- Adds `get_card_touch_policies` function to find available options. +- Adds `KeySlot` enum in rjce +- Adds `get_keyslot_touch_policy` function to set touch policy. +- Adds `set_keyslot_touch_policy` function to set touch policy. +- Updates pyo3 to `0.17.3` ## [0.10.0] - 2022-09-20 diff --git a/docs/api.rst b/docs/api.rst index b01cd61..276b04b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -279,3 +279,10 @@ For the rest of the documentation we assume that you imported the module as foll Enum class to mark the kind of certification one can do on another key. Possible values are **SignatureType.GenericCertification**, **SignatureType.PersonaCertification**, **SignatureType.CasualCertification**, **SignatureType.PositiveCertification**. + + +.. function:: get_card_touch_policies() -> List[TouchMode] + + Returns a list of Enum values from TouchMode. To be used to determine the touch capabilities of the smartcard. + Remember to verify this list before calling :func:`set_keyslot_touch_policy`. + diff --git a/docs/smartcard.rst b/docs/smartcard.rst index c996ad1..5e61ea4 100644 --- a/docs/smartcard.rst +++ b/docs/smartcard.rst @@ -18,6 +18,42 @@ The part of the code is written in Rust, so you will have to import the internal Smartcard API -------------- +.. class:: KeySlot + + These are the available KeySlots in a card. + + .. py:attribute:: Signature + .. py:attribute:: Encryption + .. py:attribute:: Authentication + .. py:attribute:: Attestation + + + +.. class:: TouchMode + + The different touch mode for a key. + + .. py:attribute:: Off + .. py:attribute:: On + .. py:attribute:: Fixed + .. py:attribute:: Cached + .. py:attribute:: CachedFixed + + + +.. function:: set_keyslot_touch_policy(adminpin: bytes, slot: KeySlot, mode: TouchMode) -> bool: + + Sets the given `TouchMode` to the slot. Returns False if it is already set as Fixed. + +.. important:: Remember to verify the available touch modes via :func:`get_card_touch_policies` first. + +.. function:: get_keyslot_touch_policy(slot: KeySlot) -> TouchMode: + + Returns the available `TouchMode` of the given slot in the smartcard. + +.. function:: get_card_version() -> tuple[int, int, int]: + + Returns a tuple containing the Yubikey firmware version. Example: (5,2,7) or (4,3,1). .. function:: reset_yubikey() -> bool: diff --git a/johnnycanencrypt/__init__.py b/johnnycanencrypt/__init__.py index a61025f..50b57d0 100644 --- a/johnnycanencrypt/__init__.py +++ b/johnnycanencrypt/__init__.py @@ -26,6 +26,7 @@ merge_keys, parse_cert_bytes, parse_cert_file, + TouchMode, ) import johnnycanencrypt.johnnycanencrypt as rjce @@ -276,7 +277,12 @@ def certify_key( other_k = otherkey cert = rjce.certify_key( - k.keyvalue, other_k.keyvalue, sig_type.value, uids, password.encode("utf-8"), oncard + k.keyvalue, + other_k.keyvalue, + sig_type.value, + uids, + password.encode("utf-8"), + oncard, ) # Now if the otherkey is secret, then merge this new public key into the secret key if other_k.keytype == KeyType.SECRET: @@ -1020,7 +1026,13 @@ def _find_keys(self, keys: List[Union[str, Key]]): final_keys.append(k.keyvalue) return final_keys - def encrypt(self, keys: Union[List[Union[str, Key]], Union[str, Key]], data: Union[str, bytes], outputfile: Union[str, bytes]="", armor=True): + def encrypt( + self, + keys: Union[List[Union[str, Key]], Union[str, Key]], + data: Union[str, bytes], + outputfile: Union[str, bytes] = "", + armor=True, + ): """Encrypts the given data with the list of keys and returns the output. :param keys: List of fingerprints or Key objects @@ -1120,7 +1132,9 @@ def encrypt_file(self, keys, inputfilepath, outputfilepath, armor=True): encrypt_filehandler_to_file(final_key_paths, fh, encrypted_file, armor) return True - def decrypt_file(self, key: Union[str, Key], encrypted_path, outputfile, password=""): + def decrypt_file( + self, key: Union[str, Key], encrypted_path, outputfile, password="" + ): """Decryptes the given file to the output path. :param key: Fingerprint or secret Key object @@ -1189,7 +1203,9 @@ def sign_detached(self, key: Union[str, Key], data: Union[str, bytes], password) jp = Johnny(k.keyvalue) return jp.sign_bytes_detached(data, password) - def verify(self, key: Union[str, Key], data: Union[str, bytes], signature: Optional[str]) -> bool: + def verify( + self, key: Union[str, Key], data: Union[str, bytes], signature: Optional[str] + ) -> bool: """Verifies the given data and the signature :param key: Fingerprint or public Key object @@ -1212,7 +1228,14 @@ def verify(self, key: Union[str, Key], data: Union[str, bytes], signature: Optio else: return jp.verify_bytes(data) - def sign_file(self, key: Union[str, Key], filepath: Union[str, bytes], outputpath: Union[str, bytes], password, cleartext=False) -> bool: + def sign_file( + self, + key: Union[str, Key], + filepath: Union[str, bytes], + outputpath: Union[str, bytes], + password, + cleartext=False, + ) -> bool: """Signs the given input file with key and saves in the outputpath. :param key: Fingerprint or secret Key object, public key in case card based operation. @@ -1256,7 +1279,13 @@ def sign_file(self, key: Union[str, Key], filepath: Union[str, bytes], outputpat return result - def sign_file_detached(self, key: Union[str, Key], filepath: Union[str, bytes], password: str, write=False): + def sign_file_detached( + self, + key: Union[str, Key], + filepath: Union[str, bytes], + password: str, + write=False, + ): """Signs the given data with the key. It also writes filename.asc in the same directory of the file as the signature if write value is True. :param key: Fingerprint or secret Key object @@ -1294,7 +1323,9 @@ def sign_file_detached(self, key: Union[str, Key], filepath: Union[str, bytes], return signature - def verify_file_detached(self, key: Union[str, Key], filepath: Union[str, bytes], signature_path): + def verify_file_detached( + self, key: Union[str, Key], filepath: Union[str, bytes], signature_path + ): """Verifies the given filepath based on the signature file. :param key: Fingerprint or public Key object @@ -1348,7 +1379,9 @@ def verify_file(self, key: Union[str, Key], filepath): jp = Johnny(k.keyvalue) return jp.verify_file(input_filepath) - def verify_and_extract_bytes(self, key: Union[str, Key], data: Union[str, bytes]) -> bytes: + def verify_and_extract_bytes( + self, key: Union[str, Key], data: Union[str, bytes] + ) -> bytes: """Verifies the given data and returns the acutal data. :param key: Fingerprint or public Key object. @@ -1367,7 +1400,9 @@ def verify_and_extract_bytes(self, key: Union[str, Key], data: Union[str, bytes] return jp.verify_and_extract_bytes(data) - def verify_and_extract_file(self, key: Union[str, Key], filepath: Union[str, bytes], output: bytes) -> bool: + def verify_and_extract_file( + self, key: Union[str, Key], filepath: Union[str, bytes], output: bytes + ) -> bool: """Verifies the given signed file and saves the actual data in output. :param key: Fingerprint or public Key object. @@ -1397,7 +1432,6 @@ def verify_and_extract_file(self, key: Union[str, Key], filepath: Union[str, byt return jp.verify_and_extract_file(input_filepath, outputpath) - def fetch_key_by_fingerprint(self, fingerprint: str): """Fetches key from keys.openpgp.org based on the fingerprint. @@ -1502,3 +1536,23 @@ def sync_smartcard(self): fingerprint = result["fingerprint"] return fingerprint + + +def get_card_touch_policies() -> Union[List[TouchMode], None]: + "Get the supported touch policies of the smartcard" + result: List[TouchMode] = [] + version = rjce.get_card_version() + if version < (4, 2, 0): + result = [] + elif version < (5, 2, 1): + result = [TouchMode.On, TouchMode.Off, TouchMode.Fixed] + elif version >= (5, 2, 1): + result = [ + TouchMode.On, + TouchMode.Off, + TouchMode.Fixed, + TouchMode.Cached, + TouchMode.CachedFixed, + ] + # Now return the result + return result diff --git a/johnnycanencrypt/johnnycanencrypt.pyi b/johnnycanencrypt/johnnycanencrypt.pyi index f070234..b110f57 100644 --- a/johnnycanencrypt/johnnycanencrypt.pyi +++ b/johnnycanencrypt/johnnycanencrypt.pyi @@ -1,9 +1,26 @@ +import io from typing import List, Dict, Optional, Tuple, Any, BinaryIO from datetime import datetime +from enum import IntEnum class CryptoError(BaseException): ... class SameKeyError(BaseException): ... +class TouchMode(IntEnum): + Off: int + On: int + Fixed: int + Cached: int + CachedFixed: int + +class KeySlot(IntEnum): + Signature: int + Encryption: int + Authentication: int + Attestation: int + +def set_keyslot_touch_policy(adminpin: bytes, slot: KeySlot, mode: TouchMode) -> bool: ... +def get_keyslot_touch_policy(slot: KeySlot) -> TouchMode: ... def update_subkeys_expiry_in_cert( certdata: bytes, fingerprints: List[str], expirytime: int, password: str ) -> bytes: ... @@ -81,6 +98,7 @@ def encrypt_file_internal( publickeys: List[List[bytes]], filepath: bytes, output: bytes, armor: Optional[bool] ) -> bytes: ... def is_smartcard_connected() -> bool: ... +def get_card_version() -> tuple[int, int, int]: ... class Johnny: def __init__(self, certdata: bytes) -> Johnny: ... diff --git a/smartcardtests/smartcards_for_primary.py b/smartcardtests/smartcards_for_primary.py index 62a73e1..917a50b 100644 --- a/smartcardtests/smartcards_for_primary.py +++ b/smartcardtests/smartcards_for_primary.py @@ -83,7 +83,15 @@ ks.sync_smartcard() other = ks.import_key("tests/files/store/kushal_updated_key.asc") -newother = ks.certify_key(k, other, ["Kushal Das ",], jce.SignatureType.PersonaCertification, password="123456".encode("utf-8"), oncard=True) +newother = ks.certify_key( + k, + other, + [ + "Kushal Das ", + ], + jce.SignatureType.PersonaCertification, + password="123456", + oncard=True, +) with open("hello.public", "wb") as f: f.write(newother.keyvalue) - diff --git a/src/lib.rs b/src/lib.rs index 4f417f4..42c5af4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3029,6 +3029,89 @@ pub fn is_smartcard_connected() -> PyResult { } } +/// Returns a tuple with the firmware version. +#[pyfunction] +pub fn get_card_version(py: Python) -> Result { + let data = match scard::internal_get_version() { + Ok(value) => value, + Err(_) => return Err(JceError::new("Can not get Yubikey version".to_string())), + }; + let result = PyTuple::new(py, data.iter()); + Ok(result.into()) +} + +/// TouchMode for Yubikeys +#[pyclass] +#[derive(Clone, Debug)] +pub enum TouchMode { + Off = 0x00, + On = 0x01, + Fixed = 0x02, + Cached = 0x03, + CachedFixed = 0x04, +} + +#[pyclass] +#[derive(Clone, Debug)] +pub enum KeySlot { + Signature = 0xD6, + Encryption = 0xD7, + Authentication = 0xD8, + Attestation = 0xD9, +} + +#[pyfunction] +pub fn get_keyslot_touch_policy(py: Python, slot: Py) -> Result> { + let actual_slot = slot.extract(py)?; + let data = match scard::get_touch_policy(actual_slot) { + Ok(value) => value, + Err(e) => return Err(JceError::new(e.to_string())), + }; + match data[0] { + 0 => { + return Ok(Py::new(py, TouchMode::Off)?); + } + 1 => { + return Ok(Py::new(py, TouchMode::On)?); + } + 2 => { + return Ok(Py::new(py, TouchMode::Fixed)?); + } + 3 => { + return Ok(Py::new(py, TouchMode::Cached)?); + } + 4 => { + return Ok(Py::new(py, TouchMode::CachedFixed)?); + } + _ => { + return Err(JceError::new("Can not parse touch policy.".to_string())); + } + } +} + +#[pyfunction] +pub fn set_keyslot_touch_policy( + py: Python, + adminpin: Vec, + slot: Py, + mode: Py, +) -> Result { + let actual_slot: KeySlot = slot.extract(py).unwrap(); + let slot_value = actual_slot as u8; + + let actual_mode: TouchMode = mode.extract(py).unwrap(); + let mode_value = actual_mode as u8; + + let pw3_apdu = talktosc::apdus::create_apdu_verify_pw3(adminpin); + // 0x20 is for the touch mode button + let touch_apdu = apdus::APDU::new(0x00, 0xDA, 0x00, slot_value, Some(vec![mode_value, 0x20])); + + match scard::set_data(pw3_apdu, touch_apdu) { + Ok(value) => Ok(value), + Err(value) => Err(CardError::new_err(format!("Error {}", value)).into()), + } +} + /// A Python module implemented in Rust. #[pymodule] fn johnnycanencrypt(_py: Python, m: &PyModule) -> PyResult<()> { @@ -3065,8 +3148,13 @@ fn johnnycanencrypt(_py: Python, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(certify_key))?; m.add_wrapped(wrap_pyfunction!(get_ssh_pubkey))?; m.add_wrapped(wrap_pyfunction!(get_signing_pubkey))?; + m.add_wrapped(wrap_pyfunction!(get_card_version))?; + m.add_wrapped(wrap_pyfunction!(get_keyslot_touch_policy))?; + m.add_wrapped(wrap_pyfunction!(set_keyslot_touch_policy))?; m.add("CryptoError", _py.get_type::())?; m.add("SameKeyError", _py.get_type::())?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/scard.rs b/src/scard.rs index d11791f..0c76201 100644 --- a/src/scard.rs +++ b/src/scard.rs @@ -3,6 +3,7 @@ use crate::openpgp::packet::key; use crate::openpgp::types::SymmetricAlgorithm; +use crate::KeySlot; use openpgp::crypto; use openpgp::packet::prelude::*; use sequoia_openpgp as openpgp; @@ -26,18 +27,24 @@ pub fn chagne_admin_pin(pw3change: apdus::APDU) -> Result resp.unwrap(), - Err(value) => return Err(value), + Err(value) => { + talktosc::disconnect(card); + return Err(value); + } }; // Verify if the admin pin worked or not. if !resp.is_okay() { + talktosc::disconnect(card); return Err(errors::TalktoSCError::PinError); } + talktosc::disconnect(card); Ok(true) } +// To get the touch policy of a given slot #[allow(unused)] -pub fn is_smartcard_connected() -> Result { +pub fn get_touch_policy(slot: KeySlot) -> Result, errors::TalktoSCError> { let card = talktosc::create_connection(); let card = match card { Ok(card) => card, @@ -50,8 +57,86 @@ pub fn is_smartcard_connected() -> Result { Ok(_) => resp.unwrap(), Err(value) => return Err(value), }; + // Just make sure we can talk + if !resp.is_okay() { + return Err(errors::TalktoSCError::PinError); + } + // Now let us ask about the touch policy + // + let slot_value = slot as u8; + let select_touchpolicy = apdus::APDU::new(0x00, 0xCA, 0x00, slot_value, None); + let resp = talktosc::send_and_parse(&card, select_touchpolicy); + let resp = match resp { + Ok(_) => resp.unwrap(), + Err(value) => { + talktosc::disconnect(card); + return Err(value); + } + }; + + talktosc::disconnect(card); + Ok(resp.get_data()) +} + +// To get the Yubikey card firmware version +#[allow(unused)] +pub fn internal_get_version() -> Result, errors::TalktoSCError> { + let card = talktosc::create_connection(); + let card = match card { + Ok(card) => card, + Err(value) => return Err(value), + }; + + let select_openpgp = apdus::create_apdu_select_openpgp(); + let resp = talktosc::send_and_parse(&card, select_openpgp); + let resp = match resp { + Ok(_) => resp.unwrap(), + Err(value) => { + talktosc::disconnect(card); + return Err(value); + } + }; + // Just make sure we can talk + if !resp.is_okay() { + talktosc::disconnect(card); + return Err(errors::TalktoSCError::PinError); + } + // Now let us ask about version + // + let select_version = apdus::APDU::new(0x00, 0xF1, 0x00, 0x00, None); + let resp = talktosc::send_and_parse(&card, select_version); + let resp = match resp { + Ok(_) => resp.unwrap(), + Err(value) => { + talktosc::disconnect(card); + return Err(value); + } + }; + + talktosc::disconnect(card); + Ok(resp.get_data()) +} + +#[allow(unused)] +pub fn is_smartcard_connected() -> Result { + let card = talktosc::create_connection(); + let card = match card { + Ok(card) => card, + Err(value) => return Err(value), + }; + + let select_openpgp = apdus::create_apdu_select_openpgp(); + let resp = talktosc::send_and_parse(&card, select_openpgp); + let resp = match resp { + Ok(_) => resp.unwrap(), + Err(value) => { + talktosc::disconnect(card); + return Err(value); + } + }; // Verify if the admin pin worked or not. if !resp.is_okay() { + talktosc::disconnect(card); return Err(errors::TalktoSCError::PinError); }