Skip to content

Commit

Permalink
requested comments
Browse files Browse the repository at this point in the history
  • Loading branch information
led0nk committed Jan 31, 2025
1 parent a277879 commit ddbc88a
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 50 deletions.
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ input:
insecure: false | true # DEPRECATED, see below
securityMode: None | Sign | SignAndEncrypt # optional (default: unset)
securityPolicy: None | Basic256Sha256 # optional (default: unset)
fingerprint: 'sha1-fingerprint-of-cert' # optional (default: unset)
serverCertificateFingerprint: 'sha3-fingerprint-of-cert' # optional (default: unset)
subscribeEnabled: false | true # optional (default: false)
useHeartbeat: false | true # optional (default: false)
pollRate: 1000 # optional (default: 1000) The rate in milliseconds at which to poll the OPC UA server when not using subscriptions
Expand Down Expand Up @@ -188,17 +188,24 @@ input:
securityPolicy: Basic256Sha256
```
##### Fingerprint
###### Server Certificate Fingerprint
You can provide the fingerprint of your OPC-UA-Servers certificate, which should be a sha1-hash.
This option ensures the 'client trusts server'-functionality.
**Key**: `serverCertificateFingerprint`
**Description**:
Use this field to explicitly trust the server’s certificate. When specified, only endpoints matching this fingerprint will be accepted. This ensures the client connects to the correct server and helps prevent man-in-the-middle attacks.

> **Important**
> - **If you omit `serverCertificateFingerprint`,** the client will still attempt to connect.
> - Initially, it will log an **info message** to remind you to set `serverCertificateFingerprint`.
> - Future releases may escalate this to a **warning** that blocks deployment in certain environments.
> - If your server's certificate changes (e.g. renewal, new server) update the `serverCertificateFingerprint` accordingly. Otherwise the connection will be rejected, signaling a potential security issue or misconfiguration.

```yaml
input:
opcua:
endpoint: 'opc.tcp://localhost:46010'
nodeIDs: ['ns=2;s=IoTSensors']
fingerprint: 'sha1-fingerprint-of-cert'
serverCertificateFingerprint: 'sha3-fingerprint-of-cert'
```

##### Insecure Mode
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ require (
go.uber.org/zap v1.27.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/api v0.188.0 // indirect
Expand Down Expand Up @@ -370,11 +370,11 @@ require (
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/crypto v0.32.0
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1228,8 +1228,8 @@ golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand Down Expand Up @@ -1385,8 +1385,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand All @@ -1395,8 +1395,8 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
30 changes: 20 additions & 10 deletions opcua_plugin/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"strings"
"time"

"crypto/sha3"
"golang.org/x/crypto/sha3"

"github.com/gopcua/opcua"
"github.com/gopcua/opcua/ua"
Expand Down Expand Up @@ -190,13 +190,19 @@ func (g *OPCUAInput) FetchAllEndpoints(ctx context.Context) ([]*ua.EndpointDescr
//
// This function will check on each element of the fetched endpoints, if the fingerprint is correct.
// Therefore we can check if the server is "trusted" by the client. It should check on the server certificate
// of each endpoint if the fingerprint (sha1) matches with the fingerprint from input.yaml.
// of each endpoint if the fingerprint (sha3) matches with the fingerprint from input.yaml.
//
// **Why This Function is Needed:**
// - To ensure the client connects to the correct server
func (g *OPCUAInput) filterEndpointsByFingerprint(endpoints []*ua.EndpointDescription) ([]*ua.EndpointDescription, error) {
if g.Fingerprint == "" {
if g.ServerCertificateFingerprint == "" {
// When there is no fingerprint set we will just trust the servers endpoints.
// This may be escalated to a warning in future releases.
g.Log.Infof(
"No 'serverCertificateFingerprint' was provided. " +
"We strongly recommend specifying 'serverCertificateFingerprint=xxxx' to verify the server's identity " +
"and avoid potential security risks. Future releases may escalate this to a warning that prevents deployment.",
)
return endpoints, nil
}

Expand All @@ -206,7 +212,7 @@ func (g *OPCUAInput) filterEndpointsByFingerprint(endpoints []*ua.EndpointDescri
// if the endpoint doesn't provide a server certificate, we skip it
// we could potentially return here since this should be the same for all endpoints
if len(ep.ServerCertificate) == 0 {
g.Log.Warnf("Endpoint %s doesn't provide any server certificate. Skipping...", ep.EndpointURL)
g.Log.Infof("Endpoint %s doesn't provide any server certificate. Skipping...", ep.EndpointURL)
continue
}

Expand All @@ -230,15 +236,19 @@ func (g *OPCUAInput) filterEndpointsByFingerprint(endpoints []*ua.EndpointDescri
continue
}

// calculating the checksum of the certificate (sha1 is needed here)
// calculating the checksum of the certificate (sha3 is needed here)
// and convert the array into a slice for encoding
// shaFingerprint := sha1.Sum(cert.Raw)
shaFingerprint := sha3.Sum512(cert.Raw)
epFingerprint := hex.EncodeToString(shaFingerprint[:])

// to have some information on the mismatched fingerprints
if epFingerprint != g.Fingerprint {
g.Log.Warnf("Fingerprint of endpoint %s doesn't match, expected: '%s', got: '%s'. Skipping...", ep.EndpointURL, g.Fingerprint, epFingerprint)
// This is not for debugging purpose, but for the user to get some
// information about the mismatch of the endpoints certificate fingerprint.
if epFingerprint != g.ServerCertificateFingerprint {
g.Log.Infof("Fingerprint of endpoint %s doesn't match, expected: '%s', got: '%s'. "+
"Either the server’s certificate was intentionally updated, or you are connecting to the wrong server. "+
"If intentional, please set the new 'serverCertificateFingerprint' in your configuration. "+
"Otherwise, double-check your security settings.",
ep.EndpointURL, g.ServerCertificateFingerprint, epFingerprint)
continue
}

Expand All @@ -247,7 +257,7 @@ func (g *OPCUAInput) filterEndpointsByFingerprint(endpoints []*ua.EndpointDescri
// return an error if there are no endpoints with matching fingerprints - to see failure
}
if len(filteredEP) == 0 {
return nil, fmt.Errorf("no endpoints with matching certificate fingerprint found: '%s' ", g.Fingerprint)
return nil, fmt.Errorf("No endpoints with matching certificate fingerprint '%s' found. ", g.ServerCertificateFingerprint)
}
return filteredEP, nil
}
Expand Down
28 changes: 14 additions & 14 deletions opcua_plugin/opcua.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ var OPCUAConfigSpec = service.NewConfigSpec().
Field(service.NewIntField("pollRate").Description("The rate in milliseconds at which to poll the OPC UA server when not using subscriptions. Defaults to 1000ms (1 second).").Default(DefaultPollRate)).
Field(service.NewBoolField("autoReconnect").Description("Set to true to automatically reconnect to the OPC UA server when the connection is lost. Defaults to 'false'").Default(false)).
Field(service.NewIntField("reconnectIntervalInSeconds").Description("The interval in seconds at which to reconnect to the OPC UA server when the connection is lost. This is only used if `autoReconnect` is set to true. Defaults to 5 seconds.").Default(5)).
Field(service.NewStringField("fingerprint").Description("Set this to the fingerprint of your OPC-UA-Servers certificate, if you're willing to connect via encryption. This checks if the client can trust the server.").Default(""))
Field(service.NewStringField("serverCertificateFingerprint").Description("Set this to the fingerprint (sha3-hash) of your OPC-UA-Servers certificate, if you're willing to connect via encryption. This checks if the client can trust the server.").Default(""))

func ParseNodeIDs(incomingNodes []string) []*ua.NodeID {

Expand Down Expand Up @@ -138,7 +138,7 @@ func newOPCUAInput(conf *service.ParsedConfig, mgr *service.Resources) (service.
return nil, err
}

fingerprint, err := conf.FieldString("fingerprint")
serverCertificateFingerprint, err := conf.FieldString("serverCertificateFingerprint")
if err != nil {
return nil, err
}
Expand All @@ -158,7 +158,7 @@ func newOPCUAInput(conf *service.ParsedConfig, mgr *service.Resources) (service.
Log: mgr.Logger(),
SecurityMode: securityMode,
SecurityPolicy: securityPolicy,
Fingerprint: fingerprint, // fingerprint is the sha1 uid for the servers certificate
ServerCertificateFingerprint: serverCertificateFingerprint, // ServerCertificateFingerprint is the sha3 hash of the servers certificate
Insecure: insecure,
SubscribeEnabled: subscribeEnabled,
SessionTimeout: sessionTimeout,
Expand Down Expand Up @@ -192,17 +192,17 @@ func init() {
}

type OPCUAInput struct {
Endpoint string
Username string
Password string
NodeIDs []*ua.NodeID
NodeList []NodeDef
SecurityMode string
SecurityPolicy string
Fingerprint string
Insecure bool
Client *opcua.Client
Log *service.Logger
Endpoint string
Username string
Password string
NodeIDs []*ua.NodeID
NodeList []NodeDef
SecurityMode string
SecurityPolicy string
ServerCertificateFingerprint string
Insecure bool
Client *opcua.Client
Log *service.Logger
// this is required for subscription
SubscribeEnabled bool
SubNotifyChan chan *opcua.PublishNotificationData
Expand Down
28 changes: 16 additions & 12 deletions opcua_plugin/opcua_plc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,22 +165,24 @@ var _ = Describe("Test Against Siemens S7", Serial, func() {
}

input = &OPCUAInput{
Endpoint: endpoint,
NodeIDs: nil,
Fingerprint: fingerprint, // correct certificate fingerprint
Endpoint: endpoint,
NodeIDs: nil,
ServerCertificateFingerprint: fingerprint, // correct certificate fingerprint
}

// Attempt to connect with matching fingerprints
err := input.Connect(ctx)
Expect(err).NotTo(HaveOccurred())
})

It("should fail due to fingerprint-mismatch", func() {
input = &OPCUAInput{
Endpoint: endpoint,
NodeIDs: nil,
Fingerprint: "test123", // incorrect certificate fingerprint
Endpoint: endpoint,
NodeIDs: nil,
ServerCertificateFingerprint: "test123", // incorrect certificate fingerprint
}

// Attempt to connect and fail due to fingerprint mismatch
err := input.Connect(ctx)
Expect(err).To(HaveOccurred())
})
Expand Down Expand Up @@ -293,22 +295,24 @@ var _ = Describe("Test Against WAGO PLC", Serial, func() {
}

input = &OPCUAInput{
Endpoint: endpoint,
NodeIDs: nil,
Fingerprint: fingerprint, // correct certificate fingerprint
Endpoint: endpoint,
NodeIDs: nil,
ServerCertificateFingerprint: fingerprint, // correct certificate fingerprint
}

// Attempt to connect with matching fingerprints
err := input.Connect(ctx)
Expect(err).NotTo(HaveOccurred())
})

It("should fail due to fingerprint-mismatch", func() {
input = &OPCUAInput{
Endpoint: endpoint,
NodeIDs: nil,
Fingerprint: "test123", // incorrect certificate fingerprint
Endpoint: endpoint,
NodeIDs: nil,
ServerCertificateFingerprint: "test123", // incorrect certificate fingerprint
}

// Attempt to connect and fail due to fingerprint mismatch
err := input.Connect(ctx)
Expect(err).To(HaveOccurred())
})
Expand Down

0 comments on commit ddbc88a

Please sign in to comment.