diff --git a/samples/demo-server/users/sample-x509.der b/samples/demo-server/users/sample-x509.der new file mode 100644 index 000000000..7bc73a1cc Binary files /dev/null and b/samples/demo-server/users/sample-x509.der differ diff --git a/samples/server.conf b/samples/server.conf index 4c8850e54..d9d5eb5c8 100644 --- a/samples/server.conf +++ b/samples/server.conf @@ -11,9 +11,12 @@ tcp_config: host: 127.0.0.1 port: 4855 user_tokens: - sample_user: + sample_password_user: user: sample pass: sample1 + sample_x509_user: + user: sample_x509 + x509: "./users/sample-x509.der" unused_user: user: unused pass: unused1 @@ -28,7 +31,8 @@ endpoints: password_security_policy: ~ user_token_ids: - ANONYMOUS - - sample_user + - sample_password_user + - sample_x509_user basic128rsa15_sign_encrypt: path: / security_policy: Basic128Rsa15 @@ -37,7 +41,8 @@ endpoints: password_security_policy: ~ user_token_ids: - ANONYMOUS - - sample_user + - sample_password_user + - sample_x509_user basic256_sign: path: / security_policy: Basic256 @@ -46,7 +51,8 @@ endpoints: password_security_policy: ~ user_token_ids: - ANONYMOUS - - sample_user + - sample_password_user + - sample_x509_user basic256_sign_encrypt: path: / security_policy: Basic256 @@ -55,7 +61,8 @@ endpoints: password_security_policy: ~ user_token_ids: - ANONYMOUS - - sample_user + - sample_password_user + - sample_x509_user basic256sha256_sign: path: / security_policy: Basic256Sha256 @@ -64,7 +71,8 @@ endpoints: password_security_policy: ~ user_token_ids: - ANONYMOUS - - sample_user + - sample_password_user + - sample_x509_user basic256sha256_sign_encrypt: path: / security_policy: Basic256Sha256 @@ -73,7 +81,8 @@ endpoints: password_security_policy: ~ user_token_ids: - ANONYMOUS - - sample_user + - sample_password_user + - sample_x509_user no_access: path: /noaccess security_policy: None @@ -89,7 +98,8 @@ endpoints: password_security_policy: ~ user_token_ids: - ANONYMOUS - - sample_user + - sample_password_user + - sample_x509_user max_subscriptions: 100 max_array_length: 1000 max_string_length: 65536 diff --git a/samples/simple-client/identity/sample-x509.der b/samples/simple-client/identity/sample-x509.der new file mode 100644 index 000000000..7bc73a1cc Binary files /dev/null and b/samples/simple-client/identity/sample-x509.der differ diff --git a/samples/simple-client/identity/sample-x509.pem b/samples/simple-client/identity/sample-x509.pem new file mode 100644 index 000000000..327474ec1 --- /dev/null +++ b/samples/simple-client/identity/sample-x509.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwS49oftNWSMyN +kmCgxTT0SilJrFKYuK77X/lGrzEdtz8HM/05nQR0McKSGLDFki59fUPzthn/btfm +XrAP/wEaOkdmZxmA4J8Wzf7W9wTn2cNW0Piruis9tgNSvplxA5dX6ZxEWbGkB5eo ++aMwRK5ON5aJo2BtVibBBaszgL9IrK4hecU9+f4SQmEjw7s5ETCPaYYb0t1vN+2f +n2IiKK4qcheGeGOpzk3gXUKSi6MKPoB+/VvwZGfxCmM+w8K5luF3RY+M0JzIHXdB +9tHjKpdSuphnMp6ZU3YMUmTpaKj4no0BTzeCRalr993cEEkuln4cTA6mh/lNbkPm +rtn3aX2DAgMBAAECggEAJylBu/agP4SAW9puOIhWEQYAUetDlcVAqXpSR09XW8B+ +8bysvYWRnbYIAKgXbGvig+G1nIeREtqufu/9sC/3MLpNbgPs+GHtNQWhXGMW5eHL +sJdPBeafAGBUMKdCMoaXseGk4tIB0ewV1mVNyMUY6ysR95UhMGh4x1vZAeHRm/TR +Yp/yIn0b2r8um5jgDjo+oghqvnvmArykXrdiMdoZED/waKM9s+zoJ4XkMs/Fe4Qn +drMILsmS6hAZGfe246G3Ud4xvqsPy57sAYgDM8LxBEfS53AmtMQsdyud4V5zdtZr +M1maShdu2RlQxoeH+Vu2fdxoOfxFJ2L2r8aQ9b85IQKBgQDfGMPL0WjLFm6XlVEK +hwjKkQ0S7F4B3JmoaMYJaqAazxjW/qh/b4xqZ4yzkQsFvTrByrGvbkXXdowj74fL +XYcrOLw0LPZJmzWsRsBIj1tbWXsvKcFbE8CRuUAk2Wei3gdlXceq8vm6cpowxt0B +MKRYrmds4q+mputqiSumggvmmQKBgQDKS8GIIoxf9968h6tayq50VoNVfIB9e5IV +CwgtMLkX+bw1Oxz6OSgJlppC9OmFON5ppiYP3aKYtu6YCpqXvqqeFwyv8TrkhQcr +uZ8MO5w5fYGLXhi0OXN/tzKof8RycF8O/SmwBCKXtXNOwq/X4/8FXK3oBvCj5f2F +A/LRDFiCewKBgFADoM2sCIq2O+nv6sX80mFcjrTXw4ulZBLrqQNdk5ip6D3LzgEO +r+zFwMfyYGKpkLZKjVnfEfuKEA8fbLO6kq0kxxNrgNW7bg+gvHwJtnlX6X9r2WZh ++jIJoADXXH0kZsCrVt5wELMXQUf3OvKfUIJh4sRBtT/vJAXstpQclkoZAoGBAIA5 +9qlY5Mur7RZplJcPI/eAIu1b5oIjgpwuCvfCC4ED/mVrW9nLwvIY8R0B6sdUHb6v +3y5tWTQduCzNg+ItrC5bA+K+MItLOxlfJk51tnfGcwepFFWgmPJaaBTgL+AuFEMG ++5ajeF3bWQSSaS2aSjrW3TDWvU/WZ5UZxJ73iV7jAoGAEjdbhqbHRiLzHlc1NPsm +wTikjvEOmE3GXgVQKv/r47vrpXmGPBBsBa08r0T00/cgRj5so2qqTe336k9Wi9vP +GQbG9lQqSXp8YH7HCpXd+PjU0NrJxdu2cdT8NLn5cMuXu7vtMZ1kyotKiHGBqYPe +O3INidX0+nHfInitSSBmCrQ= +-----END PRIVATE KEY----- diff --git a/samples/simple-server/users/sample-x509.der b/samples/simple-server/users/sample-x509.der new file mode 100644 index 000000000..7bc73a1cc Binary files /dev/null and b/samples/simple-server/users/sample-x509.der differ diff --git a/server/src/builder.rs b/server/src/builder.rs index 001fbb211..344017b83 100644 --- a/server/src/builder.rs +++ b/server/src/builder.rs @@ -42,19 +42,25 @@ impl ServerBuilder { warn!("Sample configuration is for testing purposes only. Use a proper configuration in your production environment"); let path = DEFAULT_ENDPOINT_PATH; - let sample_user_id = "sample_user"; - let user_token_ids = vec![ANONYMOUS_USER_TOKEN_ID.to_string(), sample_user_id.to_string()]; + + let user_token_ids = ["sample_password_user", "sample_x509_user", ANONYMOUS_USER_TOKEN_ID] + .iter().map(|u| u.to_string()).collect::>(); ServerBuilder::new() .application_name("OPC UA Sample Server") .application_uri("urn:OPC UA Sample Server") .create_sample_keypair(true) .discovery_server_url(Some(constants::DEFAULT_DISCOVERY_SERVER_URL.to_string())) - .user_token("sample_user", ServerUserToken { + .user_token("sample_password_user", ServerUserToken { user: "sample".to_string(), pass: Some("sample1".to_string()), x509: None, }) + .user_token("sample_x509_user", ServerUserToken { + user: "sample_x509".to_string(), + pass: None, + x509: Some("./users/sample-x509.der".to_string()), + }) .user_token("unused_user", ServerUserToken { user: "unused".to_string(), pass: Some("unused1".to_string()), diff --git a/server/src/config.rs b/server/src/config.rs index dabc004dc..08c2bb639 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -31,9 +31,9 @@ pub struct ServerUserToken { /// Password #[serde(skip_serializing_if = "Option::is_none")] pub pass: Option, - // X509 file path + // X509 file path (as a string) #[serde(skip_serializing_if = "Option::is_none")] - pub x509: Option, + pub x509: Option, } impl ServerUserToken { @@ -58,12 +58,9 @@ impl ServerUserToken { if self.pass.is_some() && self.x509.is_some() { error!("User token {} has a password and a path to an x509 cert", id); valid = false; - } - if let Some(ref path) = self.x509 { - if !path.exists() || !path.is_file() { - error!("User token {} x509 cert does not exist", id); - valid = false; - } + } else if self.pass.is_none() && self.x509.is_none() { + error!("User token {} is neither a password or an x509 cert", id); + valid = false; } valid } @@ -213,11 +210,57 @@ impl ServerEndpoint { format!("{}{}", base_endpoint, self.path) } + pub fn password_security_policy(&self) -> SecurityPolicy { + if let Some(ref security_policy) = self.password_security_policy { + if let Ok(security_policy) = SecurityPolicy::from_str(security_policy) { + if security_policy != SecurityPolicy::Unknown { + security_policy + } else { + SecurityPolicy::None + } + } else { + SecurityPolicy::None + } + } else { + SecurityPolicy::None + } + } + /// Test if the endpoint supports anonymous users pub fn supports_anonymous(&self) -> bool { self.supports_user_token_id(ANONYMOUS_USER_TOKEN_ID) } + /// Tests if this endpoint supports user pass tokens. It does this by looking to see + /// if any of the users allowed to access this endpoint are user pass users. + pub fn supports_user_pass(&self, server_tokens: &BTreeMap) -> bool { + for user_token_id in &self.user_token_ids { + if user_token_id != ANONYMOUS_USER_TOKEN_ID { + if let Some(user_token) = server_tokens.get(user_token_id) { + if user_token.is_user_pass() { + return true; + } + } + } + } + false + } + + /// Tests if this endpoint supports x509 tokens. It does this by looking to see + // /// if any of the users allowed to access this endpoint are x509 users. + pub fn supports_x509(&self, server_tokens: &BTreeMap) -> bool { + for user_token_id in &self.user_token_ids { + if user_token_id != ANONYMOUS_USER_TOKEN_ID { + if let Some(user_token) = server_tokens.get(user_token_id) { + if user_token.is_x509() { + return true; + } + } + } + } + false + } + pub fn supports_user_token_id(&self, id: &str) -> bool { self.user_token_ids.contains(id) } diff --git a/server/src/state.rs b/server/src/state.rs index 0e3f5082d..1511c9d8a 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,7 +1,6 @@ //! 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; @@ -11,7 +10,7 @@ use opcua_types::{ profiles, service_types::{ ActivateSessionRequest, ApplicationDescription, RegisteredServer, ApplicationType, EndpointDescription, - UserNameIdentityToken, UserTokenPolicy, UserTokenType, X509IdentityToken, SignatureData, + AnonymousIdentityToken, UserNameIdentityToken, UserTokenPolicy, UserTokenType, X509IdentityToken, SignatureData, ServerState as ServerStateType, }, status_code::StatusCode, @@ -21,8 +20,9 @@ use crate::config::{ServerConfig, ServerEndpoint}; use crate::diagnostics::ServerDiagnostics; use crate::callbacks::{RegisterNodes, UnregisterNodes}; -const TOKEN_POLICY_ANONYMOUS: &str = "anonymous"; -const TOKEN_POLICY_USER_PASS_PLAINTEXT: &str = "userpass_plaintext"; +const POLICY_ID_ANONYMOUS: &str = "anonymous"; +const POLICY_ID_USER_PASS: &str = "userpass"; +const POLICY_ID_X509: &str = "x509"; /// Server state is any state associated with the server as a whole that individual sessions might /// be interested in. That includes configuration info etc. @@ -125,37 +125,41 @@ impl ServerState { fn new_endpoint_description(&self, config: &ServerConfig, endpoint: &ServerEndpoint, all_fields: bool) -> EndpointDescription { let base_endpoint_url = config.base_endpoint_url(); - let mut user_identity_tokens = Vec::with_capacity(2); + let mut user_identity_tokens = Vec::with_capacity(3); + + // Anonymous policy if endpoint.supports_anonymous() { user_identity_tokens.push(UserTokenPolicy { - policy_id: UAString::from(TOKEN_POLICY_ANONYMOUS), + policy_id: UAString::from(POLICY_ID_ANONYMOUS), token_type: UserTokenType::Anonymous, issued_token_type: UAString::null(), issuer_endpoint_url: UAString::null(), security_policy_uri: UAString::null(), }); } - if !endpoint.user_token_ids.is_empty() { + // User pass policy + if endpoint.supports_user_pass(&config.user_tokens) { // 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() + let security_policy_uri = match endpoint.password_security_policy() { + SecurityPolicy::None => UAString::null(), + security_policy => UAString::from(security_policy.to_str()) }; user_identity_tokens.push(UserTokenPolicy { - policy_id: UAString::from(TOKEN_POLICY_USER_PASS_PLAINTEXT), + policy_id: UAString::from(POLICY_ID_USER_PASS), token_type: UserTokenType::Username, issued_token_type: UAString::null(), issuer_endpoint_url: UAString::null(), - security_policy_uri: password_security_policy, + security_policy_uri, + }); + } + // X509 policy + if endpoint.supports_x509(&config.user_tokens) { + user_identity_tokens.push(UserTokenPolicy { + policy_id: UAString::from(POLICY_ID_X509), + token_type: UserTokenType::Certificate, + issued_token_type: UAString::null(), + issuer_endpoint_url: UAString::null(), + security_policy_uri: UAString::null(), }); } @@ -285,13 +289,21 @@ impl ServerState { // Now validate the user identity token if user_identity_token.is_empty() { // Empty tokens are treated as anonymous - Self::authenticate_anonymous_token(endpoint) + Self::authenticate_anonymous_token(endpoint, &AnonymousIdentityToken { + policy_id: UAString::from(POLICY_ID_ANONYMOUS) + }) } else if let Ok(object_id) = user_identity_token.node_id.as_object_id() { // Read the token out from the extension object match object_id { ObjectId::AnonymousIdentityToken_Encoding_DefaultBinary => { - // Anonymous - Self::authenticate_anonymous_token(endpoint) + if let Ok(token) = user_identity_token.decode_inner::(&decoding_limits) { + // Anonymous + Self::authenticate_anonymous_token(endpoint, &token) + } else { + // Garbage in the extension object + error!("Anonymous identity token could not be decoded"); + Err(StatusCode::BadIdentityTokenInvalid) + } } ObjectId::UserNameIdentityToken_Encoding_DefaultBinary => { // Username / password @@ -314,7 +326,7 @@ impl ServerState { } } _ => { - error!("User identity token type {:?} is unrecognized", object_id); + error!("User identity token type {:?} is unsupported", object_id); Err(StatusCode::BadIdentityTokenInvalid) } } @@ -334,53 +346,28 @@ impl ServerState { } /// Authenticates an anonymous token, i.e. does the endpoint support anonymous access or not - fn authenticate_anonymous_token(endpoint: &ServerEndpoint) -> Result<(), StatusCode> { - if endpoint.supports_anonymous() { - debug!("Anonymous identity is authenticated"); - Ok(()) - } else { + fn authenticate_anonymous_token(endpoint: &ServerEndpoint, token: &AnonymousIdentityToken) -> Result<(), StatusCode> { + if token.policy_id.as_ref() != POLICY_ID_ANONYMOUS { + error!("Token doesn't possess the correct policy id"); + Err(StatusCode::BadIdentityTokenRejected) + } else if !endpoint.supports_anonymous() { error!("Endpoint \"{}\" does not support anonymous authentication", endpoint.path); Err(StatusCode::BadIdentityTokenRejected) + } else { + debug!("Anonymous identity is authenticated"); + Ok(()) } } - 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) => { - 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) } - }) - } - - /// Authenticates the username identity token with the supplied endpoint fn authenticate_username_identity_token(&self, config: &ServerConfig, endpoint: &ServerEndpoint, token: &UserNameIdentityToken, server_key: &Option, server_nonce: &ByteString) -> Result<(), StatusCode> { - // The policy_id should be used to determine the algorithm for encoding passwords etc. - if token.user_name.is_null() { + if !endpoint.supports_user_pass(&config.user_tokens) { + error!("Endpoint doesn't support username password tokens"); + Err(StatusCode::BadIdentityTokenRejected) + } else if token.policy_id.as_ref() != POLICY_ID_USER_PASS { + error!("Token doesn't possess the correct policy id"); + Err(StatusCode::BadIdentityTokenRejected) + } else if token.user_name.is_null() { error!("User identify token supplies no user name"); Err(StatusCode::BadIdentityTokenInvalid) } else { @@ -388,7 +375,7 @@ impl ServerState { 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"); + error!("Identity token password is encrypted but no server private key was supplied"); return Err(StatusCode::BadIdentityTokenInvalid); } } else { @@ -421,4 +408,44 @@ impl ServerState { Err(StatusCode::BadIdentityTokenRejected) } } + + /// Authenticate the x509 token against the endpoint + fn authenticate_x509_identity_token(&self, config: &ServerConfig, endpoint: &ServerEndpoint, token: &X509IdentityToken, user_token_signature: &SignatureData, server_certificate: &Option, server_nonce: &ByteString) -> Result<(), StatusCode> { + if !endpoint.supports_x509(&config.user_tokens) { + error!("Endpoint doesn't support x509 tokens"); + Err(StatusCode::BadIdentityTokenRejected) + } else if token.policy_id.as_ref() != POLICY_ID_X509 { + error!("Token doesn't possess the correct policy id"); + Err(StatusCode::BadIdentityTokenRejected) + } else { + let result = match server_certificate { + Some(ref server_certificate) => { + 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) } + }) + } + } } diff --git a/server/src/tests/services/session.rs b/server/src/tests/services/session.rs index a99488815..67ff110fe 100644 --- a/server/src/tests/services/session.rs +++ b/server/src/tests/services/session.rs @@ -22,7 +22,7 @@ fn anonymous_user_token() { // Makes an anonymous token and sticks it into an extension object let token = AnonymousIdentityToken { - policy_id: UAString::from(SecurityPolicy::None.to_uri()) + policy_id: UAString::from("anonymous") }; let token = ExtensionObject::from_encodable(ObjectId::AnonymousIdentityToken_Encoding_DefaultBinary, &token); @@ -45,7 +45,7 @@ fn anonymous_user_token() { fn make_user_name_identity_token(user: &str, pass: &[u8]) -> ExtensionObject { let token = UserNameIdentityToken { - policy_id: UAString::from(SecurityPolicy::None.to_uri()), + policy_id: UAString::from("userpass"), user_name: UAString::from(user), password: ByteString::from(pass), encryption_algorithm: UAString::null(),