Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"x509: invalid RDNSequence: invalid attribute value: invalid PrintableString" When connecting to some servers #686

Closed
piperoc opened this issue Aug 26, 2023 · 1 comment

Comments

@piperoc
Copy link

piperoc commented Aug 26, 2023

I'm trying to run a client against the demo server from the NodeOPCUA project.

The test is to connect securely using Basic128Rsa15 and User/Password. I use a slightly modified version for the crypto.go example.

Here's my `main.go` file code:
package main

import (
	"bufio"
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"flag"
	"fmt"
	"log"
	"math/big"
	"net"
	"net/url"
	"os"
	"strings"
	"syscall"
	"time"

	"golang.org/x/term"

	"github.com/gopcua/opcua"
	"github.com/gopcua/opcua/debug"
	"github.com/gopcua/opcua/errors"
	"github.com/gopcua/opcua/ua"
)

var (
	endpoint = flag.String("endpoint", "opc.tcp://localhost:26543", "NodeOPCUA Endpoint URL")
	certfile = flag.String("cert", "cert.pem", "Path to certificate file")
	keyfile  = flag.String("key", "key.pem", "Path to PEM Private Key file")
	gencert  = flag.Bool("gen-cert", false, "Generate a new certificate")
	policy   = flag.String("sec-policy", "Basic128Rsa15", "Security Policy URL or one of None, Basic128Rsa15, Basic256, Basic256Sha256")
	mode     = flag.String("sec-mode", "SignAndEncrypt", "Security Mode: one of None, Sign, SignAndEncrypt")
	auth     = flag.String("auth-mode", "UserName", "Authentication Mode: one of Anonymous, UserName, Certificate")
	appuri   = flag.String("app-uri", "urn:gopcua:client", "Application URI")
	list     = flag.Bool("list", false, "List the policies supported by the endpoint and exit")
	username = flag.String("user", "user1", "Username to use in auth-mode UserName; will prompt for input if omitted")
	password = flag.String("pass", "password1", "Password to use in auth-mode UserName; will prompt for input if omitted")
)

func main() {
	flag.BoolVar(&debug.Enable, "debug", false, "enable debug logging")
	flag.Parse()
	log.SetFlags(0)

	ctx := context.Background()

	// Get a list of the endpoints for our target server
	endpoints, err := opcua.GetEndpoints(ctx, *endpoint)
	if err != nil {
		log.Fatal(err)
	}

	// User asked for just the list of options: print and quit
	if *list {
		printEndpointOptions(endpoints)
		return
	}

	// Get the options to pass into the client based on the flags passed into the executable
	opts := clientOptsFromFlags(endpoints)

	// Create a Client with the selected options
	c, err := opcua.NewClient(*endpoint, opts...)
	if err != nil {
		log.Fatal(err)
	}
	if err := c.Connect(ctx); err != nil {
		log.Fatal(err)
	}
	defer c.Close(ctx)

	// Use our connection (read the server's time)
	v, err := c.Node(ua.NewNumericNodeID(0, 2258)).Value(ctx)
	if err != nil {
		log.Fatal(err)
	}
	if v != nil {
		fmt.Printf("Server's Time | Conn 1 %s | ", v.Value())
	} else {
		log.Print("v == nil")
	}

	// Detach our session and try re-establish it on a different secure channel
	s, err := c.DetachSession(ctx)
	if err != nil {
		log.Fatalf("Error detaching session: %s", err)
	}

	d, err := opcua.NewClient(*endpoint, opts...)
	if err != nil {
		log.Fatal(err)
	}

	// Create a channel only and do not activate it automatically
	d.Dial(ctx)
	defer d.Close(ctx)

	// Activate the previous session on the new channel
	err = d.ActivateSession(ctx, s)
	if err != nil {
		log.Fatalf("Error reactivating session: %s", err)
	}

	// Read the time again to prove our session is still OK
	v, err = d.Node(ua.NewNumericNodeID(0, 2258)).Value(ctx)
	if err != nil {
		log.Fatal(err)
	}
	if v != nil {
		fmt.Printf("Conn 2: %s\n", v.Value())
	} else {
		log.Print("v == nil")
	}
}

func clientOptsFromFlags(endpoints []*ua.EndpointDescription) []opcua.Option {
	opts := []opcua.Option{}

	// ApplicationURI is automatically read from the cert so is not required if a cert if provided
	if *certfile == "" && !*gencert {
		opts = append(opts, opcua.ApplicationURI(*appuri))
	}

	var cert []byte
	if *gencert || (*certfile != "" && *keyfile != "") {
		if *gencert {
			// I changed this because the ua.test code did not work
			// against the Prosys server for me
			hostname, _ := os.Hostname()
			var appuri = "urn:" + hostname + ":gotest:client"
			_, _ = create_x509cert_privateKey("ACME",
				"US",
				"Nashville",
				"100 Main Street",
				"01234", appuri)
		}
		debug.Printf("Loading cert/key from %s/%s", *certfile, *keyfile)
		c, err := tls.LoadX509KeyPair(*certfile, *keyfile)
		if err != nil {
			log.Printf("Failed to load certificate: %s", err)
		} else {
			pk, ok := c.PrivateKey.(*rsa.PrivateKey)
			if !ok {
				log.Fatalf("Invalid private key")
			}
			cert = c.Certificate[0]
			opts = append(opts, opcua.PrivateKey(pk), opcua.Certificate(cert))
		}
	}

	var secPolicy string
	switch {
	case *policy == "auto":
		// set it later
	case strings.HasPrefix(*policy, ua.SecurityPolicyURIPrefix):
		secPolicy = *policy
		*policy = ""
	case *policy == "None" || *policy == "Basic128Rsa15" || *policy == "Basic256" || *policy == "Basic256Sha256" || *policy == "Aes128_Sha256_RsaOaep" || *policy == "Aes256_Sha256_RsaPss":
		secPolicy = ua.SecurityPolicyURIPrefix + *policy
		*policy = ""
	default:
		log.Fatalf("Invalid security policy: %s", *policy)
	}

	// Select the most appropriate authentication mode from server capabilities and user input
	authMode, authOption := authFromFlags(cert)
	opts = append(opts, authOption)

	var secMode ua.MessageSecurityMode
	switch strings.ToLower(*mode) {
	case "auto":
	case "none":
		secMode = ua.MessageSecurityModeNone
		*mode = ""
	case "sign":
		secMode = ua.MessageSecurityModeSign
		*mode = ""
	case "signandencrypt":
		secMode = ua.MessageSecurityModeSignAndEncrypt
		*mode = ""
	default:
		log.Fatalf("Invalid security mode: %s", *mode)
	}

	// Allow input of only one of sec-mode,sec-policy when choosing 'None'
	if secMode == ua.MessageSecurityModeNone || secPolicy == ua.SecurityPolicyURINone {
		secMode = ua.MessageSecurityModeNone
		secPolicy = ua.SecurityPolicyURINone
	}

	// Find the best endpoint based on our input and server recommendation (highest SecurityMode+SecurityLevel)
	var serverEndpoint *ua.EndpointDescription
	switch {
	case *mode == "auto" && *policy == "auto": // No user selection, choose best
		for _, e := range endpoints {
			if serverEndpoint == nil || (e.SecurityMode >= serverEndpoint.SecurityMode && e.SecurityLevel >= serverEndpoint.SecurityLevel) {
				serverEndpoint = e
			}
		}

	case *mode != "auto" && *policy == "auto": // User only cares about mode, select highest securitylevel with that mode
		for _, e := range endpoints {
			if e.SecurityMode == secMode && (serverEndpoint == nil || e.SecurityLevel >= serverEndpoint.SecurityLevel) {
				serverEndpoint = e
			}
		}

	case *mode == "auto" && *policy != "auto": // User only cares about policy, select highest securitylevel with that policy
		for _, e := range endpoints {
			if e.SecurityPolicyURI == secPolicy && (serverEndpoint == nil || e.SecurityLevel >= serverEndpoint.SecurityLevel) {
				serverEndpoint = e
			}
		}

	default: // User cares about both
		fmt.Println("secMode: ", secMode, "secPolicy:", secPolicy)
		for _, e := range endpoints {
			if e.SecurityPolicyURI == secPolicy && e.SecurityMode == secMode && (serverEndpoint == nil || e.SecurityLevel >= serverEndpoint.SecurityLevel) {
				serverEndpoint = e
			}
		}
	}

	if serverEndpoint == nil { // Didn't find an endpoint with matching policy and mode.
		log.Printf("unable to find suitable server endpoint with selected sec-policy and sec-mode")
		printEndpointOptions(endpoints)
		log.Fatalf("quitting")
	}

	secPolicy = serverEndpoint.SecurityPolicyURI
	secMode = serverEndpoint.SecurityMode

	// Check that the selected endpoint is a valid combo
	err := validateEndpointConfig(endpoints, secPolicy, secMode, authMode)
	if err != nil {
		log.Fatalf("error validating input: %s", err)
	}

	opts = append(opts, opcua.SecurityFromEndpoint(serverEndpoint, authMode))

	log.Printf("Using config:\nEndpoint: %s\nSecurity mode: %s, %s\nAuth mode : %s\n", serverEndpoint.EndpointURL, serverEndpoint.SecurityPolicyURI, serverEndpoint.SecurityMode, authMode)
	return opts
}

func authFromFlags(cert []byte) (ua.UserTokenType, opcua.Option) {
	var err error

	var authMode ua.UserTokenType
	var authOption opcua.Option
	switch strings.ToLower(*auth) {
	case "anonymous":
		authMode = ua.UserTokenTypeAnonymous
		authOption = opcua.AuthAnonymous()

	case "username":
		authMode = ua.UserTokenTypeUserName

		if *username == "" {
			fmt.Print("Enter username: ")
			*username, err = bufio.NewReader(os.Stdin).ReadString('\n')
			*username = strings.TrimSuffix(*username, "\n")
			if err != nil {
				log.Fatalf("error reading username input: %s", err)
			}
		}

		passPrompt := true
		flag.Visit(func(f *flag.Flag) {
			if f.Name == "pass" {
				passPrompt = false
			}
		})

		if passPrompt {
			fmt.Print("Enter password: ")
			passInput, err := term.ReadPassword(int(syscall.Stdin))
			if err != nil {
				log.Fatalf("Error reading password: %s", err)
			}
			*password = string(passInput)
			fmt.Print("\n")
		}
		authOption = opcua.AuthUsername(*username, *password)

	case "certificate":
		authMode = ua.UserTokenTypeCertificate
		authOption = opcua.AuthCertificate(cert)

	case "issuedtoken":
		// todo: this is unsupported, fail here or fail in the opcua package?
		authMode = ua.UserTokenTypeIssuedToken
		authOption = opcua.AuthIssuedToken([]byte(nil))

	default:
		log.Printf("unknown auth-mode, defaulting to Anonymous")
		authMode = ua.UserTokenTypeAnonymous
		authOption = opcua.AuthAnonymous()

	}

	return authMode, authOption
}

func validateEndpointConfig(endpoints []*ua.EndpointDescription, secPolicy string, secMode ua.MessageSecurityMode, authMode ua.UserTokenType) error {
	for _, e := range endpoints {
		if e.SecurityMode == secMode && e.SecurityPolicyURI == secPolicy {
			for _, t := range e.UserIdentityTokens {
				if t.TokenType == authMode {
					return nil
				}
			}
		}
	}

	err := errors.Errorf("server does not support an endpoint with security : %s , %s", secPolicy, secMode)
	printEndpointOptions(endpoints)
	return err
}

func printEndpointOptions(endpoints []*ua.EndpointDescription) {
	log.Print("Valid options for the endpoint are:")
	log.Print("         sec-policy    |    sec-mode     |      auth-modes\n")
	log.Print("-----------------------|-----------------|---------------------------\n")
	for _, e := range endpoints {
		p := strings.TrimPrefix(e.SecurityPolicyURI, "http://opcfoundation.org/UA/SecurityPolicy#")
		m := strings.TrimPrefix(e.SecurityMode.String(), "MessageSecurityMode")
		var tt []string
		for _, t := range e.UserIdentityTokens {
			tok := strings.TrimPrefix(t.TokenType.String(), "UserTokenType")

			// Just show one entry if a server has multiple varieties of one TokenType (eg. different algorithms)
			dup := false
			for _, v := range tt {
				if tok == v {
					dup = true
					break
				}
			}
			if !dup {
				tt = append(tt, tok)
			}
		}
		log.Printf("%22s | %-15s | (%s)", p, m, strings.Join(tt, ","))
	}
}

func create_x509cert_privateKey(company string, country string, locality string,
	streetAddress string, postalCode string, applicationURI string) (string, string) {

	var serialNumber, _ = new(big.Int).SetString("759a625589c776e46640684d760180b0", 16)

	ca := &x509.Certificate{
		// use SHA256 for the case SignAndEncrypt
		SignatureAlgorithm: x509.SHA256WithRSAPSS,
		SerialNumber:       serialNumber,
		Subject: pkix.Name{
			Organization:  []string{company},
			Country:       []string{country},
			Province:      []string{""},
			Locality:      []string{locality},
			StreetAddress: []string{streetAddress},
			PostalCode:    []string{postalCode},
		},
		IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback, net.IPv4(192, 168, 0, 90)},
		NotBefore:   time.Now(),
		// 6 months
		NotAfter:              time.Now().AddDate(0, 6, 0),
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
		KeyUsage:              x509.KeyUsageContentCommitment | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment | x509.KeyUsageCertSign,
		BasicConstraintsValid: true,
	}

	hosts := strings.Split(applicationURI, ",")
	for _, h := range hosts {
		if ip := net.ParseIP(h); ip != nil {
			(*ca).IPAddresses = append((*ca).IPAddresses, ip)
		} else {
			(*ca).DNSNames = append((*ca).DNSNames, h)
		}
		if uri, err := url.Parse(h); err == nil {
			(*ca).URIs = append((*ca).URIs, uri)
		}
	}
	// create public and private key via rsa
	caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		panic(err)
	}
	// self sign the certificate
	derBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
	if err != nil {
		panic(err)
	}

	// save the certificate to a file
	certOut, err := os.Create("cert.pem")
	if err != nil {
		panic("Failed to create cert.pem file" + err.Error())
	}
	// pem encode the file
	if err := pem.Encode(certOut, &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: derBytes,
	}); err != nil {
		panic("Failed to write data to cert.pem file " + err.Error())
	}

	if err := certOut.Close(); err != nil {
		panic("error closing cert.pem file " + err.Error())
	}
	// save the private key to a file
	keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		panic(err.Error() + "\n failed to create key.pem file")
	}
	// pem encode the file
	if err := pem.Encode(keyOut, &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
	}); err != nil {
		panic(err.Error() + "\n failed to write data to key.pem")
	}

	if err := keyOut.Close(); err != nil {
		panic(err.Error() + "\n error closing key.pem file")
	}

	return "cert.pem", "key.pem"
}

here's my current go.mod file:

module TestCrypto

go 1.21

require (
	github.com/gopcua/opcua v0.5.1
	golang.org/x/term v0.11.0
)

require (
	github.com/pkg/errors v0.9.1 // indirect
	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
	golang.org/x/sys v0.11.0 // indirect
)

When I run this client I get the following:

Enter password: 
secMode:  MessageSecurityModeSignAndEncrypt secPolicy: http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15
Using config:
Endpoint: opc.tcp://BLACKBEARD:26543
Security mode: http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15, MessageSecurityModeSignAndEncrypt     
Auth mode : UserTokenTypeUserName
x509: invalid RDNSequence: invalid attribute value: invalid PrintableString

Debugger finished with the exit code 0

If I run this client against the Prosys simulator and it works fine.
If I run this against the Milo server (opc.tcp://milo.digitalpetri.com:62541/milo) everything works.
If I run this against the Unified UAAnsiC demo server and the UACPP demo server it keeps rejecting my cert but no other errors.

I used UAExpert and the Prosys OPC UA Browser against that same server. No visible errors.

The only relevant info I found about that error is in this issue on the golang repo but I'm not skilled enough to put it in context.

It looks like the error happens when the client gets the server's cert and tries parsing it.
I appreciate any help. I will keep testing with other servers/clients and SDKs to see if I get more diagnostics. Thanks

@piperoc
Copy link
Author

piperoc commented Aug 27, 2023

I have found the issue, which is a bug on the NodeOPC side.
It has been detected and documented here node-opcua/node-opcua#1289.

@piperoc piperoc closed this as completed Aug 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant