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.
Summary
When connecting with
SecurityPolicy=None, gopcua'sOpenSecureChannelRequestalways sends an empty (0-length)
ClientNonce. While this is technicallyallowed 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 silentlycloses the TCP connection (graceful FIN, not RST) upon receiving an empty one
— with no error response at all.
Environment
Steps to reproduce
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,ACKimmediately after receiving the
OpenSecureChannelRequest— noOpenSecureChannelResponse, noERRFerror 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
OpenSecureChannelRequestpayload was theClientNoncefield:ClientNonce= 1 byte (0x00)ClientNonce= empty (0 bytes)This traces back to
uapolicy/policyNone.go:nonceLengthdefaults to the zero value (0), soEncryptionAlgorithm.MakeNonce()returns a 0-length slice, which
secure_channel.go'sopen()sends as-is:I confirmed that setting
nonceLength: 1innewNoneAsymmetric(generating arandom 1-byte nonce even under
None) resolves the issue and the serveraccepts the connection.
Suggested fix
I understand a 0-length nonce under
SecurityPolicy=Noneis spec-compliant(and the library's own tests assert this:
securitypolicy_test.go:113), soI'm not proposing to change the default. Instead, could this be made
configurable — e.g. an
opcua.Optionlikeopcua.ForceNonEmptyNonce()orsimilar — 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.goto setnonceLength: 1and using areplacedirective in go.mod. Works, but obviously not upstream-friendly.Happy to share a redacted pcap capture if useful.