Skip to content

Commit df138ed

Browse files
committed
Add an example of using SSH agent extensions
Adds the `decrypt-derive-ids@metacode.biz` and `decrypt-derive@metacode.biz` 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 <wiktor@metacode.biz>
1 parent 874e986 commit df138ed

File tree

5 files changed

+507
-48
lines changed

5 files changed

+507
-48
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ required-features = ["agent"]
4646
env_logger = "0.11.0"
4747
rand = "0.8.5"
4848
rsa = { version = "0.9.6", features = ["sha2", "sha1"] }
49-
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
49+
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] }
5050
sha1 = { version = "0.10.5", default-features = false, features = ["oid"] }
5151
testresult = "0.4.0"
5252
hex-literal = "0.4.1"

examples/extensions.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
use ssh_agent_lib::proto::{extension::MessageExtension, Identity, ProtoError};
2+
use ssh_encoding::{CheckedSum, Decode, Encode, Reader, Writer};
3+
use ssh_key::public::KeyData;
4+
5+
pub struct RequestDecryptIdentities;
6+
7+
const DECRYPT_DERIVE_IDS: &str = "decrypt-derive-ids@metacode.biz";
8+
9+
impl MessageExtension for RequestDecryptIdentities {
10+
const NAME: &'static str = DECRYPT_DERIVE_IDS;
11+
}
12+
13+
impl Encode for RequestDecryptIdentities {
14+
fn encoded_len(&self) -> Result<usize, ssh_encoding::Error> {
15+
Ok(0)
16+
}
17+
18+
fn encode(&self, _writer: &mut impl Writer) -> Result<(), ssh_encoding::Error> {
19+
Ok(())
20+
}
21+
}
22+
23+
impl Decode for RequestDecryptIdentities {
24+
type Error = ProtoError;
25+
26+
fn decode(_reader: &mut impl Reader) -> core::result::Result<Self, Self::Error> {
27+
Ok(Self)
28+
}
29+
}
30+
31+
#[derive(Debug)]
32+
pub struct DecryptIdentities {
33+
pub identities: Vec<Identity>,
34+
}
35+
36+
impl MessageExtension for DecryptIdentities {
37+
const NAME: &'static str = DECRYPT_DERIVE_IDS;
38+
}
39+
40+
impl Decode for DecryptIdentities {
41+
type Error = ProtoError;
42+
43+
fn decode(reader: &mut impl Reader) -> Result<Self, Self::Error> {
44+
let len = u32::decode(reader)?;
45+
let mut identities = vec![];
46+
47+
for _ in 0..len {
48+
identities.push(Identity::decode(reader)?);
49+
}
50+
51+
Ok(Self { identities })
52+
}
53+
}
54+
55+
impl Encode for DecryptIdentities {
56+
fn encoded_len(&self) -> ssh_encoding::Result<usize> {
57+
let ids = &self.identities;
58+
let mut lengths = Vec::with_capacity(1 + ids.len());
59+
// Prefixed length
60+
lengths.push(4);
61+
62+
for id in ids {
63+
lengths.push(id.encoded_len()?);
64+
}
65+
66+
lengths.checked_sum()
67+
}
68+
69+
fn encode(&self, writer: &mut impl Writer) -> ssh_encoding::Result<()> {
70+
let ids = &self.identities;
71+
(ids.len() as u32).encode(writer)?;
72+
for id in ids {
73+
id.encode(writer)?;
74+
}
75+
Ok(())
76+
}
77+
}
78+
79+
const DECRYPT_DERIVE: &str = "decrypt-derive@metacode.biz";
80+
81+
#[derive(Clone, PartialEq, Debug)]
82+
pub struct DecryptDeriveRequest {
83+
pub pubkey: KeyData,
84+
85+
pub data: Vec<u8>,
86+
87+
pub flags: u32,
88+
}
89+
90+
impl MessageExtension for DecryptDeriveRequest {
91+
const NAME: &'static str = DECRYPT_DERIVE;
92+
}
93+
94+
impl Decode for DecryptDeriveRequest {
95+
type Error = ProtoError;
96+
97+
fn decode(reader: &mut impl Reader) -> Result<Self, Self::Error> {
98+
let pubkey = reader.read_prefixed(KeyData::decode)?;
99+
let data = Vec::decode(reader)?;
100+
let flags = u32::decode(reader)?;
101+
102+
Ok(Self {
103+
pubkey,
104+
data,
105+
flags,
106+
})
107+
}
108+
}
109+
110+
impl Encode for DecryptDeriveRequest {
111+
fn encoded_len(&self) -> ssh_encoding::Result<usize> {
112+
[
113+
self.pubkey.encoded_len_prefixed()?,
114+
self.data.encoded_len()?,
115+
self.flags.encoded_len()?,
116+
]
117+
.checked_sum()
118+
}
119+
120+
fn encode(&self, writer: &mut impl Writer) -> ssh_encoding::Result<()> {
121+
self.pubkey.encode_prefixed(writer)?;
122+
self.data.encode(writer)?;
123+
self.flags.encode(writer)?;
124+
125+
Ok(())
126+
}
127+
}
128+
129+
#[derive(Debug)]
130+
pub struct DecryptDeriveResponse {
131+
pub data: Vec<u8>,
132+
}
133+
134+
impl MessageExtension for DecryptDeriveResponse {
135+
const NAME: &'static str = DECRYPT_DERIVE;
136+
}
137+
138+
impl Encode for DecryptDeriveResponse {
139+
fn encoded_len(&self) -> Result<usize, ssh_encoding::Error> {
140+
self.data.encoded_len()
141+
}
142+
143+
fn encode(&self, writer: &mut impl Writer) -> Result<(), ssh_encoding::Error> {
144+
self.data.encode(writer)
145+
}
146+
}
147+
148+
impl Decode for DecryptDeriveResponse {
149+
type Error = ProtoError;
150+
151+
fn decode(reader: &mut impl Reader) -> core::result::Result<Self, Self::Error> {
152+
Ok(Self {
153+
data: Vec::decode(reader)?,
154+
})
155+
}
156+
}
157+
158+
#[allow(dead_code)] // rust will complain if main is missing in example crate
159+
fn main() {
160+
panic!("This is just a helper lib crate for extensions");
161+
}

examples/openpgp-card-agent.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use card_backend_pcsc::PcscBackend;
2020
use clap::Parser;
2121
use openpgp_card::{
2222
algorithm::AlgorithmAttributes,
23-
crypto_data::{EccType, PublicKeyMaterial},
23+
crypto_data::{Cryptogram, EccType, PublicKeyMaterial},
2424
Card, KeyType,
2525
};
2626
use retainer::{Cache, CacheExpiration};
@@ -29,13 +29,20 @@ use service_binding::Binding;
2929
use ssh_agent_lib::{
3030
agent::{bind, Session},
3131
error::AgentError,
32-
proto::{AddSmartcardKeyConstrained, Identity, KeyConstraint, SignRequest, SmartcardKey},
32+
proto::{
33+
extension::MessageExtension, AddSmartcardKeyConstrained, Extension, Identity,
34+
KeyConstraint, ProtoError, SignRequest, SmartcardKey,
35+
},
3336
};
3437
use ssh_key::{
3538
public::{Ed25519PublicKey, KeyData},
3639
Algorithm, Signature,
3740
};
3841
use testresult::TestResult;
42+
mod extensions;
43+
use extensions::{
44+
DecryptDeriveRequest, DecryptDeriveResponse, DecryptIdentities, RequestDecryptIdentities,
45+
};
3946

4047
#[derive(Clone)]
4148
struct CardSession {
@@ -114,6 +121,36 @@ impl CardSession {
114121
Err(error) => Err(AgentError::other(error)),
115122
}
116123
}
124+
125+
async fn decrypt_derive(
126+
&mut self,
127+
req: DecryptDeriveRequest,
128+
) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error + Send + Sync>> {
129+
if let Ok(cards) = PcscBackend::cards(None) {
130+
for card in cards {
131+
let mut card = Card::new(card?)?;
132+
let mut tx = card.transaction()?;
133+
if let PublicKeyMaterial::E(e) = tx.public_key(KeyType::Decryption)? {
134+
if let AlgorithmAttributes::Ecc(ecc) = e.algo() {
135+
if ecc.ecc_type() == EccType::ECDH {
136+
let pubkey = KeyData::Ed25519(Ed25519PublicKey(e.data().try_into()?));
137+
if pubkey == req.pubkey {
138+
let ident = tx.application_identifier()?.ident();
139+
let pin = self.pwds.get(&ident).await;
140+
if let Some(pin) = pin {
141+
tx.verify_pw1_user(pin.expose_secret().as_bytes())?;
142+
143+
let data = tx.decipher(Cryptogram::ECDH(&req.data))?;
144+
return Ok(Some(data));
145+
}
146+
}
147+
}
148+
}
149+
}
150+
}
151+
}
152+
Ok(None)
153+
}
117154
}
118155

119156
#[ssh_agent_lib::async_trait]
@@ -174,6 +211,62 @@ impl Session for CardSession {
174211
async fn sign(&mut self, request: SignRequest) -> Result<Signature, AgentError> {
175212
self.handle_sign(request).await.map_err(AgentError::Other)
176213
}
214+
215+
async fn extension(&mut self, extension: Extension) -> Result<Option<Extension>, AgentError> {
216+
if extension.name == RequestDecryptIdentities::NAME {
217+
let identities = if let Ok(cards) = PcscBackend::cards(None) {
218+
cards
219+
.flat_map(|card| {
220+
let mut card = Card::new(card?)?;
221+
let mut tx = card.transaction()?;
222+
let ident = tx.application_identifier()?.ident();
223+
if let PublicKeyMaterial::E(e) = tx.public_key(KeyType::Decryption)? {
224+
if let AlgorithmAttributes::Ecc(ecc) = e.algo() {
225+
if ecc.ecc_type() == EccType::ECDH {
226+
return Ok::<_, Box<dyn std::error::Error>>(Some(Identity {
227+
pubkey: KeyData::Ed25519(Ed25519PublicKey(
228+
e.data().try_into()?,
229+
)),
230+
comment: ident,
231+
}));
232+
}
233+
}
234+
}
235+
Ok(None)
236+
})
237+
.flatten()
238+
.collect::<Vec<_>>()
239+
} else {
240+
vec![]
241+
};
242+
243+
Ok(Some(
244+
Extension::new_message(DecryptIdentities { identities })
245+
.map_err(AgentError::other)?,
246+
))
247+
} else if extension.name == DecryptDeriveRequest::NAME {
248+
let req = extension
249+
.parse_message::<DecryptDeriveRequest>()?
250+
.expect("message to be there");
251+
252+
let decrypted = self.decrypt_derive(req).await.map_err(AgentError::Other)?;
253+
254+
if let Some(decrypted) = decrypted {
255+
Ok(Some(
256+
Extension::new_message(DecryptDeriveResponse { data: decrypted })
257+
.map_err(AgentError::other)?,
258+
))
259+
} else {
260+
Err(AgentError::from(ProtoError::UnsupportedCommand {
261+
command: 27,
262+
}))
263+
}
264+
} else {
265+
Err(AgentError::from(ProtoError::UnsupportedCommand {
266+
command: 27,
267+
}))
268+
}
269+
}
177270
}
178271

179272
#[derive(Debug, Parser)]

0 commit comments

Comments
 (0)