Browse Source

Security: Added support for server to decrypt passwords

The password is decrypted by the server implementation before calling
the access control plugin.
Jonas Green 4 years ago
parent
commit
5d375771aa
2 changed files with 135 additions and 3 deletions
  1. 3 3
      plugins/ua_accesscontrol_default.c
  2. 132 0
      src/server/ua_services_session.c

+ 3 - 3
plugins/ua_accesscontrol_default.c

@@ -75,9 +75,9 @@ activateSession_default(UA_Server *server, UA_AccessControl *ac,
         if(!UA_String_equal(&userToken->policyId, &username_policy))
             return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
 
-        /* TODO: Support encrypted username/password over unencrypted SecureChannels */
-        if(userToken->encryptionAlgorithm.length > 0)
-            return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
+        /* The userToken has been decrypted by the server before forwarding
+         * it to the plugin. This information can be used here. */
+        /* if(userToken->encryptionAlgorithm.length > 0) {} */
 
         /* Empty username and password */
         if(userToken->userName.length == 0 && userToken->password.length == 0)

+ 132 - 0
src/server/ua_services_session.c

@@ -246,6 +246,64 @@ checkSignature(const UA_Server *server, const UA_SecureChannel *channel,
     return retval;
 }
 
+#ifdef UA_ENABLE_ENCRYPTION
+static UA_StatusCode
+decryptPassword(UA_SecurityPolicy *securityPolicy, void *tempChannelContext,
+                const UA_ByteString *serverNonce, UA_UserNameIdentityToken *userToken) {
+    UA_SecurityPolicyEncryptionAlgorithm *asymEnc =
+        &securityPolicy->asymmetricModule.cryptoModule.encryptionAlgorithm;
+    if(!UA_String_equal(&userToken->encryptionAlgorithm, &asymEnc->uri))
+        return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
+
+    UA_UInt32 tokenSecretLength;
+    UA_ByteString decryptedTokenSecret, tokenServerNonce;
+    if(UA_ByteString_copy(&userToken->password, &decryptedTokenSecret) != UA_STATUSCODE_GOOD)
+        return UA_STATUSCODE_BADIDENTITYTOKENINVALID;
+
+    UA_StatusCode retval = UA_STATUSCODE_BADIDENTITYTOKENINVALID;
+    if(asymEnc->decrypt(securityPolicy, tempChannelContext,
+                        &decryptedTokenSecret) != UA_STATUSCODE_GOOD)
+        goto cleanup;
+
+    memcpy(&tokenSecretLength, decryptedTokenSecret.data, sizeof(UA_UInt32));
+
+    /* The decrypted data must be large enough to include the Encrypted Token
+     * Secret Format and the length field must indicate enough data to include
+     * the server nonce. */
+    if(decryptedTokenSecret.length < sizeof(UA_UInt32) + serverNonce->length ||
+       decryptedTokenSecret.length < sizeof(UA_UInt32) + tokenSecretLength ||
+       tokenSecretLength < serverNonce->length)
+        goto cleanup;
+
+    /* If the Encrypted Token Secret contains padding, the padding must be
+     * zeroes according to the 1.04.1 specification errata, chapter 3. */
+    for(size_t i = sizeof(UA_UInt32) + tokenSecretLength; i < decryptedTokenSecret.length; i++) {
+        if(decryptedTokenSecret.data[i] != 0)
+            goto cleanup;
+    }
+
+    /* The server nonce must match according to the 1.04.1 specification errata,
+     * chapter 3. */
+    tokenServerNonce.length = serverNonce->length;
+    tokenServerNonce.data = &decryptedTokenSecret.data[sizeof(UA_UInt32) + tokenSecretLength - serverNonce->length];
+    if(!UA_ByteString_equal(serverNonce, &tokenServerNonce))
+        goto cleanup;
+
+    /* The password was decrypted successfully. Replace usertoken with the
+     * decrypted password. The encryptionAlgorithm and policyId fields are left
+     * in the UserToken as an indication for the AccessControl plugin that
+     * evaluates the decrypted content. */
+    memcpy(userToken->password.data, &decryptedTokenSecret.data[sizeof(UA_UInt32)],
+           tokenSecretLength - serverNonce->length);
+    userToken->password.length = tokenSecretLength - serverNonce->length;
+    retval = UA_STATUSCODE_GOOD;
+
+ cleanup:
+    UA_ByteString_deleteMembers(&decryptedTokenSecret);
+    return retval;
+}
+#endif
+
 /* TODO: Check all of the following:
  *
  * Part 4, §5.6.3: When the ActivateSession Service is called for the first time
@@ -334,6 +392,80 @@ Service_ActivateSession(UA_Server *server, UA_SecureChannel *channel,
         return;
     }
 
+#ifdef UA_ENABLE_ENCRYPTION
+    /* If it is a UserNameIdentityToken, decrypt the password if encrypted */
+    if((request->userIdentityToken.encoding == UA_EXTENSIONOBJECT_DECODED) &&
+       (request->userIdentityToken.content.decoded.type == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN])) {
+       UA_UserNameIdentityToken *userToken = (UA_UserNameIdentityToken *)
+           request->userIdentityToken.content.decoded.data;
+
+       /* Find the UserTokenPolicy */
+       UA_Byte tokenIndex = 0;
+       for(; tokenIndex < ed->userIdentityTokensSize; tokenIndex++) {
+           if(ed->userIdentityTokens[tokenIndex].tokenType != UA_USERTOKENTYPE_USERNAME)
+               continue;
+           if(UA_String_equal(&userToken->policyId, &ed->userIdentityTokens[tokenIndex].policyId))
+               break;
+       }
+       if(tokenIndex == ed->userIdentityTokensSize) {
+           response->responseHeader.serviceResult = UA_STATUSCODE_BADIDENTITYTOKENINVALID;
+           return;
+       }
+
+       /* Get the SecurityPolicy. If the userTokenPolicy doesn't specify a
+        * security policy the security policy of the secure channel is used. */
+       UA_SecurityPolicy* securityPolicy;
+       if(ed->userIdentityTokens[tokenIndex].securityPolicyUri.data == NULL)
+           securityPolicy = UA_SecurityPolicy_getSecurityPolicyByUri(server, &ed->securityPolicyUri);
+       else
+           securityPolicy = UA_SecurityPolicy_getSecurityPolicyByUri(server, &ed->userIdentityTokens[tokenIndex].securityPolicyUri);
+       if(!securityPolicy) {
+          response->responseHeader.serviceResult = UA_STATUSCODE_BADINTERNALERROR;
+          return;
+       }
+
+       /* Encrypted password? */
+       if(!UA_String_equal(&securityPolicy->policyUri, &UA_SECURITY_POLICY_NONE_URI)) {
+           /* Create a temporary channel context if a different SecurityPolicy is
+            * used for the password from the SecureChannel */
+           void *tempChannelContext = channel->channelContext;
+           if(securityPolicy != channel->securityPolicy) {
+               /* TODO: This is a hack. We use our own certificate to create a
+                * channel context. Because the client does not provide one in a
+                * #None SecureChannel. We should not need a ChannelContext at all
+                * for asymmetric decryption where the remote certificate is not
+                * used. */
+               response->responseHeader.serviceResult =
+                   securityPolicy->channelModule.newContext(securityPolicy,
+                                                            &securityPolicy->localCertificate,
+                                                            &tempChannelContext);
+               if(response->responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
+                   UA_LOG_WARNING_SESSION(&server->config.logger, session, "ActivateSession: "
+                                          "Failed to create a context for the SecurityPolicy %.*s",
+                                          (int)securityPolicy->policyUri.length,
+                                          securityPolicy->policyUri.data);
+                   return;
+               }
+           }
+
+           /* Decrypt */
+           response->responseHeader.serviceResult =
+               decryptPassword(securityPolicy, tempChannelContext, &session->serverNonce, userToken);
+
+           /* Remove the temporary channel context */
+           if(securityPolicy != channel->securityPolicy)
+               securityPolicy->channelModule.deleteContext(tempChannelContext);
+       }
+
+       if(response->responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
+           UA_LOG_INFO_SESSION(&server->config.logger, session, "ActivateSession: "
+                               "Failed to decrypt the password with the status code %s",
+                               UA_StatusCode_name(response->responseHeader.serviceResult));
+       }
+
+    }
+#endif
+
     /* Callback into userland access control */
     response->responseHeader.serviceResult =
         server->config.accessControl.activateSession(server, &server->config.accessControl,