Skip to content

OpenSecureChannelRequest sends empty ClientNonce under SecurityPolicy=None, causing some embedded servers to silently drop the connection #894

Description

@Qwazikpotik

Summary

When connecting with SecurityPolicy=None, gopcua's OpenSecureChannelRequest
always sends an empty (0-length) ClientNonce. While this is technically
allowed by the OPC UA spec, at least one embedded OPC UA server implementation
(found on an Inovance AM-series PLC, ARM-Linux based CODESYS-compatible stack)
requires a non-empty nonce even under SecurityPolicy=None, and silently
closes the TCP connection (graceful FIN, not RST) upon receiving an empty one
— with no error response at all.

Environment

  • gopcua version: v0.9.0
  • Servers tested: Inovance AM402-CPU1608TP and Inovance AM521 (both embedded OPC UA servers on an ARM-Linux based CODESYS-compatible stack) — same behaviour on both
  • SecurityPolicy: None / SecurityMode: None / Anonymous auth

Steps to reproduce

opts := []opcua.Option{
    opcua.SecurityPolicy("None"),
    opcua.SecurityModeString("None"),
    opcua.AuthAnonymous(),
}
c, _ := opcua.NewClient(endpoint, opts...)
err := c.Connect(ctx) // fails with EOF

Observed behaviour

debug: secure_channel.go:1056 uasc 1/1: send *ua.OpenSecureChannelRequest with 132 bytes
debug: secure_channel.go:327 uasc 1: readChunk EOF
debug: secure_channel.go:641 uasc 1: failed to open a new secure channel
A Wireshark capture confirmed the server responds with a graceful FIN,ACK
immediately after receiving the OpenSecureChannelRequest — no
OpenSecureChannelResponse, no ERRF error frame, just a clean TCP close.

Root cause (found via Wireshark comparison against UaExpert)

Comparing a successful connection (UaExpert) against gopcua's failing one,
the only difference in the OpenSecureChannelRequest payload was the
ClientNonce field:

  • UaExpert: ClientNonce = 1 byte (0x00)
  • gopcua: ClientNonce = empty (0 bytes)

This traces back to uapolicy/policyNone.go:

func newNoneAsymmetric(*rsa.PrivateKey, *rsa.PublicKey) (*EncryptionAlgorithm, error) {
	return &EncryptionAlgorithm{
		blockSize:             NoneBlockSize,
		plainttextBlockSize:   NoneBlockSize - NoneMinPadding,
		encrypt:               &None{},
		decrypt:               &None{},
		signature:             &None{},
		verifySignature:       &None{},
		signatureLength:       0,
		remoteSignatureLength: 0,
		// nonceLength is not set here, defaults to 0
	}, nil
}

nonceLength defaults to the zero value (0), so EncryptionAlgorithm.MakeNonce()
returns a 0-length slice, which secure_channel.go's open() sends as-is:

localNonce, err := algo.MakeNonce()
...
req := &ua.OpenSecureChannelRequest{
    ...
    ClientNonce: localNonce,
    ...
}

I confirmed that setting nonceLength: 1 in newNoneAsymmetric (generating a
random 1-byte nonce even under None) resolves the issue and the server
accepts the connection.

Suggested fix

I understand a 0-length nonce under SecurityPolicy=None is spec-compliant
(and the library's own tests assert this: securitypolicy_test.go:113), so
I'm not proposing to change the default. Instead, could this be made
configurable — e.g. an opcua.Option like opcua.ForceNonEmptyNonce() or
similar — so users connecting to non-conformant servers don't need to patch
the library locally?

Happy to submit a PR if a maintainer can confirm the preferred approach.

Workaround currently in use

Locally patching uapolicy/policyNone.go to set nonceLength: 1 and using a
replace directive in go.mod. Works, but obviously not upstream-friendly.

Happy to share a redacted pcap capture if useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions