Skip to content

Commit

Permalink
Merge pull request #70 from wiktor-k/wiktor/add-extensions-to-examples
Browse files Browse the repository at this point in the history
Add an example of using SSH agent extensions for curve 25519 decryption
  • Loading branch information
wiktor-k authored May 20, 2024
2 parents 874e986 + df138ed commit 2fcef7a
Show file tree
Hide file tree
Showing 5 changed files with 507 additions and 48 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ required-features = ["agent"]
env_logger = "0.11.0"
rand = "0.8.5"
rsa = { version = "0.9.6", features = ["sha2", "sha1"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] }
sha1 = { version = "0.10.5", default-features = false, features = ["oid"] }
testresult = "0.4.0"
hex-literal = "0.4.1"
Expand Down
161 changes: 161 additions & 0 deletions examples/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use ssh_agent_lib::proto::{extension::MessageExtension, Identity, ProtoError};
use ssh_encoding::{CheckedSum, Decode, Encode, Reader, Writer};
use ssh_key::public::KeyData;

pub struct RequestDecryptIdentities;

const DECRYPT_DERIVE_IDS: &str = "[email protected]";

impl MessageExtension for RequestDecryptIdentities {
const NAME: &'static str = DECRYPT_DERIVE_IDS;
}

impl Encode for RequestDecryptIdentities {
fn encoded_len(&self) -> Result<usize, ssh_encoding::Error> {
Ok(0)
}

fn encode(&self, _writer: &mut impl Writer) -> Result<(), ssh_encoding::Error> {
Ok(())
}
}

impl Decode for RequestDecryptIdentities {
type Error = ProtoError;

fn decode(_reader: &mut impl Reader) -> core::result::Result<Self, Self::Error> {
Ok(Self)
}
}

#[derive(Debug)]
pub struct DecryptIdentities {
pub identities: Vec<Identity>,
}

impl MessageExtension for DecryptIdentities {
const NAME: &'static str = DECRYPT_DERIVE_IDS;
}

impl Decode for DecryptIdentities {
type Error = ProtoError;

fn decode(reader: &mut impl Reader) -> Result<Self, Self::Error> {
let len = u32::decode(reader)?;
let mut identities = vec![];

for _ in 0..len {
identities.push(Identity::decode(reader)?);
}

Ok(Self { identities })
}
}

impl Encode for DecryptIdentities {
fn encoded_len(&self) -> ssh_encoding::Result<usize> {
let ids = &self.identities;
let mut lengths = Vec::with_capacity(1 + ids.len());
// Prefixed length
lengths.push(4);

for id in ids {
lengths.push(id.encoded_len()?);
}

lengths.checked_sum()
}

fn encode(&self, writer: &mut impl Writer) -> ssh_encoding::Result<()> {
let ids = &self.identities;
(ids.len() as u32).encode(writer)?;
for id in ids {
id.encode(writer)?;
}
Ok(())
}
}

const DECRYPT_DERIVE: &str = "[email protected]";

#[derive(Clone, PartialEq, Debug)]
pub struct DecryptDeriveRequest {
pub pubkey: KeyData,

pub data: Vec<u8>,

pub flags: u32,
}

impl MessageExtension for DecryptDeriveRequest {
const NAME: &'static str = DECRYPT_DERIVE;
}

impl Decode for DecryptDeriveRequest {
type Error = ProtoError;

fn decode(reader: &mut impl Reader) -> Result<Self, Self::Error> {
let pubkey = reader.read_prefixed(KeyData::decode)?;
let data = Vec::decode(reader)?;
let flags = u32::decode(reader)?;

Ok(Self {
pubkey,
data,
flags,
})
}
}

impl Encode for DecryptDeriveRequest {
fn encoded_len(&self) -> ssh_encoding::Result<usize> {
[
self.pubkey.encoded_len_prefixed()?,
self.data.encoded_len()?,
self.flags.encoded_len()?,
]
.checked_sum()
}

fn encode(&self, writer: &mut impl Writer) -> ssh_encoding::Result<()> {
self.pubkey.encode_prefixed(writer)?;
self.data.encode(writer)?;
self.flags.encode(writer)?;

Ok(())
}
}

#[derive(Debug)]
pub struct DecryptDeriveResponse {
pub data: Vec<u8>,
}

impl MessageExtension for DecryptDeriveResponse {
const NAME: &'static str = DECRYPT_DERIVE;
}

impl Encode for DecryptDeriveResponse {
fn encoded_len(&self) -> Result<usize, ssh_encoding::Error> {
self.data.encoded_len()
}

fn encode(&self, writer: &mut impl Writer) -> Result<(), ssh_encoding::Error> {
self.data.encode(writer)
}
}

impl Decode for DecryptDeriveResponse {
type Error = ProtoError;

fn decode(reader: &mut impl Reader) -> core::result::Result<Self, Self::Error> {
Ok(Self {
data: Vec::decode(reader)?,
})
}
}

#[allow(dead_code)] // rust will complain if main is missing in example crate
fn main() {
panic!("This is just a helper lib crate for extensions");
}
97 changes: 95 additions & 2 deletions examples/openpgp-card-agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use card_backend_pcsc::PcscBackend;
use clap::Parser;
use openpgp_card::{
algorithm::AlgorithmAttributes,
crypto_data::{EccType, PublicKeyMaterial},
crypto_data::{Cryptogram, EccType, PublicKeyMaterial},
Card, KeyType,
};
use retainer::{Cache, CacheExpiration};
Expand All @@ -29,13 +29,20 @@ use service_binding::Binding;
use ssh_agent_lib::{
agent::{bind, Session},
error::AgentError,
proto::{AddSmartcardKeyConstrained, Identity, KeyConstraint, SignRequest, SmartcardKey},
proto::{
extension::MessageExtension, AddSmartcardKeyConstrained, Extension, Identity,
KeyConstraint, ProtoError, SignRequest, SmartcardKey,
},
};
use ssh_key::{
public::{Ed25519PublicKey, KeyData},
Algorithm, Signature,
};
use testresult::TestResult;
mod extensions;
use extensions::{
DecryptDeriveRequest, DecryptDeriveResponse, DecryptIdentities, RequestDecryptIdentities,
};

#[derive(Clone)]
struct CardSession {
Expand Down Expand Up @@ -114,6 +121,36 @@ impl CardSession {
Err(error) => Err(AgentError::other(error)),
}
}

async fn decrypt_derive(
&mut self,
req: DecryptDeriveRequest,
) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error + Send + Sync>> {
if let Ok(cards) = PcscBackend::cards(None) {
for card in cards {
let mut card = Card::new(card?)?;
let mut tx = card.transaction()?;
if let PublicKeyMaterial::E(e) = tx.public_key(KeyType::Decryption)? {
if let AlgorithmAttributes::Ecc(ecc) = e.algo() {
if ecc.ecc_type() == EccType::ECDH {
let pubkey = KeyData::Ed25519(Ed25519PublicKey(e.data().try_into()?));
if pubkey == req.pubkey {
let ident = tx.application_identifier()?.ident();
let pin = self.pwds.get(&ident).await;
if let Some(pin) = pin {
tx.verify_pw1_user(pin.expose_secret().as_bytes())?;

let data = tx.decipher(Cryptogram::ECDH(&req.data))?;
return Ok(Some(data));
}
}
}
}
}
}
}
Ok(None)
}
}

#[ssh_agent_lib::async_trait]
Expand Down Expand Up @@ -174,6 +211,62 @@ impl Session for CardSession {
async fn sign(&mut self, request: SignRequest) -> Result<Signature, AgentError> {
self.handle_sign(request).await.map_err(AgentError::Other)
}

async fn extension(&mut self, extension: Extension) -> Result<Option<Extension>, AgentError> {
if extension.name == RequestDecryptIdentities::NAME {
let identities = if let Ok(cards) = PcscBackend::cards(None) {
cards
.flat_map(|card| {
let mut card = Card::new(card?)?;
let mut tx = card.transaction()?;
let ident = tx.application_identifier()?.ident();
if let PublicKeyMaterial::E(e) = tx.public_key(KeyType::Decryption)? {
if let AlgorithmAttributes::Ecc(ecc) = e.algo() {
if ecc.ecc_type() == EccType::ECDH {
return Ok::<_, Box<dyn std::error::Error>>(Some(Identity {
pubkey: KeyData::Ed25519(Ed25519PublicKey(
e.data().try_into()?,
)),
comment: ident,
}));
}
}
}
Ok(None)
})
.flatten()
.collect::<Vec<_>>()
} else {
vec![]
};

Ok(Some(
Extension::new_message(DecryptIdentities { identities })
.map_err(AgentError::other)?,
))
} else if extension.name == DecryptDeriveRequest::NAME {
let req = extension
.parse_message::<DecryptDeriveRequest>()?
.expect("message to be there");

let decrypted = self.decrypt_derive(req).await.map_err(AgentError::Other)?;

if let Some(decrypted) = decrypted {
Ok(Some(
Extension::new_message(DecryptDeriveResponse { data: decrypted })
.map_err(AgentError::other)?,
))
} else {
Err(AgentError::from(ProtoError::UnsupportedCommand {
command: 27,
}))
}
} else {
Err(AgentError::from(ProtoError::UnsupportedCommand {
command: 27,
}))
}
}
}

#[derive(Debug, Parser)]
Expand Down
Loading

0 comments on commit 2fcef7a

Please sign in to comment.