diff --git a/core/src/crypto/user_identity.rs b/core/src/crypto/user_identity.rs index 8394bbc07..739adf8ff 100644 --- a/core/src/crypto/user_identity.rs +++ b/core/src/crypto/user_identity.rs @@ -156,9 +156,10 @@ 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/ @@ -166,7 +167,8 @@ pub fn verify_x509_identity_token(token: &X509IdentityToken, user_token_signatur // 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); diff --git a/server/src/config.rs b/server/src/config.rs index 578425946..dabc004dc 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -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)] diff --git a/server/src/services/session.rs b/server/src/services/session.rs index d485e4a69..889762e7d 100644 --- a/server/src/services/session.rs +++ b/server/src/services/session.rs @@ -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; } } diff --git a/server/src/state.rs b/server/src/state.rs index 242f774c7..0e3f5082d 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -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, }, @@ -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(); @@ -306,12 +306,7 @@ impl ServerState { ObjectId::X509IdentityToken_Encoding_DefaultBinary => { // X509 certs if let Ok(token) = user_identity_token.decode_inner::(&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"); @@ -349,17 +344,36 @@ impl ServerState { } } - fn authenticate_x509_identity_token(&self, token: &X509IdentityToken, user_token_signature: &SignatureData, server_certificate: &Option, 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, 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) } + }) } @@ -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 diff --git a/server/src/tests/services/method.rs b/server/src/tests/services/method.rs index 51ac7adf1..8e84bc60d 100644 --- a/server/src/tests/services/method.rs +++ b/server/src/tests/services/method.rs @@ -36,7 +36,7 @@ fn new_call_method_request(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, @@ -48,7 +48,7 @@ fn create_subscription_request() -> CreateSubscriptionRequest { fn create_monitored_items_request(subscription_id: u32, client_handle: u32, node_id: T) -> CreateMonitoredItemsRequest where T: 'static + Into { 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 { @@ -73,7 +73,7 @@ fn create_monitored_items_request(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 { 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); diff --git a/server/src/tests/services/mod.rs b/server/src/tests/services/mod.rs index 97b8908f3..952715941 100644 --- a/server/src/tests/services/mod.rs +++ b/server/src/tests/services/mod.rs @@ -90,7 +90,7 @@ fn do_subscription_service_test(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, @@ -121,7 +121,7 @@ fn create_monitored_items_request(subscription_id: u32, mut node_id: Vec) }) .collect::>()); CreateMonitoredItemsRequest { - request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1), + request_header: RequestHeader::dummy(), subscription_id, timestamps_to_return: TimestampsToReturn::Both, items_to_create, diff --git a/server/src/tests/services/monitored_item.rs b/server/src/tests/services/monitored_item.rs index a1145e615..0ed6fa57a 100644 --- a/server/src/tests/services/monitored_item.rs +++ b/server/src/tests/services/monitored_item.rs @@ -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]), @@ -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>, Option>) { 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()) }, @@ -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, }; diff --git a/server/src/tests/services/node_management.rs b/server/src/tests/services/node_management.rs index 309188251..3249e1f3f 100644 --- a/server/src/tests/services/node_management.rs +++ b/server/src/tests/services/node_management.rs @@ -32,7 +32,7 @@ fn do_node_management_service_test(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); @@ -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); @@ -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); @@ -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); @@ -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); diff --git a/server/src/tests/services/session.rs b/server/src/tests/services/session.rs index 592565faa..a99488815 100644 --- a/server/src/tests/services/session.rs +++ b/server/src/tests/services/session.rs @@ -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() { @@ -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); } @@ -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); } diff --git a/server/src/tests/services/subscription.rs b/server/src/tests/services/subscription.rs index 5b92e62c4..a305a43b4 100644 --- a/server/src/tests/services/subscription.rs +++ b/server/src/tests/services/subscription.rs @@ -74,7 +74,7 @@ fn test_revised_keep_alive_lifetime_counts() { fn publish_with_no_subscriptions() { do_subscription_service_test(|_, session, address_space, ss, _| { let request = PublishRequest { - request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1), + request_header: RequestHeader::dummy(), subscription_acknowledgements: None, // Option>, }; // Publish and expect a service fault BadNoSubscription @@ -103,7 +103,7 @@ fn publish_response_subscription() { let notification_message = { let request_id = 1001; let request = PublishRequest { - request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1), + request_header: RequestHeader::dummy(), subscription_acknowledgements: None, // Option>, }; debug!("PublishRequest {:#?}", request); @@ -184,7 +184,7 @@ fn publish_keep_alive() { let notification_message = { let request_id = 1001; let request = PublishRequest { - request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1), + request_header: RequestHeader::dummy(), subscription_acknowledgements: None, // Option>, }; debug!("PublishRequest {:#?}", request); @@ -255,7 +255,7 @@ fn republish() { // try for a notification message known to exist let request = RepublishRequest { - request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1), + request_header: RequestHeader::dummy(), subscription_id, retransmit_sequence_number: sequence_number, }; @@ -266,7 +266,7 @@ fn republish() { // try for a subscription id that does not exist, expect service fault let request = RepublishRequest { - request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1), + request_header: RequestHeader::dummy(), subscription_id: subscription_id + 1, retransmit_sequence_number: sequence_number, }; @@ -275,7 +275,7 @@ fn republish() { // try for a sequence nr that does not exist let request = RepublishRequest { - request_header: RequestHeader::new(&NodeId::null(), &DateTime::now(), 1), + request_header: RequestHeader::dummy(), subscription_id, retransmit_sequence_number: sequence_number + 1, }; diff --git a/types/src/service_types/impls.rs b/types/src/service_types/impls.rs index 2d9118292..0092832cc 100644 --- a/types/src/service_types/impls.rs +++ b/types/src/service_types/impls.rs @@ -175,6 +175,10 @@ impl RequestHeader { additional_header: ExtensionObject::null(), } } + + pub fn dummy() -> RequestHeader { + RequestHeader::new(&NodeId::null(), &DateTime::now(), 1) + } } //ResponseHeader = 392, @@ -401,6 +405,15 @@ impl EndpointDescription { None } } + + /// Returns a reference to a policy that matches the supplied policy id + pub fn find_policy_by_id(&self, policy_id: &str) -> Option<&UserTokenPolicy> { + if let Some(ref policies) = self.user_identity_tokens { + policies.iter().find(|t| t.policy_id.as_ref() == policy_id) + } else { + None + } + } } impl UserNameIdentityToken {