Files
modbus/server_tls_test.go
2025-11-07 13:53:18 +08:00

573 lines
17 KiB
Go

package modbus
import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"testing"
"time"
)
const (
clientCertWithRoleOID string = `
-----BEGIN CERTIFICATE-----
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
hG79y2z0UfKJstN+
-----END CERTIFICATE-----
`
clientKeyWithRoleOID string = `
-----BEGIN PRIVATE KEY-----
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
isPLG4c6aPGxSbHirNfl6tBSngDy+A==
-----END PRIVATE KEY-----
`
)
// TestTLSServer tests the TLS layer of the modbus server.
func TestTLSServer(t *testing.T) {
var err error
var server *ModbusServer
var serverKeyPair tls.Certificate
var client1KeyPair tls.Certificate
var client2KeyPair tls.Certificate
var clientCp *x509.CertPool
var serverCp *x509.CertPool
var th *tlsTestHandler
var c1 *ModbusClient
var c2 *ModbusClient
var regs []uint16
var coils []bool
th = &tlsTestHandler{}
// load server keypair (from client_tls_test.go)
serverKeyPair, err = tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
if err != nil {
t.Errorf("failed to load test server key pair: %v", err)
return
}
// load the first client keypair (from client_tls_test.go)
// this client cert doesn't have any Modbus Role extension
client1KeyPair, err = tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
if err != nil {
t.Errorf("failed to load test client key pair: %v", err)
return
}
// load the second client keypair (defined above)
// this client cert has an "operator2" Modbus Role extension
client2KeyPair, err = tls.X509KeyPair(
[]byte(clientCertWithRoleOID), []byte(clientKeyWithRoleOID))
if err != nil {
t.Errorf("failed to load test client key pair: %v", err)
return
}
// load the server cert into the client CA cert pool to get the server cert
// accepted by clients
clientCp = x509.NewCertPool()
if !clientCp.AppendCertsFromPEM([]byte(serverCert)) {
t.Errorf("failed to load test server cert into cert pool")
}
// start with an empty server cert pool initially to reject the client
// certificate
serverCp = x509.NewCertPool()
server, err = NewServer(&ServerConfiguration{
URL: "tcp+tls://localhost:5802",
MaxClients: 2,
TLSServerCert: &serverKeyPair,
TLSClientCAs: serverCp,
}, th)
if err != nil {
t.Errorf("failed to create server: %v", err)
}
err = server.Start()
if err != nil {
t.Errorf("failed to start server: %v", err)
}
// create 2 modbus clients
c1, err = NewClient(&ClientConfiguration{
URL: "tcp+tls://localhost:5802",
TLSClientCert: &client1KeyPair,
TLSRootCAs: clientCp,
})
if err != nil {
t.Errorf("failed to create client: %v", err)
}
c2, err = NewClient(&ClientConfiguration{
URL: "tcp+tls://localhost:5802",
TLSClientCert: &client2KeyPair,
TLSRootCAs: clientCp,
})
if err != nil {
t.Errorf("failed to create client: %v", err)
}
// attempt to connect and use the first client. since its cert
// is not trusted by the server, a TLS error should occur on the first
// request.
err = c1.Open()
if err != nil {
t.Errorf("c1.Open() should have succeeded")
}
coils, err = c1.ReadCoils(0, 5)
if err == nil {
t.Error("c1.ReadCoils() should have failed")
}
c1.Close()
// now place both client certs in the server's authorized client list
// to get them past the TLS client cert validation procedure
if !serverCp.AppendCertsFromPEM([]byte(clientCert)) {
t.Errorf("failed to load client#1 cert into cert pool")
}
if !serverCp.AppendCertsFromPEM([]byte(clientCertWithRoleOID)) {
t.Errorf("failed to load client#2 cert into cert pool")
}
// connect both clients: should succeed
err = c1.Open()
if err != nil {
t.Error("c1.Open() should have succeeded")
}
err = c2.Open()
if err != nil {
t.Error("c2.Open() should have succeeded")
}
// client #2 (with 'operator2' role) should have read/write access to coils while
// client #1 (without role) should only be able to read.
err = c1.WriteCoil(0, true)
if err != ErrIllegalFunction {
t.Errorf("c1.WriteCoil() should have failed with %v, got: %v",
ErrIllegalFunction, err)
}
coils, err = c1.ReadCoils(0, 5)
if err != nil {
t.Errorf("c1.ReadCoils() should have succeeded, got: %v", err)
}
if coils[0] {
t.Errorf("coils[0] should have been false")
}
err = c2.WriteCoil(0, true)
if err != nil {
t.Errorf("c2.WriteCoil() should have succeeded, got: %v", err)
}
coils, err = c2.ReadCoils(0, 5)
if err != nil {
t.Errorf("c2.ReadCoils() should have succeeded, got: %v", err)
}
if !coils[0] {
t.Errorf("coils[0] should have been true")
}
coils, err = c1.ReadCoils(0, 5)
if err != nil {
t.Errorf("c1.ReadCoils() should have succeeded, got: %v", err)
}
if !coils[0] {
t.Errorf("coils[0] should have been true")
}
// client #1 should only be allowed access to holding registers of unit id #1
// while client#2 should be allowed access to holding registers of unit ids #1 and #4
c1.SetUnitId(1)
err = c1.WriteRegister(2, 100)
if err != nil {
t.Errorf("c1.WriteRegister() should have succeeded, got: %v", err)
}
c1.SetUnitId(4)
err = c1.WriteRegister(2, 200)
if err != ErrIllegalFunction {
t.Errorf("c1.WriteRegister() should have failed with %v, got: %v",
ErrIllegalFunction, err)
}
c2.SetUnitId(1)
regs, err = c2.ReadRegisters(1, 2, HOLDING_REGISTER)
if err != nil {
t.Errorf("c2.ReadRegisters() should have succeeded, got: %v", err)
}
if regs[0] != 0 || regs[1] != 100 {
t.Errorf("unexpected register values: %v", regs)
}
c2.SetUnitId(4)
err = c2.WriteRegister(2, 200)
if err != nil {
t.Errorf("c2.WriteRegister() should have succeeded, got: %v", err)
}
regs, err = c2.ReadRegisters(1, 2, HOLDING_REGISTER)
if err != nil {
t.Errorf("c2.ReadRegisters() should have succeeded, got: %v", err)
}
if regs[0] != 0 || regs[1] != 200 {
t.Errorf("unexpected register values: %v", regs)
}
// close the server and all client connections
server.Stop()
// make sure all underlying TCP client connections have been freed
time.Sleep(10 * time.Millisecond)
server.lock.Lock()
if len(server.tcpClients) != 0 {
t.Errorf("expected 0 client connections, saw: %v", len(server.tcpClients))
}
server.lock.Unlock()
// cleanup
c1.Close()
c2.Close()
return
}
type tlsTestHandler struct {
coils [10]bool
holdingId1 [10]uint16
holdingId4 [10]uint16
}
func (th *tlsTestHandler) HandleCoils(req *CoilsRequest) (res []bool, err error) {
// coils access is allowed to any client with a valid cert, but
// the "operator2" role is required to write
if req.IsWrite && req.ClientRole != "operator2" {
err = ErrIllegalFunction
return
}
if req.Addr + req.Quantity > uint16(len(th.coils)) {
err = ErrIllegalDataAddress
return
}
for i := 0; i < int(req.Quantity); i++ {
if req.IsWrite {
th.coils[int(req.Addr) + i] = req.Args[i]
}
res = append(res, th.coils[int(req.Addr) + i])
}
return
}
func (th *tlsTestHandler) HandleDiscreteInputs(req *DiscreteInputsRequest) (res []bool, err error) {
// there are no digital inputs on this device
err = ErrIllegalDataAddress
return
}
func (th *tlsTestHandler) HandleHoldingRegisters(req *HoldingRegistersRequest) (res []uint16, err error) {
// gate unit id #4 behind the "operator2" role while access to unit id #1
// is allowed to any valid cert
if req.UnitId == 0x04 {
if req.ClientRole != "operator2" {
err = ErrIllegalFunction
return
}
if req.Addr + req.Quantity > uint16(len(th.holdingId4)) {
err = ErrIllegalDataAddress
return
}
for i := 0; i < int(req.Quantity); i++ {
if req.IsWrite {
th.holdingId4[int(req.Addr) + i] = req.Args[i]
}
res = append(res, th.holdingId4[int(req.Addr) + i])
}
} else if req.UnitId == 0x01 {
if req.Addr + req.Quantity > uint16(len(th.holdingId1)) {
err = ErrIllegalDataAddress
return
}
for i := 0; i < int(req.Quantity); i++ {
if req.IsWrite {
th.holdingId1[int(req.Addr) + i] = req.Args[i]
}
res = append(res, th.holdingId1[int(req.Addr) + i])
}
} else {
err = ErrIllegalFunction
return
}
return
}
func (th *tlsTestHandler) HandleInputRegisters(req *InputRegistersRequest) (res []uint16, err error) {
// there are no inputs registers on this device
err = ErrIllegalDataAddress
return
}
func TestServerExtractRole(t *testing.T) {
var ms *ModbusServer
var pemBlock *pem.Block
var x509Cert *x509.Certificate
var err error
var role string
ms = &ModbusServer{
logger: newLogger("test-server-role-extraction", nil),
}
// load a client cert without role OID
pemBlock, _ = pem.Decode([]byte(clientCert))
if err != nil {
t.Errorf("failed to decode client cert: %v", err)
return
}
x509Cert, err = x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
t.Errorf("failed to parse client cert: %v", err)
return
}
// calling extractRole on a cert without role extension should return an
// empty string (see R-23 of the MBAPS spec)
role = ms.extractRole(x509Cert)
if role != "" {
t.Errorf("role should have been empty, got: '%s'", role)
}
// load a certificate with a single role extension of "operator2"
pemBlock, _ = pem.Decode([]byte(clientCertWithRoleOID))
if err != nil {
t.Errorf("failed to decode client cert: %v", err)
return
}
x509Cert, err = x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
t.Errorf("failed to parse client cert: %v", err)
return
}
role = ms.extractRole(x509Cert)
if role != "operator2" {
t.Errorf("role should have been 'operator2', got: '%s'", role)
}
// build a certificate with multiple Modbus Role extensions: they should
// all be rejected
x509Cert = &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: modbusRoleOID,
Value: []byte{
0x0c, 0x04, 0x66, 0x77, 0x67, 0x78,
// ^ ASN1:UTF8String
// ^ length
// ^ 4-byte string 'fwgx'
},
},
{
Id: modbusRoleOID,
Value: []byte{
0x0c, 0x02, 0x66, 0x67,
// ^ ASN1:UTF8String
// ^ length
// ^ 2-byte string 'fwwf'
},
},
},
}
role = ms.extractRole(x509Cert)
if role != "" {
t.Errorf("role should have been empty, got: '%s'", role)
}
// build a certificate with a single Modbus Role extension of the wrong
// type: the role should be rejected
x509Cert = &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: modbusRoleOID,
Value: []byte{
0x13, 0x04, 0x66, 0x77, 0x67, 0x78,
// ^ ASN1:PrintableString
// ^ length
// ^ 4-byte string 'fwgx'
},
},
},
}
role = ms.extractRole(x509Cert)
if role != "" {
t.Errorf("role should have been empty, got: '%s'", role)
}
// build a certificate with a single, short Modbus Role extension: the role
// should be rejected
x509Cert = &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: modbusRoleOID,
Value: []byte{
0x0c,
// ^ ASN1:UTF8String
// ^ missing length + payload bytes
},
},
},
}
role = ms.extractRole(x509Cert)
if role != "" {
t.Errorf("role should have been empty, got: '%s'", role)
}
// build a certificate with one bad Modbus Role extension (short) and one
// valid: they should both be rejected
x509Cert = &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: modbusRoleOID,
Value: []byte{
0x0c,
// ^ ASN1:UTF8String
// ^ missing length + payload bytes
},
},
{
Id: modbusRoleOID,
Value: []byte{
0x0c, 0x02, 0x66, 0x67,
// ^ ASN1:UTF8String
// ^ length
// ^ 2-byte string 'fwwf'
},
},
},
}
role = ms.extractRole(x509Cert)
if role != "" {
t.Errorf("role should have been empty, got: '%s'", role)
}
// build a certificate with a single, valid Modbus Role extension: it should be
// accepted
x509Cert = &x509.Certificate{
Extensions: []pkix.Extension{
{
Id: modbusRoleOID,
Value: []byte{
0x0c, 0x04, 0x66, 0x77, 0x67, 0x78,
// ^ ASN1:UTF8String
// ^ length
// ^ 4-byte string 'fwgx'
},
},
},
}
role = ms.extractRole(x509Cert)
if role != "fwgx" {
t.Errorf("role should have been 'fwgx', got: '%s'", role)
}
return
}