diff --git a/CHANGELOG.md b/CHANGELOG.md index c919cd590..334ea93ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,11 @@ Planned future work is listed at the bottom. ## 0.7 (in progress) - Address space nodes have been made more memory efficient, saving about 3MB of runtime space with the standard node set. + - Client and server side support for encrypted passwords in user name identity tokens. - TODO address space. Add a create on demand callback - TODO gen_types.js. Refactor so it could be used to generate code for any model - TODO support events - TODO More control over limits on the server - number of subscriptions, monitored items, sessions - - Client side UserNameIdentityToken with encrypted password support. Plaintext password is already supported - - TODO Server side UserNameIdentityToken with encrypted password support. - TODO X509IdentityToken support - TODO Integration tests are broken and need to be fixed. - TODO Multiple chunk support in client and server, sending and receiving diff --git a/client/src/builder.rs b/client/src/builder.rs index 49e601346..4d0b7c904 100644 --- a/client/src/builder.rs +++ b/client/src/builder.rs @@ -9,7 +9,7 @@ use crate::{client::*, config::*}; /// /// # Example /// -/// ```rust,no_run +/// ```no_run /// use opcua_client::prelude::*; /// /// fn main() { diff --git a/client/src/client.rs b/client/src/client.rs index cc73d5c09..48c3a58bd 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -91,7 +91,7 @@ impl Client { /// /// # Example /// - /// ```rust,no_run + /// ```no_run /// use opcua_client::prelude::*; /// use std::path::PathBuf; /// @@ -331,7 +331,7 @@ impl Client { /// /// # Example /// - /// ```rust,no_run + /// ```no_run /// use opcua_client::prelude::*; /// use std::path::PathBuf; /// @@ -472,7 +472,7 @@ impl Client { /// /// # Example /// - /// ```rust,no_run + /// ```no_run /// use opcua_client::prelude::*; /// let endpoints = [ /// EndpointDescription::from("opc.tcp://foo:123"), diff --git a/core/src/crypto/user_identity.rs b/core/src/crypto/user_identity.rs index 04ff05566..e6e1a8c84 100644 --- a/core/src/crypto/user_identity.rs +++ b/core/src/crypto/user_identity.rs @@ -86,7 +86,7 @@ pub fn make_user_name_identity_token(channel_security_policy: SecurityPolicy, us pub fn decrypt_user_identity_token_password(user_identity_token: &UserNameIdentityToken, server_nonce: &[u8], server_key: &PrivateKey) -> Result { if user_identity_token.encryption_algorithm.is_empty() { // Assumed to be UTF-8 plain text - String::from_utf8(user_identity_token.password.as_ref().to_vec()).map_err(|_| StatusCode::BadDecodingError) + user_identity_token.plaintext_password() } else { // Determine the padding from the algorithm. let padding = match user_identity_token.encryption_algorithm.as_ref() { diff --git a/docs/compatibility.md b/docs/compatibility.md index 47c3a2c4d..6005b282e 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -112,12 +112,9 @@ The following security policies are supported - None, Basic128Rsa15, Basic256, B The server and client support the following user identities 1. Anonymous - i.e. no identity -2. UserName - plaintext password only, i.e. the encryption algorithm field supplied with the identity token must be a - null string. +2. UserName - encrypted and plaintext. User/pass identities are defined by configuration. -User/pass identities are defined by configuration - -X509 and UserName with encrypted passwords are intended for a future release. +X509 is intended for a future release. ## Crypto diff --git a/samples/server.conf b/samples/server.conf index e65cb36af..4c8850e54 100644 --- a/samples/server.conf +++ b/samples/server.conf @@ -25,6 +25,7 @@ endpoints: security_policy: Basic128Rsa15 security_mode: Sign security_level: 2 + password_security_policy: ~ user_token_ids: - ANONYMOUS - sample_user @@ -33,6 +34,7 @@ endpoints: security_policy: Basic128Rsa15 security_mode: SignAndEncrypt security_level: 2 + password_security_policy: ~ user_token_ids: - ANONYMOUS - sample_user @@ -41,6 +43,7 @@ endpoints: security_policy: Basic256 security_mode: Sign security_level: 3 + password_security_policy: ~ user_token_ids: - ANONYMOUS - sample_user @@ -49,6 +52,7 @@ endpoints: security_policy: Basic256 security_mode: SignAndEncrypt security_level: 3 + password_security_policy: ~ user_token_ids: - ANONYMOUS - sample_user @@ -57,6 +61,7 @@ endpoints: security_policy: Basic256Sha256 security_mode: Sign security_level: 4 + password_security_policy: ~ user_token_ids: - ANONYMOUS - sample_user @@ -65,6 +70,7 @@ endpoints: security_policy: Basic256Sha256 security_mode: SignAndEncrypt security_level: 4 + password_security_policy: ~ user_token_ids: - ANONYMOUS - sample_user @@ -73,12 +79,14 @@ endpoints: security_policy: None security_mode: None security_level: 1 + password_security_policy: ~ user_token_ids: [] none: path: / security_policy: None security_mode: None security_level: 1 + password_security_policy: ~ user_token_ids: - ANONYMOUS - sample_user diff --git a/server/src/config.rs b/server/src/config.rs index fd76f0211..578425946 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -36,7 +36,6 @@ pub struct ServerUserToken { pub x509: Option, } - impl ServerUserToken { pub fn new_user_pass(user: T, pass: T) -> Self where T: Into { ServerUserToken { @@ -80,6 +79,8 @@ pub struct ServerEndpoint { pub security_mode: String, /// Security level, higher being more secure pub security_level: u8, + /// Password security policy when a client supplies a user name identity token + pub password_security_policy: Option, /// User tokens pub user_token_ids: BTreeSet, } @@ -92,6 +93,7 @@ impl<'a> From<(&'a str, SecurityPolicy, MessageSecurityMode, &'a [&'a str])> for security_policy: v.1.to_string(), security_mode: v.2.to_string(), security_level: Self::security_level(v.1), + password_security_policy: None, user_token_ids: v.3.iter().map(|id| id.to_string()).collect(), } } @@ -104,6 +106,7 @@ impl ServerEndpoint { security_policy: security_policy.to_string(), security_mode: security_mode.to_string(), security_level: Self::security_level(security_policy), + password_security_policy: None, user_token_ids: user_token_ids.iter().map(|id| id.clone()).collect(), } } @@ -162,6 +165,14 @@ impl ServerEndpoint { } } + if let Some(ref password_security_policy) = self.password_security_policy { + let password_security_policy = SecurityPolicy::from_str(password_security_policy).unwrap(); + if password_security_policy == SecurityPolicy::Unknown { + error!("Endpoint {} is invalid. Password security policy \"{}\" is invalid. Valid values are None, Basic128Rsa15, Basic256, Basic256Sha256", id, password_security_policy); + valid = false; + } + } + // Validate the security policy and mode let security_policy = SecurityPolicy::from_str(&self.security_policy).unwrap(); let security_mode = MessageSecurityMode::from(self.security_mode.as_ref()); @@ -288,7 +299,6 @@ impl Config for ServerConfig { fn product_uri(&self) -> UAString { UAString::from(self.product_uri.as_ref()) } } - impl Default for ServerConfig { fn default() -> Self { let pki_dir = PathBuf::from("./pki"); diff --git a/server/src/lib.rs b/server/src/lib.rs index cbfdf5d3f..f9b2d5e67 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -14,7 +14,7 @@ //! //! This is a minimal server which runs with the default address space on the default port. //! -//! ```rust,no_run +//! ```no_run //! use opcua_server::prelude::*; //! //! fn main() { diff --git a/server/src/services/session.rs b/server/src/services/session.rs index 1dff97f6e..b7a2b5682 100644 --- a/server/src/services/session.rs +++ b/server/src/services/session.rs @@ -154,15 +154,18 @@ impl SessionService { StatusCode::Good }; - // Authenticate the user identity token if service_result.is_good() { - service_result = server_state.authenticate_endpoint(endpoint_url, security_policy, security_mode, &request.user_identity_token); + if let Err(err) = server_state.authenticate_endpoint(endpoint_url, security_policy, security_mode, &request.user_identity_token, &session.session_nonce) { + service_result = err; + } } + // Authenticate the user identity token let response = if service_result.is_good() { session.activated = true; session.session_nonce = server_nonce; let diagnostic_infos = None; + ActivateSessionResponse { response_header: ResponseHeader::new_good(&request.request_header), server_nonce: session.session_nonce.clone(), diff --git a/server/src/state.rs b/server/src/state.rs index 9c3c436e0..141d78279 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,6 +1,7 @@ //! Provides server state information, such as status, configuration, running servers and so on. use std::sync::{Arc, RwLock}; +use std::str::FromStr; use opcua_core::prelude::*; use opcua_core::crypto::user_identity; @@ -135,12 +136,26 @@ impl ServerState { }); } if !endpoint.user_token_ids.is_empty() { + // The endpoint may set a password security policy + let password_security_policy = if let Some(ref security_policy) = endpoint.password_security_policy { + if let Ok(security_policy) = SecurityPolicy::from_str(security_policy) { + if security_policy != SecurityPolicy::Unknown { + UAString::from(security_policy.to_str()) + } else { + UAString::null() + } + } else { + UAString::null() + } + } else { + UAString::null() + }; user_identity_tokens.push(UserTokenPolicy { policy_id: UAString::from(TOKEN_POLICY_USER_PASS_PLAINTEXT), token_type: UserTokenType::Username, issued_token_type: UAString::null(), issuer_endpoint_url: UAString::null(), - security_policy_uri: UAString::from(SecurityPolicy::None.to_uri()), + security_policy_uri: password_security_policy, }); } @@ -262,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) -> StatusCode { + pub fn authenticate_endpoint(&self, 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(); @@ -271,50 +286,48 @@ impl ServerState { if user_identity_token.is_null() || user_identity_token.is_empty() { // Empty tokens are treated as anonymous Self::authenticate_anonymous_token(endpoint) - } else { + } else if let Ok(object_id) = user_identity_token.node_id.as_object_id() { // Read the token out from the extension object - if let Ok(object_id) = user_identity_token.node_id.as_object_id() { - match object_id { - ObjectId::AnonymousIdentityToken_Encoding_DefaultBinary => { - // Anonymous - Self::authenticate_anonymous_token(endpoint) - } - ObjectId::UserNameIdentityToken_Encoding_DefaultBinary => { - // Username / password - let result = user_identity_token.decode_inner::(&decoding_limits); - if let Ok(token) = result { - self.authenticate_username_identity_token(&config, endpoint, &token) - } else { - // Garbage in the extension object - error!("User name identity token could not be decoded"); - StatusCode::BadIdentityTokenInvalid - } - } - ObjectId::X509IdentityToken_Encoding_DefaultBinary => { - // X509 certs could be recognized here - let result = user_identity_token.decode_inner::(&decoding_limits); - if let Ok(_) = result { - error!("X509 identity token type is not supported"); - StatusCode::BadIdentityTokenRejected - } else { - // Garbage in the extension object - error!("X509 identity token could not be decoded"); - StatusCode::BadIdentityTokenInvalid - } + match object_id { + ObjectId::AnonymousIdentityToken_Encoding_DefaultBinary => { + // Anonymous + Self::authenticate_anonymous_token(endpoint) + } + ObjectId::UserNameIdentityToken_Encoding_DefaultBinary => { + // Username / password + let result = user_identity_token.decode_inner::(&decoding_limits); + if let Ok(token) = result { + self.authenticate_username_identity_token(&config, endpoint, &token, server_nonce, &self.server_pkey) + } else { + // Garbage in the extension object + error!("User name identity token could not be decoded"); + Err(StatusCode::BadIdentityTokenInvalid) } - _ => { - error!("User identity token type {:?} is unrecognized", object_id); - StatusCode::BadIdentityTokenInvalid + } + ObjectId::X509IdentityToken_Encoding_DefaultBinary => { + // X509 certs could be recognized here + let result = user_identity_token.decode_inner::(&decoding_limits); + if let Ok(_) = result { + error!("X509 identity token type is not supported"); + Err(StatusCode::BadIdentityTokenRejected) + } else { + // Garbage in the extension object + error!("X509 identity token could not be decoded"); + Err(StatusCode::BadIdentityTokenInvalid) } } - } else { - error!("Cannot read user identity token"); - StatusCode::BadIdentityTokenInvalid + _ => { + error!("User identity token type {:?} is unrecognized", object_id); + Err(StatusCode::BadIdentityTokenInvalid) + } } + } else { + error!("Cannot read user identity token"); + Err(StatusCode::BadIdentityTokenInvalid) } } else { error!("Cannot find endpoint that matches path \"{}\", security policy {:?}, and security mode {:?}", endpoint_url, security_policy, security_mode); - StatusCode::BadTcpEndpointUrlInvalid + Err(StatusCode::BadTcpEndpointUrlInvalid) } } @@ -324,57 +337,58 @@ impl ServerState { } /// Authenticates an anonymous token, i.e. does the endpoint support anonymous access or not - fn authenticate_anonymous_token(endpoint: &ServerEndpoint) -> StatusCode { + fn authenticate_anonymous_token(endpoint: &ServerEndpoint) -> Result<(), StatusCode> { if endpoint.supports_anonymous() { debug!("Anonymous identity is authenticated"); - StatusCode::Good + Ok(()) } else { error!("Endpoint \"{}\" does not support anonymous authentication", endpoint.path); - StatusCode::BadIdentityTokenRejected + Err(StatusCode::BadIdentityTokenRejected) } } /// Authenticates the username identity token with the supplied endpoint - fn authenticate_username_identity_token(&self, config: &ServerConfig, endpoint: &ServerEndpoint, token: &UserNameIdentityToken) -> StatusCode { + fn authenticate_username_identity_token(&self, config: &ServerConfig, endpoint: &ServerEndpoint, token: &UserNameIdentityToken, server_nonce: &ByteString, server_key: &Option) -> Result<(), StatusCode> { // The policy_id should be used to determine the algorithm for encoding passwords etc. if token.user_name.is_null() { error!("User identify token supplies no user name"); - StatusCode::BadIdentityTokenInvalid - } - else { - // TODO Server's user token policy should be checked here. - if !token.encryption_algorithm.is_null() { - // Plaintext is the only supported algorithm at this time - error!("Only unencrypted passwords are supported, {:?}", token); - return StatusCode::BadIdentityTokenInvalid; - } - // let password = user_identity::decrypt_user_identity_token_password(&token, nonce, key); + Err(StatusCode::BadIdentityTokenInvalid) + } else { + let token_password = if !token.encryption_algorithm.is_null() { + if let Some(ref server_key) = server_key { + user_identity::decrypt_user_identity_token_password(&token, server_nonce.as_ref(), server_key)? + } else { + error!("Identity token password is encrypted and no server private key was supplied"); + return Err(StatusCode::BadIdentityTokenInvalid); + } + } else { + token.plaintext_password()? + }; // 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() { // test for empty password - let result = if server_user_token.pass.is_none() { + let valid = if server_user_token.pass.is_none() { // Empty password for user - token.authenticate(&server_user_token.user, b"") + token_password.is_empty() } else { // Password compared as UTF-8 bytes let server_password = server_user_token.pass.as_ref().unwrap().as_bytes(); - token.authenticate(&server_user_token.user, server_password) + server_password == token_password.as_bytes() }; - let valid = result.is_ok(); if !valid { error!("Cannot authenticate \"{}\", password is invalid", server_user_token.user); - return StatusCode::BadIdentityTokenRejected; + return Err(StatusCode::BadIdentityTokenRejected); } else { - return StatusCode::Good; + return Ok(()); } } } } error!("Cannot authenticate \"{}\", user not found for endpoint", token.user_name); - StatusCode::BadIdentityTokenRejected + Err(StatusCode::BadIdentityTokenRejected) } } } diff --git a/server/src/tests/services/session.rs b/server/src/tests/services/session.rs index ccb19556f..592565faa 100644 --- a/server/src/tests/services/session.rs +++ b/server/src/tests/services/session.rs @@ -14,17 +14,19 @@ fn anonymous_user_token() { }; let token = ExtensionObject::from_encodable(ObjectId::AnonymousIdentityToken_Encoding_DefaultBinary, &token); - let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token); + let server_nonce = ByteString::random(20); + + let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce); trace!("result = {:?}", result); - assert!(result.is_good()); + assert!(result.is_ok()); - let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/x", SecurityPolicy::None, MessageSecurityMode::None, &token); + let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/x", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce); trace!("result = {:?}", result); - assert_eq!(result, StatusCode::BadTcpEndpointUrlInvalid); + assert_eq!(result.unwrap_err(), StatusCode::BadTcpEndpointUrlInvalid); - let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/noaccess", SecurityPolicy::None, MessageSecurityMode::None, &token); + let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/noaccess", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce); trace!("result = {:?}", result); - assert_eq!(result, StatusCode::BadIdentityTokenRejected); + assert_eq!(result.unwrap_err(), StatusCode::BadIdentityTokenRejected); } fn make_user_name_identity_token(user: &str, pass: &[u8]) -> ExtensionObject { @@ -43,21 +45,23 @@ fn user_name_pass_token() { let server_state = server.server_state(); let server_state = server_state.read().unwrap(); + let server_nonce = ByteString::random(20); + // 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); - assert!(result.is_good()); + let result = server_state.authenticate_endpoint("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); - assert_eq!(result, StatusCode::BadIdentityTokenRejected); + let result = server_state.authenticate_endpoint("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); - assert_eq!(result, StatusCode::BadIdentityTokenRejected); + let result = server_state.authenticate_endpoint("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); - assert_eq!(result, StatusCode::BadIdentityTokenRejected); + let result = server_state.authenticate_endpoint("opc.tcp://localhost:4855/", SecurityPolicy::None, MessageSecurityMode::None, &token, &server_nonce); + assert_eq!(result.unwrap_err(), StatusCode::BadIdentityTokenRejected); } diff --git a/types/src/service_types/impls.rs b/types/src/service_types/impls.rs index 73bb1e6f5..2d9118292 100644 --- a/types/src/service_types/impls.rs +++ b/types/src/service_types/impls.rs @@ -409,6 +409,15 @@ impl UserNameIdentityToken { !self.user_name.is_null() && !self.password.is_null() } + // Get the plaintext password as a string, if possible. + pub fn plaintext_password(&self) -> Result { + if !self.encryption_algorithm.is_empty() { + // Should not be calling this function at all encryption is applied + panic!(); + } + String::from_utf8(self.password.as_ref().to_vec()).map_err(|_| StatusCode::BadDecodingError) + } + /// Authenticates the token against the supplied username and password. pub fn authenticate(&self, username: &str, password: &[u8]) -> Result<(), StatusCode> { // No comparison will be made unless user and pass are explicitly set to something in the token