diff --git a/README.md b/README.md index 0498fd3..26530a3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/go.mod b/go.mod index 880b179..ae30a04 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 7c53a42..535ea0f 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/opcua_plugin/connect.go b/opcua_plugin/connect.go index c59be4a..9f1040e 100644 --- a/opcua_plugin/connect.go +++ b/opcua_plugin/connect.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "crypto/sha3" + "golang.org/x/crypto/sha3" "github.com/gopcua/opcua" "github.com/gopcua/opcua/ua" @@ -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 } @@ -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 } @@ -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 } @@ -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 } diff --git a/opcua_plugin/opcua.go b/opcua_plugin/opcua.go index db20e79..beca786 100644 --- a/opcua_plugin/opcua.go +++ b/opcua_plugin/opcua.go @@ -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 { @@ -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 } @@ -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, @@ -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 diff --git a/opcua_plugin/opcua_plc_test.go b/opcua_plugin/opcua_plc_test.go index 8368332..ea27d60 100644 --- a/opcua_plugin/opcua_plc_test.go +++ b/opcua_plugin/opcua_plc_test.go @@ -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()) }) @@ -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()) })