Skip to content

Commit 2fcef7a

Browse files
authored
Merge pull request #70 from wiktor-k/wiktor/add-extensions-to-examples
Add an example of using SSH agent extensions for curve 25519 decryption
2 parents 874e986 + df138ed commit 2fcef7a

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)