Skip to content

Commit

Permalink
Add an example of using SSH agent extensions
Browse files Browse the repository at this point in the history
Adds the `[email protected]` and `[email protected]`
extensions with encoding and decoding rules.

The extension is used to facilitate curve 25519 decryption over SSH
agent connections.

This PR additionally makes the OpenPGP Card agent example implement
these two extensions thus providing clients with public keys of its
decryption keys. Additionally a `decrypt derive` extension similar to
the sign request is implemented.

The PGP wrapper example has been extended to emit encryption subkeys
if the agent supports them. An extra `decrypt` subcommand has been
added. The docs have been updated to showcase how to use the feature:

```sh
echo I like strawberries | gpg -er 4EB27E153DDC454364B36B59A142E92C91BE3AD5 > /tmp/encrypted.pgp
SSH_AUTH_SOCK=/tmp/ext-agent.sock cargo run --example pgp-wrapper -- decrypt < /tmp/encrypted.pgp
...
I like strawberries
```

Signed-off-by: Wiktor Kwapisiewicz <[email protected]>
  • Loading branch information
wiktor-k committed May 20, 2024
1 parent 874e986 commit df138ed
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 df138ed

Please sign in to comment.