diff --git a/Project.toml b/Project.toml index 6b4d8e7..151846f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Open62541" uuid = "e9b70463-8ccb-4e30-a2e2-0d1ec8db6536" authors = ["Martin Kosch and contributors"] -version = "0.2.0" +version = "0.2.1" [deps] CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" diff --git a/docs/make.jl b/docs/make.jl index 918f63b..5f836f4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -22,6 +22,7 @@ makedocs(; "tutorials/client_first_steps.md", "tutorials/combined_variables.md", "tutorials/combined_username_password_login.md", + "tutorials/combined_encrypted_un_pw_login.md", "tutorials/further_resources.md" ], "Manual" => [ diff --git a/docs/src/tutorials/combined_encrypted_un_pw_login.md b/docs/src/tutorials/combined_encrypted_un_pw_login.md new file mode 100644 index 0000000..6b8d304 --- /dev/null +++ b/docs/src/tutorials/combined_encrypted_un_pw_login.md @@ -0,0 +1,119 @@ +# Encrypted username/password authentication using basic access control + +In this tutorial, we will showcase how authentication using a username and password can be +accomplished using Open62541.jl. Following up from [Username/password authentication using basic access control](@ref) +the server and client will now be configured to use encryption, so that usernames and +passwords are transmitted safely across the network. + +## Configuring the server +Here we configure the server to accept a username/password combination. We will also set up +encryption and disallow anonymous logins. The code block is commented line by line. + +```julia +using Open62541 + +#generate a basic server certificate +#generate a basic server certificate +certificate = UA_ByteString_new() +privateKey = UA_ByteString_new() +subject = UA_String_Array_new([UA_String_fromChars("C=DE"), + UA_String_fromChars("O=SampleOrganization"), + UA_String_fromChars("CN=Open62541Server@localhost")]) +lenSubject = UA_UInt32(3) +subjectAltName = UA_String_Array_new([UA_String_fromChars("DNS:localhost"), + UA_String_fromChars("URI:urn:open62541.server.application")]) +lenSubjectAltName = UA_UInt32(2) +kvm = UA_KeyValueMap_new() +expiresIn = UA_UInt16(14) +retval0 = UA_KeyValueMap_setScalar(kvm, JUA_QualifiedName(0, "expires-in-days"), Ref(expiresIn), + UA_TYPES_PTRS[UA_TYPES_UINT16]) +retval1 = UA_CreateCertificate(UA_Log_Stdout_new(UA_LOGLEVEL_FATAL), subject.ptr, lenSubject, + subjectAltName.ptr, lenSubjectAltName, UA_CERTIFICATEFORMAT_DER, kvm, privateKey, + certificate) + +#configure the open62541 server; we choose a default config on port 4840. +server = JUA_Server() +config = JUA_ServerConfig(server) +JUA_ServerConfig_setDefault(config) +JUA_ServerConfig_addSecurityPolicyBasic256Sha256(config, certificate, + privateKey) +JUA_ServerConfig_addAllEndpoints(config) +config.securityPolicyNoneDiscoveryOnly = true +login = JUA_UsernamePasswordLogin("BruceWayne", "IamBatman") #specifies the user BruceWayne and his secret password. +allowAnonymous = false #disallow anonymous login +JUA_AccessControl_default(config, allowAnonymous, login) #set the access control inside the server config. + +JUA_Server_runUntilInterrupt(server) #start the server, shut it down by pressing CTRL+C repeatedly once you are finished with it. +``` + +## Using the client +Start a new Julia session and run the program shown below. Once you are finished, +you may want to return to the first Julia session and stop the server (press +CTRL + C repeatedly). Again, the code block is commented line by line. + +```julia +using Open62541 + +#initiate client, configure it and connect to server +client = JUA_Client() +config = UA_Client_getConfig(client) + +#generate a client certificate +certificate = UA_ByteString_new() +privateKey = UA_ByteString_new() +subject = UA_String_Array_new([UA_String_fromChars("C=DE"), + UA_String_fromChars("O=SampleOrganization"), + UA_String_fromChars("CN=Open62541Client@localhost")]) +lenSubject = UA_UInt32(3) +subjectAltName = UA_String_Array_new([UA_String_fromChars("DNS:localhost"), + UA_String_fromChars("URI:urn:open62541.client.application")]) +lenSubjectAltName = UA_UInt32(2) +kvm = UA_KeyValueMap_new() +expiresIn = UA_UInt16(14) +UA_KeyValueMap_setScalar(kvm, JUA_QualifiedName(0, "expires-in-days"), Ref(expiresIn), UA_TYPES_PTRS[UA_TYPES_UINT16]) +UA_CreateCertificate(UA_Log_Stdout_new(UA_LOGLEVEL_FATAL), subject.ptr, lenSubject, + subjectAltName.ptr, lenSubjectAltName, UA_CERTIFICATEFORMAT_DER, kvm, privateKey, + certificate) +revocationList = UA_ByteString_new() +revocationListSize = 0 +trustList = UA_ByteString_new() +trustListSize = 0 + +UA_ClientConfig_setDefaultEncryption(config, certificate, privateKey, + trustList, trustListSize, revocationList, revocationListSize) + +#set a few values manually +UA_CertificateVerification_AcceptAll(config.certificateVerification) #accept any server certificate +config.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT +config.clientDescription.applicationUri = UA_String_fromChars("urn:open62541.client.application") + +retval1 = JUA_Client_connectUsername(client, + "opc.tcp://localhost:4840", + "BruceWayne", + "IamBatman") #connect using the username and password +JUA_Client_disconnect(client) #disconnect + +#now let us try to connect with the wrong login credentials. +retval2 = JUA_Client_connectUsername(client, + "opc.tcp://localhost:4840", + "PeterParker", + "IamSpiderman") #try connecting using a wrong username/password + +#now let us try connecting as an anonymous user +retval3 = JUA_Client_connect(client, "opc.tcp://localhost:4840") + +#now let us try connecting without encryption +client = JUA_Client() +JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) +retval4 = JUA_Client_connectUsername(client, + "opc.tcp://localhost:4840", + "BruceWayne", + "IamBatman") #try connecting using a wrong username/password +``` +`retval1` should be `UA_STATUSCODE_GOOD` (= 0) indicating that authentication was sucessful, +whereas `retval2` and `retval3` should be `UA_STATUSCODE_BADUSERACCESSDENIED` (= 2149515264) +indicating that the second login and third login attempt were rejected (wrong user +credentials). The fourth login attempt returns `retval4`, which should be +`UA_STATUSCODE_BADIDENTITYTOKENREJECTED` (= 2149646336), because we tried using an +unencrypted connection to a server that demands an encrypted one. Therefore, the server has +rejected the identity token. diff --git a/docs/src/tutorials/combined_username_password_login.md b/docs/src/tutorials/combined_username_password_login.md index fcb51fd..eb87217 100644 --- a/docs/src/tutorials/combined_username_password_login.md +++ b/docs/src/tutorials/combined_username_password_login.md @@ -3,9 +3,14 @@ In this tutorial, we will showcase how authentication using a username and password (rather than an anonymous user) can be accomplished using Open62541.jl. +!!! warning + Note that in this basic configuration the login credentials are transmitted unencrypted + over the network, which is obviously *not recommended* when network traffic is + potentially exposed to unwanted listeners. + ## Configuring the server -Here we configure the server to accept a The -code block is commented line by line. +Here we configure the server to accept a username/password combination. We will also disallow +anonymous logins. The code block is commented line by line. ```julia using Open62541 @@ -16,7 +21,7 @@ config = JUA_ServerConfig(server) JUA_ServerConfig_setDefault(config) login = JUA_UsernamePasswordLogin("BruceWayne", "IamBatman") #specifies the user BruceWayne and his secret password. allowAnonymous = false #disallow anonymous login -retval = JUA_AccessControl_default(config, allowAnonymous, login) #set the access control inside the server config. +JUA_AccessControl_default(config, allowAnonymous, login) #set the access control inside the server config. JUA_Server_runUntilInterrupt(server) #start the server, shut it down by pressing CTRL+C repeatedly once you are finished with it. ``` @@ -32,9 +37,10 @@ using Open62541 #initiate client, configure it and connect to server client = JUA_Client() config = JUA_ClientConfig(client) +config.allowNonePolicyPassword = true #allow logging in with username/password on un-encrypted connections. JUA_ClientConfig_setDefault(config) -retval = JUA_Client_connectUsername(client, +retval1 = JUA_Client_connectUsername(client, "opc.tcp://localhost:4840", "BruceWayne", "IamBatman") #connect using the username and password @@ -47,12 +53,7 @@ retval2 = JUA_Client_connectUsername(client, "IamSpiderman") #try connecting using a wrong username/password JUA_Client_disconnect(client) #disconnect - ``` -`retval` should be `UA_STATUSCODE_GOOD` (= 0) indicating that authentication was sucessful, +`retval1` should be `UA_STATUSCODE_GOOD` (= 0) indicating that authentication was sucessful, whereas `retval2` should be `UA_STATUSCODE_BADUSERACCESSDENIED` (= 2149515264) indicating that the second login attempt was rejected. - -Note that in this basic configuration the login credentials are transmitted unencrypted, -which is obviously not recommended when network traffic is potentially exposed to -unwanted listeners. diff --git a/test/client_encryption.jl b/test/client_encryption.jl new file mode 100644 index 0000000..f8332af --- /dev/null +++ b/test/client_encryption.jl @@ -0,0 +1,147 @@ +using Distributed +Distributed.addprocs(1) # Add a single worker process to run the server + +Distributed.@everywhere begin + using Open62541 + using Test + using Pkg +end + +# Create a new server running at a worker process +Distributed.@spawnat Distributed.workers()[end] begin + #generate a basic server certificate + certificate = UA_ByteString_new() + privateKey = UA_ByteString_new() + subject = UA_String_Array_new([UA_String_fromChars("C=DE"), + UA_String_fromChars("O=SampleOrganization"), + UA_String_fromChars("CN=Open62541Server@localhost")]) + lenSubject = UA_UInt32(3) + subjectAltName = UA_String_Array_new([UA_String_fromChars("DNS:localhost"), + UA_String_fromChars("URI:urn:open62541.server.application")]) + lenSubjectAltName = UA_UInt32(2) + kvm = UA_KeyValueMap_new() + expiresIn = UA_UInt16(14) + retval0 = UA_KeyValueMap_setScalar(kvm, JUA_QualifiedName(0, "expires-in-days"), Ref(expiresIn), UA_TYPES_PTRS[UA_TYPES_UINT16]) + retval1 = UA_CreateCertificate( + UA_Log_Stdout_new(UA_LOGLEVEL_FATAL), subject.ptr, lenSubject, subjectAltName.ptr, lenSubjectAltName, + UA_CERTIFICATEFORMAT_DER, kvm, privateKey, certificate) + + #configure server + server = JUA_Server() + config = JUA_ServerConfig(server) + retval2 = JUA_ServerConfig_setDefault(config) + retval3 = JUA_ServerConfig_addSecurityPolicyBasic256Sha256(config, certificate, + privateKey) + retval4 = JUA_ServerConfig_addAllEndpoints(config) + config.securityPolicyNoneDiscoveryOnly = true + + #check + @test retval0 == UA_STATUSCODE_GOOD + @test retval1 == UA_STATUSCODE_GOOD + @test retval2 == UA_STATUSCODE_GOOD + @test retval3 == UA_STATUSCODE_GOOD + @test retval4 == UA_STATUSCODE_GOOD + + #clean up + UA_KeyValueMap_delete(kvm) + UA_ByteString_delete(privateKey) + UA_ByteString_delete(certificate) + + #run it + JUA_Server_runUntilInterrupt(server) +end + +#client code +client = JUA_Client() +config = UA_Client_getConfig(client) + +#generate a client certificate +certificate = UA_ByteString_new() +privateKey = UA_ByteString_new() +subject = UA_String_Array_new([UA_String_fromChars("C=DE"), + UA_String_fromChars("O=SampleOrganization"), + UA_String_fromChars("CN=Open62541Client@localhost")]) +lenSubject = UA_UInt32(3) +subjectAltName = UA_String_Array_new([UA_String_fromChars("DNS:localhost"), + UA_String_fromChars("URI:urn:open62541.client.application")]) +lenSubjectAltName = UA_UInt32(2) +kvm = UA_KeyValueMap_new() +expiresIn = UA_UInt16(14) +retval0 = UA_KeyValueMap_setScalar(kvm, JUA_QualifiedName(0, "expires-in-days"), Ref(expiresIn), UA_TYPES_PTRS[UA_TYPES_UINT16]) +retval1 = UA_CreateCertificate( + UA_Log_Stdout_new(UA_LOGLEVEL_FATAL), subject.ptr, lenSubject, subjectAltName.ptr, lenSubjectAltName, + UA_CERTIFICATEFORMAT_DER, kvm, privateKey, certificate) +revocationList = UA_ByteString_new() +revocationListSize = 0 +trustList = UA_ByteString_new() +trustListSize = 0 + +retval2 = UA_ClientConfig_setDefaultEncryption(config, certificate, privateKey, + trustList, trustListSize, + revocationList, revocationListSize) + +#clean up +UA_ByteString_delete(revocationList) +UA_ByteString_delete(trustList) +UA_ByteString_delete(privateKey) +UA_ByteString_delete(certificate) +UA_KeyValueMap_delete(kvm) + +#set a few values manually +UA_CertificateVerification_AcceptAll(config.certificateVerification) #accept any server certificate +config.securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT +config.clientDescription.applicationUri = UA_String_fromChars("urn:open62541.client.application") + +#check +@test retval0 == UA_STATUSCODE_GOOD +@test retval1 == UA_STATUSCODE_GOOD +@test retval2 == UA_STATUSCODE_GOOD + +#now connect it +max_duration = 90.0 # Maximum waiting time for server startup +sleep_time = 2.0 # Sleep time in seconds between each connection trial +let trial + trial = 0 + while trial < max_duration / sleep_time + retval = JUA_Client_connect(client, "opc.tcp://localhost:4840") + if retval == UA_STATUSCODE_GOOD + println("Connection established.") + break + end + sleep(sleep_time) + trial = trial + 1 + end + @test trial < max_duration / sleep_time # Check if maximum number of trials has been exceeded +end + +# Read nodeid from server +nodeid = JUA_NodeId(0, UA_NS0ID_SERVER_SERVERSTATUS_BUILDINFO_SOFTWAREVERSION) +open62541_version_server = JUA_Client_readValueAttribute(client, nodeid) + +vn2string(vn::VersionNumber) = "$(vn.major).$(vn.minor).$(vn.patch)" +@static if VERSION < v"1.9" + pkgdir_old(m::Core.Module) = abspath(Base.pathof(Base.moduleroot(m)), "..", "..") + function pkgproject_old(m::Core.Module) + Pkg.Operations.read_project(Pkg.Types.projectfile_path(pkgdir_old(m))) + end + pkgversion_old(m::Core.Module) = pkgproject_old(m).version + open62541_version_julia = pkgversion_old(open62541_jll) +else + open62541_version_julia = pkgversion(open62541_jll) +end +open62541_version_julia = vn2string(open62541_version_julia) +@test open62541_version_server == open62541_version_julia + +JUA_Client_disconnect(client) + +#now try connecting unencrypted +client = JUA_Client() +retval4 = JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) +retval5 = JUA_Client_connect(client, "opc.tcp://localhost:4840") + +@test retval4 == UA_STATUSCODE_GOOD +@test retval5 == UA_STATUSCODE_BADSECURITYPOLICYREJECTED + +println("Ungracefully kill server process...") +Distributed.interrupt(Distributed.workers()[end]) +Distributed.rmprocs(Distributed.workers()[end]; waitfor = 0) \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index c0ec742..86c63a6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -103,3 +103,7 @@ end @testset "Username/password login & access control" begin include("username_password_login_accesscontrol.jl") end + +@testset "Encryption" begin + include("client_encryption.jl") +end diff --git a/test/simple_server_client.jl b/test/simple_server_client.jl index 1fab6e7..bfa8473 100644 --- a/test/simple_server_client.jl +++ b/test/simple_server_client.jl @@ -20,7 +20,7 @@ end # Specify client and connect to server after server startup client = JUA_Client() JUA_ClientConfig_setDefault(JUA_ClientConfig(client)) -max_duration = 30.0 # Maximum waiting time for server startup +max_duration = 90.0 # Maximum waiting time for server startup sleep_time = 2.0 # Sleep time in seconds between each connection trial let trial trial = 0 diff --git a/test/username_password_login_accesscontrol.jl b/test/username_password_login_accesscontrol.jl index 59963f7..e28f17f 100644 --- a/test/username_password_login_accesscontrol.jl +++ b/test/username_password_login_accesscontrol.jl @@ -69,6 +69,7 @@ Distributed.@spawnat Distributed.workers()[end] begin config.accessControl.allowAddReference = cb_allowAddReference config.accessControl.allowDeleteNode = cb_allowDeleteNode config.accessControl.allowDeleteReference = cb_allowDeleteReference + config.allowNonePolicyPassword = true #allow logging in with username/password on un-encrypted connections. UA_Server_run(server, Ref(true)) end