Skip to content

Commit

Permalink
Merge pull request #122 from kushaldas/touch_me
Browse files Browse the repository at this point in the history
Touch the Yubikey for various operations
  • Loading branch information
kushaldas authored Nov 8, 2022
2 parents ee9d1aa + 80bcebf commit 57b537d
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

36 changes: 36 additions & 0 deletions docs/smartcard.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
74 changes: 64 additions & 10 deletions johnnycanencrypt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
merge_keys,
parse_cert_bytes,
parse_cert_file,
TouchMode,
)

import johnnycanencrypt.johnnycanencrypt as rjce
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
18 changes: 18 additions & 0 deletions johnnycanencrypt/johnnycanencrypt.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
Expand Down Expand Up @@ -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: ...
Expand Down
12 changes: 10 additions & 2 deletions smartcardtests/smartcards_for_primary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>",], jce.SignatureType.PersonaCertification, password="123456".encode("utf-8"), oncard=True)
newother = ks.certify_key(
k,
other,
[
"Kushal Das <[email protected]>",
],
jce.SignatureType.PersonaCertification,
password="123456",
oncard=True,
)
with open("hello.public", "wb") as f:
f.write(newother.keyvalue)

88 changes: 88 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3029,6 +3029,89 @@ pub fn is_smartcard_connected() -> PyResult<bool> {
}
}

/// Returns a tuple with the firmware version.
#[pyfunction]
pub fn get_card_version(py: Python) -> Result<PyObject> {
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<KeySlot>) -> Result<Py<TouchMode>> {
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<u8>,
slot: Py<KeySlot>,
mode: Py<TouchMode>,
) -> Result<bool> {
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<()> {
Expand Down Expand Up @@ -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::<CryptoError>())?;
m.add("SameKeyError", _py.get_type::<SameKeyError>())?;
m.add_class::<Johnny>()?;
m.add_class::<TouchMode>()?;
m.add_class::<KeySlot>()?;
Ok(())
}
Loading

0 comments on commit 57b537d

Please sign in to comment.