Skip to content

Commit

Permalink
x509 user token work
Browse files Browse the repository at this point in the history
  • Loading branch information
locka99 committed May 2, 2019
1 parent c8ba120 commit 79f1324
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 45 deletions.
6 changes: 4 additions & 2 deletions core/src/crypto/user_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,19 @@ pub(crate) fn legacy_password_decrypt(secret: &ByteString, server_nonce: &[u8],
}
}

/// Verify that the X509 identity token supplied to a server contains a valid signature.
pub fn verify_x509_identity_token(token: &X509IdentityToken, user_token_signature: &SignatureData, security_policy: SecurityPolicy, server_cert: &X509, server_nonce: &[u8]) -> Result<(), StatusCode> {
// Since it is not obvious at all from the spec what the user token signature is supposed to be, I looked
// at the internet for answers
// at the internet for clues:
//
// https://stackoverflow.com/questions/46683342/securing-opensecurechannel-messages-and-x509identitytoken
// https://forum.prosysopc.com/forum/opc-ua/clarification-on-opensecurechannel-messages-and-x509identitytoken-specifications/
//
// These suggest that the signature is produced by appending the server nonce to the server certificate
// and signing with the user certificate's private key.
//
// More or less like the standard handshake between client and server but with the identity cert.
// This is the same as the standard handshake between client and server but using the identity cert. It would have been nice
// if the spec actually said this.

let signing_cert = super::x509::X509::from_byte_string(&token.certificate_data)?;
let result = super::verify_signature_data(user_token_signature, security_policy, &signing_cert, server_cert, server_nonce);
Expand Down
8 changes: 8 additions & 0 deletions server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ impl ServerUserToken {
}
valid
}

pub fn is_user_pass(&self) -> bool {
self.x509.is_none()
}

pub fn is_x509(&self) -> bool {
self.x509.is_some()
}
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ impl SessionService {
};

if service_result.is_good() {
if let Err(err) = server_state.authenticate_endpoint(endpoint_url, security_policy, security_mode, &request.user_identity_token, &session.session_nonce) {
if let Err(err) = server_state.authenticate_endpoint(request, endpoint_url, security_policy, security_mode, &request.user_identity_token, &session.session_nonce) {
service_result = err;
}
}
Expand Down
44 changes: 29 additions & 15 deletions server/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use opcua_types::{
node_ids::ObjectId,
profiles,
service_types::{
ApplicationDescription, RegisteredServer, ApplicationType, EndpointDescription,
ActivateSessionRequest, ApplicationDescription, RegisteredServer, ApplicationType, EndpointDescription,
UserNameIdentityToken, UserTokenPolicy, UserTokenType, X509IdentityToken, SignatureData,
ServerState as ServerStateType,
},
Expand Down Expand Up @@ -277,7 +277,7 @@ impl ServerState {
/// It is possible that the endpoint does not exist, or that the token is invalid / unsupported
/// or that the token cannot be used with the end point. The return codes reflect the responses
/// that ActivateSession would expect from a service call.
pub fn authenticate_endpoint(&self, endpoint_url: &str, security_policy: SecurityPolicy, security_mode: MessageSecurityMode, user_identity_token: &ExtensionObject, server_nonce: &ByteString) -> Result<(), StatusCode> {
pub fn authenticate_endpoint(&self, request: &ActivateSessionRequest, endpoint_url: &str, security_policy: SecurityPolicy, security_mode: MessageSecurityMode, user_identity_token: &ExtensionObject, server_nonce: &ByteString) -> Result<(), StatusCode> {
// Get security from endpoint url
let config = trace_read_lock_unwrap!(self.config);
let decoding_limits = config.decoding_limits();
Expand Down Expand Up @@ -306,12 +306,7 @@ impl ServerState {
ObjectId::X509IdentityToken_Encoding_DefaultBinary => {
// X509 certs
if let Ok(token) = user_identity_token.decode_inner::<X509IdentityToken>(&decoding_limits) {
// TODO get this from activate session
let user_token_signature = SignatureData {
algorithm: UAString::null(),
signature: ByteString::null()
};
self.authenticate_x509_identity_token(&token, &user_token_signature, &self.server_certificate, server_nonce)
self.authenticate_x509_identity_token(&config, endpoint, &token, &request.user_token_signature, &self.server_certificate, server_nonce)
} else {
// Garbage in the extension object
error!("X509 identity token could not be decoded");
Expand Down Expand Up @@ -349,17 +344,36 @@ impl ServerState {
}
}

fn authenticate_x509_identity_token(&self, token: &X509IdentityToken, user_token_signature: &SignatureData, server_certificate: &Option<X509>, server_nonce: &ByteString) -> Result<(), StatusCode> {
match server_certificate {
fn authenticate_x509_identity_token(&self, config: &ServerConfig, endpoint: &ServerEndpoint, token: &X509IdentityToken, user_token_signature: &SignatureData, server_certificate: &Option<X509>, server_nonce: &ByteString) -> Result<(), StatusCode> {
let result = match server_certificate {
Some(ref server_certificate) => {
// TODO
let security_policy = SecurityPolicy::Basic128Rsa15;
user_identity::verify_x509_identity_token(token, user_token_signature, security_policy, server_certificate, server_nonce.as_ref())
let security_policy = endpoint.security_policy();
// The security policy has to be something that can encrypt
match security_policy {
SecurityPolicy::Unknown | SecurityPolicy::None => Err(StatusCode::BadIdentityTokenInvalid),
security_policy => {
// Verify token
user_identity::verify_x509_identity_token(token, user_token_signature, security_policy, server_certificate, server_nonce.as_ref())
}
}
}
None => {
Err(StatusCode::BadIdentityTokenInvalid)
}
}
};

result.and_then(|_| {
let valid = true;
// Check the endpoint to see if this token is supported
for user_token_id in &endpoint.user_token_ids {
if let Some(server_user_token) = config.user_tokens.get(user_token_id) {
if server_user_token.is_x509() {
// TODO verify there is a x509 on disk that matches the one supplied
}
}
}
if valid { Ok(()) } else { Err(StatusCode::BadIdentityTokenInvalid) }
})
}


Expand All @@ -384,7 +398,7 @@ impl ServerState {
// Iterate ids in endpoint
for user_token_id in &endpoint.user_token_ids {
if let Some(server_user_token) = config.user_tokens.get(user_token_id) {
if &server_user_token.user == token.user_name.as_ref() {
if server_user_token.is_user_pass() && &server_user_token.user == token.user_name.as_ref() {
// test for empty password
let valid = if server_user_token.pass.is_none() {
// Empty password for user
Expand Down
6 changes: 3 additions & 3 deletions server/src/tests/services/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn new_call_method_request<S, T>(object_id: S, method_id: T, input_arguments: Op

fn create_subscription_request() -> CreateSubscriptionRequest {
CreateSubscriptionRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
requested_publishing_interval: 100f64,
requested_lifetime_count: 100,
requested_max_keep_alive_count: 100,
Expand All @@ -48,7 +48,7 @@ fn create_subscription_request() -> CreateSubscriptionRequest {

fn create_monitored_items_request<T>(subscription_id: u32, client_handle: u32, node_id: T) -> CreateMonitoredItemsRequest where T: 'static + Into<NodeId> {
CreateMonitoredItemsRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
subscription_id,
timestamps_to_return: TimestampsToReturn::Both,
items_to_create: Some(vec![MonitoredItemCreateRequest {
Expand All @@ -73,7 +73,7 @@ fn create_monitored_items_request<T>(subscription_id: u32, client_handle: u32, n
/// This is a convenience for tests
fn call_single(s: &MethodService, address_space: &mut AddressSpace, server_state: &ServerState, session: &mut Session, request: CallMethodRequest) -> Result<CallMethodResult, StatusCode> {
let response = s.call(address_space, server_state, session, &CallRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
methods_to_call: Some(vec![request]),
})?;
let response: CallResponse = supported_message_as!(response, CallResponse);
Expand Down
4 changes: 2 additions & 2 deletions server/src/tests/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fn do_subscription_service_test<T>(f: T)
/// Creates a blank subscription request
fn create_subscription_request(max_keep_alive_count: u32, lifetime_count: u32) -> CreateSubscriptionRequest {
CreateSubscriptionRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
requested_publishing_interval: 100f64,
requested_lifetime_count: lifetime_count,
requested_max_keep_alive_count: max_keep_alive_count,
Expand Down Expand Up @@ -121,7 +121,7 @@ fn create_monitored_items_request<T>(subscription_id: u32, mut node_id: Vec<T>)
})
.collect::<Vec<_>>());
CreateMonitoredItemsRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
subscription_id,
timestamps_to_return: TimestampsToReturn::Both,
items_to_create,
Expand Down
6 changes: 3 additions & 3 deletions server/src/tests/services/monitored_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fn make_create_request(sampling_interval: Duration, queue_size: u32) -> Monitore

fn set_monitoring_mode(session: &mut Session, subscription_id: u32, monitored_item_id: u32, monitoring_mode: MonitoringMode, mis: &MonitoredItemService) {
let request = SetMonitoringModeRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
subscription_id,
monitoring_mode,
monitored_item_ids: Some(vec![monitored_item_id]),
Expand All @@ -69,7 +69,7 @@ fn set_monitoring_mode(session: &mut Session, subscription_id: u32, monitored_it

fn set_triggering(session: &mut Session, subscription_id: u32, monitored_item_id: u32, links_to_add: &[u32], links_to_remove: &[u32], mis: &MonitoredItemService) -> (Option<Vec<StatusCode>>, Option<Vec<StatusCode>>) {
let request = SetTriggeringRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
subscription_id,
triggering_item_id: monitored_item_id,
links_to_add: if links_to_add.is_empty() { None } else { Some(links_to_add.to_vec()) },
Expand All @@ -82,7 +82,7 @@ fn set_triggering(session: &mut Session, subscription_id: u32, monitored_item_id
fn publish_request(now: &DateTimeUtc, session: &mut Session, address_space: &AddressSpace, ss: &SubscriptionService) {
let request_id = 1001;
let request = PublishRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
subscription_acknowledgements: None,
};

Expand Down
12 changes: 6 additions & 6 deletions server/src/tests/services/node_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ fn do_node_management_service_test<T>(can_modify_address_space: bool, f: T)
fn do_add_node_test_with_expected_error(can_modify_address_space: bool, item: AddNodesItem, expected_status_code: StatusCode) {
do_node_management_service_test(can_modify_address_space, |server_state, session, address_space, nms| {
let response = nms.add_nodes(server_state, session, address_space, &AddNodesRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
nodes_to_add: Some(vec![item]),
});
let response: AddNodesResponse = supported_message_as!(response.unwrap(), AddNodesResponse);
Expand All @@ -51,7 +51,7 @@ fn do_add_node_test_with_expected_error(can_modify_address_space: bool, item: Ad
fn do_add_references_test(can_modify_address_space: bool, item: AddReferencesItem, expected_status_code: StatusCode) {
do_node_management_service_test(can_modify_address_space, |server_state, session, address_space, nms| {
let response = nms.add_references(server_state, session, address_space, &AddReferencesRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
references_to_add: Some(vec![item]),
});
let response: AddReferencesResponse = supported_message_as!(response.unwrap(), AddReferencesResponse);
Expand All @@ -67,7 +67,7 @@ fn do_add_references_test(can_modify_address_space: bool, item: AddReferencesIte
fn do_delete_nodes_test(can_modify_address_space: bool, item: DeleteNodesItem, expected_status_code: StatusCode) {
do_node_management_service_test(can_modify_address_space, |server_state, session, address_space, nms| {
let response = nms.delete_nodes(server_state, session, address_space, &DeleteNodesRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
nodes_to_delete: Some(vec![item]),
});
let response: DeleteNodesResponse = supported_message_as!(response.unwrap(), DeleteNodesResponse);
Expand All @@ -80,7 +80,7 @@ fn do_delete_nodes_test(can_modify_address_space: bool, item: DeleteNodesItem, e
fn do_delete_references_test(can_modify_address_space: bool, item: DeleteReferencesItem, expected_status_code: StatusCode) {
do_node_management_service_test(can_modify_address_space, |server_state, session, address_space, nms| {
let response = nms.delete_references(server_state, session, address_space, &DeleteReferencesRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
references_to_delete: Some(vec![item]),
});
let response: DeleteReferencesResponse = supported_message_as!(response.unwrap(), DeleteReferencesResponse);
Expand Down Expand Up @@ -146,14 +146,14 @@ fn add_nodes_nothing_to_do() {
// Empty request
do_node_management_service_test(true, |server_state, session, address_space, nms: NodeManagementService| {
let response = nms.add_nodes(server_state, session, address_space, &AddNodesRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
nodes_to_add: None,
});
let response: ServiceFault = supported_message_as!(response.unwrap(), ServiceFault);
assert_eq!(response.response_header.service_result, StatusCode::BadNothingToDo);

let response = nms.add_nodes(server_state, session, address_space, &AddNodesRequest {
request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1),
request_header: RequestHeader::dummy(),
nodes_to_add: Some(vec![]),
});
let response: ServiceFault = supported_message_as!(response.unwrap(), ServiceFault);
Expand Down
30 changes: 23 additions & 7 deletions server/src/tests/services/session.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
use crate::tests::*;

use crate::builder::ServerBuilder;
use opcua_types::service_types::{ActivateSessionRequest, SignatureData, RequestHeader};

fn dummy_activate_session_request() -> ActivateSessionRequest {
ActivateSessionRequest {
request_header: RequestHeader::dummy(),
client_signature: SignatureData { algorithm: UAString::null(), signature: ByteString::null() },
client_software_certificates: None,
locale_ids: None,
user_identity_token: ExtensionObject::null(),
user_token_signature: SignatureData { algorithm: UAString::null(), signature: ByteString::null() },
}
}

#[test]
fn anonymous_user_token() {
Expand All @@ -16,15 +28,17 @@ fn anonymous_user_token() {

let server_nonce = ByteString::random(20);

let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
let request = dummy_activate_session_request();

let result = server_state.authenticate_endpoint(&request, "opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
trace!("result = {:?}", result);
assert!(result.is_ok());

let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/x", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
let result = server_state.authenticate_endpoint(&request, "opc.tcp://localhost:4855/x", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
trace!("result = {:?}", result);
assert_eq!(result.unwrap_err(), StatusCode::BadTcpEndpointUrlInvalid);

let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/noaccess", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
let result = server_state.authenticate_endpoint(&request, "opc.tcp://localhost:4855/noaccess", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
trace!("result = {:?}", result);
assert_eq!(result.unwrap_err(), StatusCode::BadIdentityTokenRejected);
}
Expand All @@ -47,21 +61,23 @@ fn user_name_pass_token() {

let server_nonce = ByteString::random(20);

let request = dummy_activate_session_request();

// Test that a good user authenticates
let token = make_user_name_identity_token("sample", b"sample1");
let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
let result = server_state.authenticate_endpoint(&request, "opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
assert!(result.is_ok());

// Invalid tests
let token = make_user_name_identity_token("samplex", b"sample1");
let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
let result = server_state.authenticate_endpoint(&request, "opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
assert_eq!(result.unwrap_err(), StatusCode::BadIdentityTokenRejected);

let token = make_user_name_identity_token("sample", b"sample");
let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
let result = server_state.authenticate_endpoint(&request, "opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
assert_eq!(result.unwrap_err(), StatusCode::BadIdentityTokenRejected);

let token = make_user_name_identity_token("", b"sample");
let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
let result = server_state.authenticate_endpoint(&request, "opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce);
assert_eq!(result.unwrap_err(), StatusCode::BadIdentityTokenRejected);
}
Loading

0 comments on commit 79f1324

Please sign in to comment.