Skip to content

Commit

Permalink
pairing: Add check for the PairingDataResponse
Browse files Browse the repository at this point in the history
This adds a method to the pairing service that allows to check the
validity of the PairingDataResponse. This is that the public key from
the session_id matches the public key from the CSR.

We may want to add a signature for the restrictions to ensure that GL
did not manipulate them. We are OK for now as a user has to aggree to
the restrictions anyway.

Signed-off-by: Peter Neuroth <[email protected]>
  • Loading branch information
nepet committed Dec 3, 2023
1 parent 582597e commit ed1aed8
Show file tree
Hide file tree
Showing 9 changed files with 856 additions and 11 deletions.
388 changes: 379 additions & 9 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions libs/gl-client-py/glclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ def get_pairing_data(self, session_id: str) -> schedpb.GetPairingDataResponse:
def approve_pairing(self, session_id: str, node_id: bytes, device_name: str, restrs: str):
self.inner.approve_pairing(session_id, node_id, device_name, restrs)

def verify_pairing_data(self, data: schedpb.GetPairingDataRequest) -> Optional[str]:
try:
self.inner.verify_pairing_data(data.SerializeToString())
return None
except Exception as e:
return str(e)


class Node(object):
def __init__(self, node_id: bytes, network: str, grpc_uri: str, tls: Optional[TlsConfig] = None, rune: Optional[str] = None, auth: Optional[bytes] = None) -> None:
Expand Down
1 change: 1 addition & 0 deletions libs/gl-client-py/glclient/glclient.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class AttestationDevicePairingClient:
def __init__(self, tls: Optional["TlsConfig"], auth: Optional[bytes], rune: Optional[bytes], uri: Optional[str]): ...
def get_pairing_data(self, session_id: str) -> bytes: ...
def approve_pairing(self, session_id: str, node_id: bytes, device_name: str, restrs: str):...
def verify_pairing_data(self, data: bytes): ...


class Node:
Expand Down
12 changes: 12 additions & 0 deletions libs/gl-client-py/src/pairing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::runtime::exec;
use crate::tls::TlsConfig;
use bytes::BufMut;
use gl_client::pairing::{attestation_device, new_device, Error, PairingSessionData};
use gl_client::pb::scheduler::GetPairingDataResponse;
use prost::Message;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
Expand Down Expand Up @@ -114,6 +115,17 @@ impl AttestationDevicePairingClient {
.await
}))?)
}

fn verify_pairing_data(&self, data: Vec<u8>) -> PyResult<()> {
let pd = GetPairingDataResponse::decode(&data[..]).map_err(|e| {
PyValueError::new_err(format!(
"could not decode data={:?} as PairingData: {}",
data, e,
))
})?;

Ok(attestation_device::Client::verify_pairing_data(pd).map_err(PairingError)?)
}
}

/// A wrapper class to return an iterable from a mpsc channel.
Expand Down
32 changes: 32 additions & 0 deletions libs/gl-client-py/tests/test_pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,35 @@ def test_pairing_session(scheduler, nobody_id, sclient, signer, tls):
assert(m.device_key)
# assert(m.rune) fixme: enable once we pass back a rune during the tests.
assert(m.auth)


def test_paring_data_validation(scheduler, sclient, signer):
"""A simple test to ensure that data validation works as intended.
If the data is valid, the public key belongs to the private key that was
used to sign the csr subject.
"""
name = "new_device"
desc = "my description"
restrs = "method^list"

dc = NewDevicePairingClient()
session = dc.pair_device(name, desc, restrs)
session_iter = iter(session)
m = next(session_iter)
session_id = m.data.split(':')[1]


res = sclient.register(signer)
ac = AttestationDevicePairingClient(auth=res.auth)

# check for pairing data.
session_id = m.data.split(':')[1]
m = ac.get_pairing_data(session_id)

assert(ac.verify_pairing_data(m) is None)

# Change the public key and try again
pk = '01' + m.session_id[2:] if m.session_id[0:1] == '00' else '00' + m.session_id[2:]
m.session_id = pk
assert(ac.verify_pairing_data(m))
3 changes: 3 additions & 0 deletions libs/gl-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ http = "0.2"
http-body = "^0.4"
lightning-invoice = "0.24.0"
log = "^0.4"
picky = "6.3"
picky-asn1-x509 = "0.12"
picky-asn1-der = "0.4"
pin-project = "1.1.3"
prost = "0.11"
prost-derive = "0.11"
Expand Down
72 changes: 71 additions & 1 deletion libs/gl-client/src/pairing/attestation_device.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::time::{SystemTime, UNIX_EPOCH};

use super::{into_approve_pairing_error, Error};
use super::{into_approve_pairing_error, into_verify_pairing_data_error, Error};
use crate::{
pb::scheduler::{
pairing_client::PairingClient, ApprovePairingRequest, Empty, GetPairingDataRequest,
Expand All @@ -10,6 +10,8 @@ use crate::{
tls::TlsConfig,
};
use bytes::BufMut as _;
use picky::{pem::Pem, x509::Csr};
use picky_asn1_x509::{PublicKey, SubjectPublicKeyInfo};
use ring::{
rand,
signature::{self, EcdsaKeyPair},
Expand Down Expand Up @@ -164,4 +166,72 @@ impl Client {
.await?
.into_inner())
}

pub fn verify_pairing_data(data: GetPairingDataResponse) -> Result<()> {
let mut crs = std::io::Cursor::new(&data.csr);
let pem = Pem::read_from(&mut crs).map_err(into_verify_pairing_data_error)?;
let csr = Csr::from_pem(&pem).map_err(into_verify_pairing_data_error)?;
let sub_pk_der = csr
.public_key()
.to_der()
.map_err(into_verify_pairing_data_error)?;
let sub_pk_info: SubjectPublicKeyInfo =
picky_asn1_der::from_bytes(&sub_pk_der).map_err(into_verify_pairing_data_error)?;

if let PublicKey::Ec(bs) = sub_pk_info.subject_public_key {
let pk = hex::encode(bs.0.payload_view());

if pk == data.session_id {
Ok(())
} else {
Err(Error::VerifyPairingDataError(format!(
"public key {} does not match pk {}",
data.session_id, pk
)))
}
} else {
Err(Error::VerifyPairingDataError(format!(
"public key is not ecdsa"
)))
}
}
}

#[cfg(test)]
pub mod tests {
use super::*;
use crate::tls;

#[test]
fn test_verify_pairing_data() {
let pem = tls::generate_ecdsa_key_pair().serialize_pem();
let device_cert = tls::generate_self_signed_device_cert_from_pem(
&pem,
&hex::encode("00"),
"my-device",
vec!["localhost".into()],
);
let csr = device_cert.serialize_request_pem().unwrap();
let pk = hex::encode(device_cert.get_key_pair().public_key_raw());

// Check with public key as session id.
let pd = GetPairingDataResponse {
session_id: pk,
csr: csr.clone().into_bytes(),
device_name: "my-device".to_string(),
desc: "".to_string(),
restrs: "".to_string(),
};
assert!(Client::verify_pairing_data(pd).is_ok());

// Check with different public key as session id.
let pd = GetPairingDataResponse {
session_id: "00".to_string(),
csr: csr.into_bytes(),
device_name: "my-device".to_string(),
desc: "".to_string(),
restrs: "".to_string(),
};
assert!(Client::verify_pairing_data(pd).is_err());
}
}
7 changes: 6 additions & 1 deletion libs/gl-client/src/pairing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ pub enum Error {
RuneError(#[from] runeauth::RuneError),
#[error("could not approve pairing: {0}")]
ApprovePairingError(String),
#[error("could not verify pairing data: {0}")]
VerifyPairingDataError(String),
}

pub enum PairingSessionData {
PairingResponse(PairDeviceResponse),
PairingQr(PairingQr),
Expand All @@ -33,3 +34,7 @@ fn into_build_client_error<T: ToString>(v: T) -> Error {
fn into_approve_pairing_error<T: ToString>(v: T) -> Error {
Error::ApprovePairingError(v.to_string())
}

fn into_verify_pairing_data_error<T: ToString>(v: T) -> Error {
Error::VerifyPairingDataError(v.to_string())
}
Loading

0 comments on commit ed1aed8

Please sign in to comment.