diff --git a/.vscode/settings.json b/.vscode/settings.json index 08548263..17339c48 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,7 +16,7 @@ "IDENTITY_KEY": "${workspaceFolder}/.tmp/pki_certs/identitykey.pem", "COAP_CRT": "${workspaceFolder}/.tmp/pki_certs/coapcrt.pem", "COAP_KEY": "${workspaceFolder}/.tmp/pki_certs/coapkey.pem", - "CLOUD_ID": "adebc667-1f2b-41e3-bf5c-6d6eabc68cc6", + "CLOUD_SID": "adebc667-1f2b-41e3-bf5c-6d6eabc68cc6", }, "files.watcherExclude": { "**/plgd-dev/device/v2/**": true diff --git a/Makefile b/Makefile index b8215b81..573aab19 100644 --- a/Makefile +++ b/Makefile @@ -124,24 +124,50 @@ test: env build-testcontainer $(SERVICE_NAME):$(VERSION_TAG) -test.parallel 1 -test.v -test.coverprofile=/tmp/coverage.txt test-bridge: + sudo rm -rf $(TMP_PATH)/data || : + mkdir -p $(TMP_PATH)/data + # pull image + docker pull $(HUB_TEST_DEVICE_IMAGE) + # prepare environment + docker run \ + --rm \ + --network=host \ + --name hub-device-tests-environment \ + --env PREPARE_ENV=true \ + --env RUN=false \ + --env COAP_GATEWAY_CLOUD_ID="$(CLOUD_SID)" \ + -v $(TMP_PATH):/tmp \ + -v $(TMP_PATH)/data:/data \ + $(HUB_TEST_DEVICE_IMAGE) + + # start device rm -rf $(TMP_PATH)/bridge || : mkdir -p $(TMP_PATH)/bridge go build -C ./test/ocfbridge -cover -o ./ocfbridge pkill -KILL ocfbridge || : - GOCOVERDIR=$(TMP_PATH)/bridge ./test/ocfbridge/ocfbridge -config ./test/ocfbridge/config.yaml & + CLOUD_SID=$(CLOUD_SID) CA_POOL=$(TMP_PATH)/data/certs/root_ca.crt \ + CERT_FILE=$(TMP_PATH)/data/certs/external/coap-gateway.crt \ + KEY_FILE=$(TMP_PATH)/data/certs/external/coap-gateway.key \ + GOCOVERDIR=$(TMP_PATH)/bridge \ + ./test/ocfbridge/ocfbridge & - docker pull $(HUB_TEST_DEVICE_IMAGE) && \ + # run tests docker run \ - --network=host \ --rm \ + --network=host \ --name hub-device-tests \ + --env PREPARE_ENV=false \ + --env RUN=true \ + --env COAP_GATEWAY_CLOUD_ID="$(CLOUD_SID)" \ --env TEST_DEVICE_NAME="bridged-device-0" \ --env TEST_DEVICE_TYPE="bridged" \ --env GRPC_GATEWAY_TEST_DISABLED=1 \ --env IOTIVITY_LITE_TEST_RUN="(TestOffboard|TestOffboardWithoutSignIn|TestOffboardWithRepeat|TestRepublishAfterRefresh)$$" \ -v $(TMP_PATH):/tmp \ + -v $(TMP_PATH)/data:/data \ $(HUB_TEST_DEVICE_IMAGE) + # stop device pkill -TERM ocfbridge || : while pgrep -x ocfbridge > /dev/null; do \ echo "waiting for ocfbridge to exit"; \ @@ -150,9 +176,10 @@ test-bridge: go tool covdata textfmt -i=$(TMP_PATH)/bridge -o $(TMP_PATH)/bridge.coverage.txt clean: - docker rm -f devsim-net-host || true - docker rm -f hub-device-tests || true - pkill -KILL ocfbridge || true + docker rm -f devsim-net-host || : + docker rm -f hub-device-tests-environment || : + docker rm -f hub-device-tests || : + pkill -KILL ocfbridge || : sudo rm -rf .tmp/* .PHONY: build-testcontainer build certificates clean env test unit-test diff --git a/bridge/device/cloud/manager.go b/bridge/device/cloud/manager.go index 930dec3e..483157a2 100644 --- a/bridge/device/cloud/manager.go +++ b/bridge/device/cloud/manager.go @@ -21,6 +21,7 @@ package cloud import ( "context" "crypto/tls" + "crypto/x509" "fmt" "log" goSync "sync" @@ -31,6 +32,7 @@ import ( "github.com/plgd-dev/device/v2/bridge/resources" "github.com/plgd-dev/device/v2/bridge/resources/discovery" "github.com/plgd-dev/device/v2/pkg/codec/cbor" + "github.com/plgd-dev/device/v2/pkg/net/coap" ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/cloud" @@ -45,7 +47,10 @@ import ( "github.com/plgd-dev/go-coap/v3/tcp/client" ) -type GetLinksFilteredBy func(endpoints schema.Endpoints, deviceIDfilter uuid.UUID, resourceTypesFitler []string, policyBitMaskFitler schema.BitMask) (links schema.ResourceLinks) +type ( + GetLinksFilteredBy func(endpoints schema.Endpoints, deviceIDfilter uuid.UUID, resourceTypesFitler []string, policyBitMaskFitler schema.BitMask) (links schema.ResourceLinks) + GetCertificates func(deviceID string) []tls.Certificate +) type Config struct { AccessToken string @@ -58,11 +63,13 @@ type Config struct { } type Manager struct { - handler net.RequestHandler - getLinks GetLinksFilteredBy - maxMessageSize uint32 - deviceID uuid.UUID - save func() + handler net.RequestHandler + getLinks GetLinksFilteredBy + maxMessageSize uint32 + deviceID uuid.UUID + save func() + caPool CAPool + getCertificates func(deviceID string) []tls.Certificate private struct { mutex goSync.Mutex @@ -77,18 +84,33 @@ type Manager struct { trigger chan bool } -func New(deviceID uuid.UUID, save func(), handler net.RequestHandler, getLinks GetLinksFilteredBy, maxMessageSize uint32) *Manager { +func New(deviceID uuid.UUID, save func(), handler net.RequestHandler, getLinks GetLinksFilteredBy, caPool CAPool, opts ...Option) (*Manager, error) { + if !caPool.IsValid() { + return nil, fmt.Errorf("invalid ca pool") + } + o := OptionsCfg{ + maxMessageSize: net.DefaultMaxMessageSize, + getCertificates: func(string) []tls.Certificate { + return nil + }, + } + for _, opt := range opts { + opt(&o) + } + c := &Manager{ - done: make(chan struct{}), - trigger: make(chan bool, 10), - handler: handler, - getLinks: getLinks, - maxMessageSize: maxMessageSize, - deviceID: deviceID, - save: save, + done: make(chan struct{}), + trigger: make(chan bool, 10), + handler: handler, + getLinks: getLinks, + deviceID: deviceID, + maxMessageSize: o.maxMessageSize, + save: save, + caPool: caPool, + getCertificates: o.getCertificates, } c.private.cfg.ProvisioningStatus = cloud.ProvisioningStatus_UNINITIALIZED - return c + return c, nil } func (c *Manager) Get(request *net.Request) (*pool.Message, error) { @@ -301,10 +323,23 @@ func (c *Manager) dial(ctx context.Context) error { } _ = c.close() cfg := c.getCloudConfiguration() + + caPool, err := c.caPool.GetPool() + if err != nil { + return fmt.Errorf("cannot get ca pool: %w", err) + } tlsConfig := &tls.Config{ - // TODO: set RootCAs from configuration InsecureSkipVerify: true, //nolint:gosec + Certificates: c.getCertificates(c.deviceID.String()), + VerifyPeerCertificate: coap.NewVerifyPeerCertificate(caPool, func(cert *x509.Certificate) error { + cloudID, errP := uuid.Parse(c.getCloudConfiguration().CloudID) + if errP != nil { + return fmt.Errorf("cannot parse cloudID: %w", errP) + } + return coap.VerifyCloudCertificate(cert, cloudID) + }), } + ep := schema.Endpoint{ URI: cfg.URL, } diff --git a/bridge/device/cloud/manager_test.go b/bridge/device/cloud/manager_test.go index d94e7ca1..aaed3f05 100644 --- a/bridge/device/cloud/manager_test.go +++ b/bridge/device/cloud/manager_test.go @@ -28,6 +28,7 @@ import ( "github.com/plgd-dev/device/v2/bridge/device/cloud" bridgeTest "github.com/plgd-dev/device/v2/bridge/test" cloudSchema "github.com/plgd-dev/device/v2/schema/cloud" + "github.com/plgd-dev/device/v2/test" testClient "github.com/plgd-dev/device/v2/test/client" mockCoapGW "github.com/plgd-dev/device/v2/test/coap-gateway" mockCoapGWService "github.com/plgd-dev/device/v2/test/coap-gateway/service" @@ -72,7 +73,7 @@ func TestProvisioningOnDeviceRestart(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - err = c.OnboardDevice(ctx, deviceID, "authorizationProvider", "coaps+tcp://"+mockCoapGW.COAP_GW_HOST, "authorizationCode", "cloudID") + err = c.OnboardDevice(ctx, deviceID, "authorizationProvider", "coaps+tcp://"+mockCoapGW.COAP_GW_HOST, "authorizationCode", test.CloudSID()) require.NoError(t, err) // wait for sign in diff --git a/bridge/device/cloud/options.go b/bridge/device/cloud/options.go new file mode 100644 index 00000000..39a9dd5f --- /dev/null +++ b/bridge/device/cloud/options.go @@ -0,0 +1,40 @@ +/**************************************************************************** + * + * Copyright (c) 2023 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package cloud + +type OptionsCfg struct { + maxMessageSize uint32 + getCertificates GetCertificates +} + +type Option func(*OptionsCfg) + +func WithMaxMessageSize(maxMessageSize uint32) Option { + return func(o *OptionsCfg) { + if maxMessageSize > 0 { + o.maxMessageSize = maxMessageSize + } + } +} + +func WithGetCertificates(getCertificates GetCertificates) Option { + return func(o *OptionsCfg) { + o.getCertificates = getCertificates + } +} diff --git a/bridge/device/cloud/security.go b/bridge/device/cloud/security.go new file mode 100644 index 00000000..d812f706 --- /dev/null +++ b/bridge/device/cloud/security.go @@ -0,0 +1,59 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package cloud + +import ( + "crypto/x509" + "fmt" +) + +type GetCAPool func() []*x509.Certificate + +type CAPool struct { + getCAPool GetCAPool + useSystemCAPool bool +} + +func MakeCAPool(getCAPool GetCAPool, useSystemCAPool bool) CAPool { + return CAPool{ + getCAPool: getCAPool, + useSystemCAPool: useSystemCAPool, + } +} + +func (c *CAPool) IsValid() bool { + return c.useSystemCAPool || (c.getCAPool != nil && c.getCAPool() != nil) +} + +func (c *CAPool) GetPool() (*x509.CertPool, error) { + var pool *x509.CertPool + if c.useSystemCAPool { + systemPool, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("cannot get system pool: %w", err) + } + pool = systemPool + } else { + pool = x509.NewCertPool() + } + for _, ca := range c.getCAPool() { + pool.AddCert(ca) + } + return pool, nil +} diff --git a/bridge/device/cloud/security_test.go b/bridge/device/cloud/security_test.go new file mode 100644 index 00000000..027249a1 --- /dev/null +++ b/bridge/device/cloud/security_test.go @@ -0,0 +1,42 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package cloud_test + +import ( + "crypto/x509" + "testing" + + "github.com/plgd-dev/device/v2/bridge/device/cloud" + "github.com/stretchr/testify/require" +) + +func TestGetPool(t *testing.T) { + getCAPool := func() []*x509.Certificate { + return []*x509.Certificate{{}} + } + caPool1 := cloud.MakeCAPool(getCAPool, true) + require.True(t, caPool1.IsValid()) + _, err := caPool1.GetPool() + require.NoError(t, err) + + caPool2 := cloud.MakeCAPool(getCAPool, false) + require.True(t, caPool2.IsValid()) + _, err = caPool2.GetPool() + require.NoError(t, err) +} diff --git a/bridge/device/device.go b/bridge/device/device.go index f3c6d73e..f46fe039 100644 --- a/bridge/device/device.go +++ b/bridge/device/device.go @@ -85,19 +85,77 @@ func (d *Device) ExportConfig() Config { return cfg } -func New(cfg Config, onDeviceUpdated func(d *Device), additionalProperties resourcesDevice.GetAdditionalPropertiesForResponseFunc) *Device { - if onDeviceUpdated == nil { - onDeviceUpdated = func(d *Device) { +type OnDeviceUpdated func(d *Device) + +type OptionsCfg struct { + onDeviceUpdated OnDeviceUpdated + getAdditionalProperties resourcesDevice.GetAdditionalPropertiesForResponseFunc + getCertificates cloud.GetCertificates + caPool cloud.CAPool +} + +type Option func(*OptionsCfg) + +func WithOnDeviceUpdated(onDeviceUpdated OnDeviceUpdated) Option { + return func(o *OptionsCfg) { + o.onDeviceUpdated = onDeviceUpdated + } +} + +func WithGetAdditionalPropertiesForResponse(getAdditionalProperties resourcesDevice.GetAdditionalPropertiesForResponseFunc) Option { + return func(o *OptionsCfg) { + o.getAdditionalProperties = getAdditionalProperties + } +} + +func WithGetCertificates(getCertificates cloud.GetCertificates) Option { + return func(o *OptionsCfg) { + o.getCertificates = getCertificates + } +} + +func WithCAPool(caPool cloud.CAPool) Option { + return func(o *OptionsCfg) { + o.caPool = caPool + } +} + +func New(cfg Config, opts ...Option) (*Device, error) { + o := OptionsCfg{ + onDeviceUpdated: func(d *Device) { // do nothing - } + }, + getAdditionalProperties: func() map[string]interface{} { return nil }, + caPool: cloud.MakeCAPool(nil, false), } + for _, opt := range opts { + opt(&o) + } + cfg.ResourceTypes = resources.Unique(append(cfg.ResourceTypes, plgdDevice.ResourceType)) d := &Device{ cfg: cfg, resources: sync.NewMap[string, Resource](), - onDeviceUpdated: onDeviceUpdated, + onDeviceUpdated: o.onDeviceUpdated, + } + + if cfg.Cloud.Enabled { + opts := []cloud.Option{cloud.WithMaxMessageSize(cfg.MaxMessageSize)} + if o.getCertificates != nil { + opts = append(opts, cloud.WithGetCertificates(o.getCertificates)) + } + cm, err := cloud.New(d.cfg.ID, func() { + d.onDeviceUpdated(d) + }, d.HandleRequest, d.GetLinksFilteredBy, o.caPool, opts...) + if err != nil { + return nil, fmt.Errorf("cannot create cloud manager: %w", err) + } + d.cloudManager = cm + d.AddResource(cloudResource.New(cloudSchema.ResourceURI, d.cloudManager)) + d.cloudManager.ImportConfig(cfg.Cloud.Config) } - d.AddResource(resourcesDevice.New(plgdDevice.ResourceURI, d, additionalProperties)) + + d.AddResource(resourcesDevice.New(plgdDevice.ResourceURI, d, o.getAdditionalProperties)) // oic/res is not discoverable discoverResource := discovery.New(plgdResources.ResourceURI, d.GetLinks) discoverResource.PolicyBitMask = schema.Discoverable @@ -107,14 +165,7 @@ func New(cfg Config, onDeviceUpdated func(d *Device), additionalProperties resou d.UnregisterFromCloud() })) - if cfg.Cloud.Enabled { - d.cloudManager = cloud.New(d.cfg.ID, func() { - d.onDeviceUpdated(d) - }, d.HandleRequest, d.GetLinksFilteredBy, cfg.MaxMessageSize) - d.AddResource(cloudResource.New(cloudSchema.ResourceURI, d.cloudManager)) - d.cloudManager.ImportConfig(cfg.Cloud.Config) - } - return d + return d, nil } func (d *Device) AddResource(resource Resource) { diff --git a/bridge/device/device_test.go b/bridge/device/device_test.go index 294f3f79..cb42a20d 100644 --- a/bridge/device/device_test.go +++ b/bridge/device/device_test.go @@ -59,7 +59,8 @@ var ( func TestNewDevice(t *testing.T) { cfg := deviceCfg - dev := device.New(cfg, nil, nil) + dev, err := device.New(cfg) + require.NoError(t, err) require.Equal(t, cfg.ID, dev.GetID()) require.Equal(t, cfg.Name, dev.GetName()) require.Equal(t, cfg.ProtocolIndependentID, dev.GetProtocolIndependentID()) @@ -75,13 +76,15 @@ func TestNewDeviceWithCloud(t *testing.T) { cfg := deviceCfg cfg.Cloud = cloudCfg - dev := device.New(cfg, nil, nil) + dev, err := device.New(cfg, device.WithCAPool(cloud.MakeCAPool(nil, true))) + require.NoError(t, err) cfg.ResourceTypes = append(cfg.ResourceTypes, "oic.wk.d") require.Equal(t, cfg, dev.ExportConfig()) } func TestGetResource(t *testing.T) { - dev := device.New(deviceCfg, nil, nil) + dev, err := device.New(deviceCfg) + require.NoError(t, err) _, ok := dev.GetResource(plgdDevice.ResourceURI) require.True(t, ok) @@ -96,7 +99,8 @@ func TestGetResource(t *testing.T) { func TestRangeResources(t *testing.T) { cfg := deviceCfg - dev := device.New(cfg, nil, nil) + dev, err := device.New(cfg) + require.NoError(t, err) resourceHrefs := []string{} dev.Range(func(href string, _ device.Resource) bool { resourceHrefs = append(resourceHrefs, href) @@ -109,7 +113,8 @@ func TestRangeResources(t *testing.T) { require.Contains(t, resourceHrefs, maintenanceSchema.ResourceURI) cfg.Cloud = cloudCfg - devWithCloud := device.New(cfg, nil, nil) + devWithCloud, err := device.New(cfg, device.WithCAPool(cloud.MakeCAPool(nil, true))) + require.NoError(t, err) resourceHrefs = []string{} devWithCloud.Range(func(href string, _ device.Resource) bool { resourceHrefs = append(resourceHrefs, href) @@ -124,7 +129,8 @@ func TestRangeResources(t *testing.T) { } func TestLoadAndDeleteResource(t *testing.T) { - dev := device.New(deviceCfg, nil, nil) + dev, err := device.New(deviceCfg) + require.NoError(t, err) _, ok := dev.LoadAndDeleteResource("/fail") require.False(t, ok) @@ -141,7 +147,8 @@ func TestLoadAndDeleteResource(t *testing.T) { } func TestCloseAndDeleteResource(t *testing.T) { - dev := device.New(deviceCfg, nil, nil) + dev, err := device.New(deviceCfg) + require.NoError(t, err) ok := dev.CloseAndDeleteResource("/fail") require.False(t, ok) diff --git a/bridge/getResource_test.go b/bridge/getResource_test.go index c346c97c..7b50415b 100644 --- a/bridge/getResource_test.go +++ b/bridge/getResource_test.go @@ -82,7 +82,6 @@ func TestGetResource(t *testing.T) { href: device.ResourceURI, opts: []client.GetOption{ client.WithDiscoveryConfiguration(core.DefaultDiscoveryConfiguration()), - client.WithDeviceID(d1.GetID().String()), }, }, want: coap.DetailedResponse[interface{}]{ @@ -101,7 +100,6 @@ func TestGetResource(t *testing.T) { href: device.ResourceURI, opts: []client.GetOption{ client.WithInterface(interfaces.OC_IF_BASELINE), - client.WithDeviceID(d2.GetID().String()), }, }, @@ -121,9 +119,6 @@ func TestGetResource(t *testing.T) { args: args{ deviceID: d1.GetID().String(), href: "/invalid/href", - opts: []client.GetOption{ - client.WithDeviceID(d1.GetID().String()), - }, }, wantErr: true, }, @@ -132,9 +127,6 @@ func TestGetResource(t *testing.T) { args: args{ deviceID: "notfound", href: device.ResourceURI, - opts: []client.GetOption{ - client.WithDeviceID(d1.GetID().String()), - }, }, wantErr: true, }, @@ -143,15 +135,12 @@ func TestGetResource(t *testing.T) { args: args{ deviceID: d1.GetID().String(), href: failRes.GetHref(), - opts: []client.GetOption{ - client.WithDeviceID(d1.GetID().String()), - }, }, wantErr: true, }, } - c, err := testClient.NewTestSecureClient() + c, err := testClient.NewTestSecureClientWithBridgeSupport() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/bridge/net/network.go b/bridge/net/network.go index 2e19e16f..9c4d0bce 100644 --- a/bridge/net/network.go +++ b/bridge/net/network.go @@ -242,7 +242,7 @@ func (s coAPServers) Close() error { return errors.ErrorOrNil() } -func getPortFromAddress(addr gonet.Addr) (string, error) { +func GetPortFromAddress(addr gonet.Addr) (string, error) { udpAddr, ok := addr.(*gonet.UDPAddr) if ok { return fmt.Sprintf("%d", udpAddr.Port), nil @@ -275,7 +275,7 @@ func newServers(cfg *Config, m *mux.Router) (coAPServers, bool, bool, error) { return nil, false, false, err } if addr.port == "0" { - port, err := getPortFromAddress(conn.LocalAddr()) + port, err := GetPortFromAddress(conn.LocalAddr()) if err != nil { _ = servers.Close() return nil, false, false, err @@ -427,7 +427,7 @@ func (n *Net) Serve() error { func (n *Net) Close() error { if !n.serving.Load() { - return nil + return n.servers.Close() } n.servers.Stop() <-n.done diff --git a/bridge/net/network_test.go b/bridge/net/network_test.go new file mode 100644 index 00000000..3e1a415e --- /dev/null +++ b/bridge/net/network_test.go @@ -0,0 +1,48 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgn.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implien. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package net_test + +import ( + "net" + "testing" + + bridgeNet "github.com/plgd-dev/device/v2/bridge/net" + "github.com/stretchr/testify/require" +) + +func TestGetPortFromAddress(t *testing.T) { + udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:42") + require.NoError(t, err) + port, err := bridgeNet.GetPortFromAddress(udpAddr) + require.NoError(t, err) + require.Equal(t, "42", port) + + tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:42") + require.NoError(t, err) + port, err = bridgeNet.GetPortFromAddress(tcpAddr) + require.NoError(t, err) + require.Equal(t, "42", port) +} + +func TestGetPortFromAddress_Fail(t *testing.T) { + ipAddr, err := net.ResolveIPAddr("ip", "127.0.0.1") + require.NoError(t, err) + _, err = bridgeNet.GetPortFromAddress(ipAddr) + require.Error(t, err) +} diff --git a/bridge/onboardDevice_test.go b/bridge/onboardDevice_test.go index a7fa0f21..232db442 100644 --- a/bridge/onboardDevice_test.go +++ b/bridge/onboardDevice_test.go @@ -25,7 +25,6 @@ import ( "github.com/google/uuid" bridgeTest "github.com/plgd-dev/device/v2/bridge/test" - "github.com/plgd-dev/device/v2/client" "github.com/plgd-dev/device/v2/test" testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/require" @@ -82,7 +81,7 @@ func TestOnboardDevice(t *testing.T) { }, } - c, err := testClient.NewTestSecureClient() + c, err := testClient.NewTestSecureClientWithBridgeSupport() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -94,13 +93,13 @@ func TestOnboardDevice(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ttCtx, ttCancel := context.WithTimeout(ctx, time.Second) defer ttCancel() - err = c.OnboardDevice(ttCtx, tt.args.deviceID, tt.args.authorizationProvider, tt.args.cloudURL, tt.args.authorizationCode, tt.args.cloudID, client.WithDeviceID(d.GetID().String())) + err = c.OnboardDevice(ttCtx, tt.args.deviceID, tt.args.authorizationProvider, tt.args.cloudURL, tt.args.authorizationCode, tt.args.cloudID) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) - err = c.OffboardDevice(ttCtx, tt.args.deviceID, client.WithDeviceID(d.GetID().String())) + err = c.OffboardDevice(ttCtx, tt.args.deviceID) require.NoError(t, err) }) } diff --git a/bridge/test/test.go b/bridge/test/test.go index 491495b4..b7f72fe3 100644 --- a/bridge/test/test.go +++ b/bridge/test/test.go @@ -19,11 +19,15 @@ package test import ( + "crypto/tls" + "crypto/x509" "testing" "github.com/google/uuid" "github.com/plgd-dev/device/v2/bridge/device" + "github.com/plgd-dev/device/v2/bridge/device/cloud" "github.com/plgd-dev/device/v2/bridge/service" + "github.com/plgd-dev/device/v2/test" "github.com/stretchr/testify/require" ) @@ -62,7 +66,14 @@ func NewBridgedDeviceWithConfig(t *testing.T, s *service.Service, cfg device.Con cfg.ID = di cfg.ProtocolIndependentID = piid require.NoError(t, cfg.Validate()) - return device.New(cfg, nil, nil) + caPool := cloud.MakeCAPool(func() []*x509.Certificate { + return test.GetRootCA(t) + }, false) + dev, err := device.New(cfg, device.WithCAPool(caPool), device.WithGetCertificates(func(deviceID string) []tls.Certificate { + return []tls.Certificate{test.GetMfgCertificate(t)} + })) + require.NoError(t, err) + return dev } d, ok := s.CreateDevice(cfg.ID, newDevice) require.True(t, ok) @@ -79,21 +90,14 @@ func makeDeviceConfig(id uuid.UUID, cloudEnabled bool) device.Config { } if cloudEnabled { cfg.Cloud.Enabled = true + cfg.Cloud.CloudID = test.CloudSID() } return cfg } func NewBridgedDevice(t *testing.T, s *service.Service, cloudEnabled bool, id string) service.Device { - newDevice := func(di uuid.UUID, piid uuid.UUID) service.Device { - cfg := makeDeviceConfig(di, cloudEnabled) - cfg.ProtocolIndependentID = piid - require.NoError(t, cfg.Validate()) - return device.New(cfg, nil, nil) - } u, err := uuid.Parse(id) require.NoError(t, err) - d, ok := s.CreateDevice(u, newDevice) - require.True(t, ok) - d.Init() - return d + cfg := makeDeviceConfig(u, cloudEnabled) + return NewBridgedDeviceWithConfig(t, s, cfg) } diff --git a/bridge/updateResource_test.go b/bridge/updateResource_test.go index 904c2844..0fb8362a 100644 --- a/bridge/updateResource_test.go +++ b/bridge/updateResource_test.go @@ -11,7 +11,6 @@ import ( "github.com/plgd-dev/device/v2/bridge/net" "github.com/plgd-dev/device/v2/bridge/resources" bridgeTest "github.com/plgd-dev/device/v2/bridge/test" - "github.com/plgd-dev/device/v2/client" "github.com/plgd-dev/device/v2/pkg/codec/cbor" codecOcf "github.com/plgd-dev/device/v2/pkg/codec/ocf" "github.com/plgd-dev/device/v2/pkg/net/coap" @@ -98,7 +97,7 @@ func TestUpdateResource(t *testing.T) { require.NoError(t, errC) }() - c, err := testClient.NewTestSecureClient() + c, err := testClient.NewTestSecureClientWithBridgeSupport() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -110,7 +109,7 @@ func TestUpdateResource(t *testing.T) { var got coap.DetailedResponse[interface{}] err = c.UpdateResource(ctx, d.GetID().String(), "/test", map[string]interface{}{ "name": "updated", - }, &got, client.WithDeviceID(d.GetID().String())) + }, &got) require.NoError(t, err) require.Equal(t, codes.Changed, got.Code) require.Equal(t, "updated", rds.getName()) @@ -118,12 +117,12 @@ func TestUpdateResource(t *testing.T) { // fail - invalid data err = c.UpdateResource(ctx, d.GetID().String(), "/test", map[string]interface{}{ "name": 1, - }, &got, client.WithDeviceID(d.GetID().String())) + }, &got) require.Error(t, err) // fail - invalid href err = c.UpdateResource(ctx, d.GetID().String(), "/invalid", map[string]interface{}{ "name": "updated", - }, &got, client.WithDeviceID(d.GetID().String())) + }, &got) require.Error(t, err) } diff --git a/client/app/app.go b/client/app/app.go index afc71fea..f5e32537 100644 --- a/client/app/app.go +++ b/client/app/app.go @@ -21,7 +21,7 @@ import ( "crypto/x509" "fmt" - "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" ) type AppConfig struct { @@ -46,13 +46,13 @@ func NewApp(cfg *AppConfig) (*App, error) { var manufacturerCert tls.Certificate var err error if len(cfg.RootCA) != 0 { - rootCA, err = security.ParseX509FromPEM([]byte(cfg.RootCA)) + rootCA, err = pkgX509.ParsePemCertificates([]byte(cfg.RootCA)) if err != nil { return nil, fmt.Errorf("invalid Root CA: %w", err) } } if cfg.Manufacturer != nil && len(cfg.Manufacturer.CA) != 0 { - manufacturerCA, err = security.ParseX509FromPEM([]byte(cfg.Manufacturer.CA)) + manufacturerCA, err = pkgX509.ParsePemCertificates([]byte(cfg.Manufacturer.CA)) if err != nil { return nil, fmt.Errorf("invalid Manufacturer's CA: %w", err) } diff --git a/client/core/client_test.go b/client/core/client_test.go index 9dd0c087..fa5d6185 100644 --- a/client/core/client_test.go +++ b/client/core/client_test.go @@ -30,11 +30,11 @@ import ( "github.com/plgd-dev/device/v2/client/core/otm/manufacturer" pkgError "github.com/plgd-dev/device/v2/pkg/error" "github.com/plgd-dev/device/v2/pkg/net/coap" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/test" "github.com/plgd-dev/go-coap/v3/tcp" "github.com/plgd-dev/go-coap/v3/udp" - "github.com/plgd-dev/kit/v2/security" "github.com/stretchr/testify/require" ) @@ -64,12 +64,12 @@ func NewTestSecureClientWithCert(cert tls.Certificate, disableDTLS, disableTCPTL return nil, err } - mfgCa, err := security.ParseX509FromPEM(test.RootCACrt) + mfgCa, err := pkgX509.ParsePemCertificates(test.RootCACrt) if err != nil { return nil, err } - identityIntermediateCA, err := security.ParseX509FromPEM(test.IdentityIntermediateCA) + identityIntermediateCA, err := pkgX509.ParsePemCertificates(test.IdentityIntermediateCA) if err != nil { return nil, err } diff --git a/client/core/otm/client.go b/client/core/otm/client.go index 63fc287e..03c2dfae 100644 --- a/client/core/otm/client.go +++ b/client/core/otm/client.go @@ -22,11 +22,11 @@ import ( "fmt" "github.com/plgd-dev/device/v2/pkg/net/coap" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" "github.com/plgd-dev/device/v2/schema/credential" "github.com/plgd-dev/device/v2/schema/csr" "github.com/plgd-dev/device/v2/schema/doxm" kitNet "github.com/plgd-dev/kit/v2/net" - kitSecurity "github.com/plgd-dev/kit/v2/security" ) // SignFunc handles a certifice signing request (csr), the csr and returned certificate chain are encoded in PEM format @@ -127,7 +127,7 @@ func ProvisionOwnerCredentials(ctx context.Context, tlsClient *coap.ClientCloseH return fmt.Errorf("cannot sign csr for setup device owner credentials: %w", err) } - certsFromChain, err := kitSecurity.ParseX509FromPEM(signedCsr) + certsFromChain, err := pkgX509.ParsePemCertificates(signedCsr) if err != nil { return fmt.Errorf("failed to parse chain of X509 certs: %w", err) } diff --git a/client/deviceOwnershipSDK.go b/client/deviceOwnershipSDK.go index 7f49eb8c..9adcca25 100644 --- a/client/deviceOwnershipSDK.go +++ b/client/deviceOwnershipSDK.go @@ -30,7 +30,7 @@ import ( "github.com/plgd-dev/device/v2/client/core/otm" justworks "github.com/plgd-dev/device/v2/client/core/otm/just-works" "github.com/plgd-dev/device/v2/client/core/otm/manufacturer" - "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" ) type Signer = interface { @@ -91,7 +91,7 @@ func newDeviceOwnershipSDK(app ApplicationCallback, sdkDeviceID string, dialTLS return nil, fmt.Errorf("invalid validFrom(%v) for device ownership SDK: %w", validFrom, err) } - signerCAs, err := security.ParseX509Certificates(signerCert) + signerCAs, err := pkgX509.ParseCertificates(signerCert) if err != nil { return nil, fmt.Errorf("could not parse signer certificates") } diff --git a/client/initialization.go b/client/initialization.go index 8043d0fd..bc745210 100644 --- a/client/initialization.go +++ b/client/initialization.go @@ -27,7 +27,7 @@ import ( "fmt" "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" - kitSecurity "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" ) func generateSDKCertificate(ctx context.Context, csr []byte, sign SignFunc, priv *ecdsa.PrivateKey) (tls.Certificate, []*x509.Certificate, error) { @@ -46,7 +46,7 @@ func generateSDKCertificate(ctx context.Context, csr []byte, sign SignFunc, priv return tls.Certificate{}, nil, fmt.Errorf("cannot create tls certificate: %w", err) } - certsFromChain, err := kitSecurity.ParseX509FromPEM(cert) + certsFromChain, err := pkgX509.ParsePemCertificates(cert) if err != nil { return tls.Certificate{}, nil, fmt.Errorf("cannot parse cert chain: %w", err) } diff --git a/cmd/ocfbridge/config.go b/cmd/ocfbridge/config.go index 03a0a246..8887678b 100644 --- a/cmd/ocfbridge/config.go +++ b/cmd/ocfbridge/config.go @@ -6,10 +6,40 @@ import ( "github.com/plgd-dev/device/v2/bridge/service" ) +type TLSConfig struct { + CAPoolPath string `yaml:"caPoolPath" json:"caPool" description:"file path to the root certificates in PEM format"` + KeyPath string `yaml:"keyPath" json:"keyFile" description:"file path to the private key in PEM format"` + CertPath string `yaml:"certPath" json:"certFile" description:"file path to the certificate in PEM format"` + UseSystemCAPool bool `yaml:"useSystemCAPool" json:"useSystemCaPool" description:"use system certification pool"` +} + +func (c *TLSConfig) Validate() error { + if c.CAPoolPath == "" && !c.UseSystemCAPool { + return fmt.Errorf("caPool is required") + } + if (c.KeyPath == "" && c.CertPath != "") || (c.KeyPath != "" && c.CertPath == "") { + return fmt.Errorf("keyFile and certFile must be set together") + } + return nil +} + +type CloudConfig struct { + Enabled bool `yaml:"enabled" json:"enabled" description:"enable cloud connection"` + TLS TLSConfig `yaml:"tls" json:"tls"` +} + +func (c *CloudConfig) Validate() error { + if c.Enabled { + return c.TLS.Validate() + } + return nil +} + type Config struct { service.Config `yaml:",inline"` - NumGeneratedBridgedDevices int `yaml:"numGeneratedBridgedDevices"` - NumResourcesPerDevice int `yaml:"numResourcesPerDevice"` + Cloud CloudConfig `yaml:"cloud" json:"cloud"` + NumGeneratedBridgedDevices int `yaml:"numGeneratedBridgedDevices"` + NumResourcesPerDevice int `yaml:"numResourcesPerDevice"` } func (c *Config) Validate() error { diff --git a/cmd/ocfbridge/config.yaml b/cmd/ocfbridge/config.yaml index bb621047..2f45c4e4 100644 --- a/cmd/ocfbridge/config.yaml +++ b/cmd/ocfbridge/config.yaml @@ -6,5 +6,7 @@ apis: - "127.0.0.1:15683" - "[::1]:15683" maxMessageSize: 2097152 +cloud: + enabled: false numGeneratedBridgedDevices: 100 numResourcesPerDevice: 256 diff --git a/cmd/ocfbridge/main.go b/cmd/ocfbridge/main.go index 9efb8669..e73e1e62 100644 --- a/cmd/ocfbridge/main.go +++ b/cmd/ocfbridge/main.go @@ -2,9 +2,10 @@ package main import ( "bytes" + "crypto/tls" + "crypto/x509" "flag" "fmt" - "log" "os" "os/signal" "path/filepath" @@ -15,11 +16,13 @@ import ( "github.com/google/uuid" "github.com/plgd-dev/device/v2/bridge/device" + "github.com/plgd-dev/device/v2/bridge/device/cloud" "github.com/plgd-dev/device/v2/bridge/net" "github.com/plgd-dev/device/v2/bridge/resources" "github.com/plgd-dev/device/v2/bridge/service" "github.com/plgd-dev/device/v2/pkg/codec/cbor" codecOcf "github.com/plgd-dev/device/v2/pkg/codec/ocf" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/go-coap/v3/message/codes" @@ -43,6 +46,9 @@ func loadConfig(configFile string) (Config, error) { if err != nil { return Config{}, err } + if err = cfg.Validate(); err != nil { + return Config{}, err + } return cfg, nil } @@ -141,6 +147,42 @@ func addResource(d service.Device, idx int, obsWatcher *coapSync.Map[uint64, fun d.AddResource(res) } +func getCloudTLS(cfg CloudConfig) (cloud.CAPool, *tls.Certificate, error) { + ca, err := pkgX509.ReadPemCertificates(cfg.TLS.CAPoolPath) + if err != nil { + return cloud.CAPool{}, nil, fmt.Errorf("cannot load ca: %w", err) + } + caPool := cloud.MakeCAPool(func() []*x509.Certificate { + return ca + }, cfg.TLS.UseSystemCAPool) + + if cfg.TLS.KeyPath == "" { + return caPool, nil, nil + } + + cert, err := tls.LoadX509KeyPair(cfg.TLS.CertPath, cfg.TLS.KeyPath) + if err != nil { + return cloud.CAPool{}, nil, fmt.Errorf("cannot load cert: %w", err) + } + return caPool, &cert, nil +} + +func handleSignals(s *service.Service) { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + for sig := range sigCh { + switch sig { + case syscall.SIGINT: + os.Exit(0) + return + case syscall.SIGTERM: + _ = s.Shutdown() + return + } + } +} + func main() { configFile := flag.String("config", "config.yaml", "path to config file") flag.Parse() @@ -152,22 +194,42 @@ func main() { if err != nil { panic(err) } + + opts := []device.Option{ + device.WithGetAdditionalPropertiesForResponse(func() map[string]interface{} { + return map[string]interface{}{ + "my-property": "my-value", + } + }), + } + if cfg.Cloud.Enabled { + caPool, cert, errC := getCloudTLS(cfg.Cloud) + if errC != nil { + panic(errC) + } + opts = append(opts, device.WithCAPool(caPool)) + if cert != nil { + opts = append(opts, device.WithGetCertificates(func(string) []tls.Certificate { + return []tls.Certificate{*cert} + })) + } + } + for i := 0; i < cfg.NumGeneratedBridgedDevices; i++ { newDevice := func(id uuid.UUID, piid uuid.UUID) service.Device { - d := device.New(device.Config{ + d, errD := device.New(device.Config{ Name: fmt.Sprintf("bridged-device-%d", i), ResourceTypes: []string{"oic.d.virtual"}, ID: id, ProtocolIndependentID: piid, MaxMessageSize: cfg.Config.API.CoAP.MaxMessageSize, Cloud: device.CloudConfig{ - Enabled: true, + Enabled: cfg.Cloud.Enabled, }, - }, nil, func() map[string]interface{} { - return map[string]interface{}{ - "my-property": "my-value", - } - }) + }, opts...) + if errD != nil { + panic(errD) + } return d } d, ok := s.CreateDevice(uuid.New(), newDevice) @@ -177,27 +239,11 @@ func main() { } } - // Signal handling. go func() { - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - for sig := range sigCh { - log.Printf("Trapped \"%v\" signal\n", sig) - switch sig { - case syscall.SIGINT: - log.Println("Exiting...") - os.Exit(0) - return - case syscall.SIGTERM: - _ = s.Shutdown() - return - } - } + handleSignals(s) }() - err = s.Serve() - if err != nil { + if err = s.Serve(); err != nil { panic(err) } } diff --git a/cmd/ocfclient/main.go b/cmd/ocfclient/main.go index 22a109a6..3458c197 100644 --- a/cmd/ocfclient/main.go +++ b/cmd/ocfclient/main.go @@ -37,7 +37,7 @@ import ( "github.com/plgd-dev/device/v2/pkg/codec/json" "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" "github.com/plgd-dev/device/v2/pkg/security/signer" - "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" ) type Options struct { @@ -387,11 +387,11 @@ func generateIntermediateCertificate(certConfig generateCertificate.Configuratio if err != nil { return err } - signerCert, err := security.LoadX509(signCert) + signerCert, err := pkgX509.ReadPemCertificates(signCert) if err != nil { return err } - signerKey, err := security.LoadX509PrivateKey(signKey) + signerKey, err := pkgX509.ReadPemEcdsaPrivateKey(signKey) if err != nil { return err } @@ -415,11 +415,11 @@ func generateIdentityCertificate(certConfig generateCertificate.Configuration, i if err != nil { return err } - signerCert, err := security.LoadX509(signCert) + signerCert, err := pkgX509.ReadPemCertificates(signCert) if err != nil { return err } - signerKey, err := security.LoadX509PrivateKey(signKey) + signerKey, err := pkgX509.ReadPemEcdsaPrivateKey(signKey) if err != nil { return err } diff --git a/go.mod b/go.mod index 9f3aa77f..784a6cb1 100644 --- a/go.mod +++ b/go.mod @@ -27,11 +27,10 @@ require ( github.com/dsnet/golib/memfile v1.0.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pion/transport/v3 v3.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect golang.org/x/net v0.20.0 // indirect diff --git a/go.sum b/go.sum index 05910ec1..9bccdd95 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -81,6 +82,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= github.com/lestrrat-go/jwx v1.0.2/go.mod h1:TPF17WiSFegZo+c20fdpw49QD+/7n4/IsGvEmCSWwT0= github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8= @@ -143,16 +145,11 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/pkg/net/coap/client.go b/pkg/net/coap/client.go index ff0326bc..9af82f48 100644 --- a/pkg/net/coap/client.go +++ b/pkg/net/coap/client.go @@ -21,15 +21,12 @@ import ( "context" "crypto/tls" "crypto/x509" - "encoding/asn1" "fmt" "io" "net" - "strings" "sync" "time" - "github.com/google/uuid" piondtls "github.com/pion/dtls/v2" codecOcf "github.com/plgd-dev/device/v2/pkg/codec/ocf" "github.com/plgd-dev/go-coap/v3/dtls" @@ -69,70 +66,6 @@ type Codec interface { Decode(m *pool.Message, v interface{}) error } -var ExtendedKeyUsage_IDENTITY_CERTIFICATE = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44924, 1, 6} - -func GetDeviceIDFromIdentityCertificate(cert *x509.Certificate) (string, error) { - // verify EKU manually - ekuHasClient := false - for _, eku := range cert.ExtKeyUsage { - if eku == x509.ExtKeyUsageClientAuth { - ekuHasClient = true - break - } - } - if !ekuHasClient { - return "", fmt.Errorf("not contains ExtKeyUsageClientAuth") - } - ekuHasOcfID := false - for _, eku := range cert.UnknownExtKeyUsage { - if eku.Equal(ExtendedKeyUsage_IDENTITY_CERTIFICATE) { - ekuHasOcfID = true - break - } - } - if !ekuHasOcfID { - return "", fmt.Errorf("not contains ExtKeyUsage with OCF ID(1.3.6.1.4.1.44924.1.6") - } - cn := strings.Split(cert.Subject.CommonName, ":") - if len(cn) != 2 { - return "", fmt.Errorf("invalid subject common name: %v", cert.Subject.CommonName) - } - if strings.ToLower(cn[0]) != "uuid" { - return "", fmt.Errorf("invalid subject common name %v: 'uuid' - not found", cert.Subject.CommonName) - } - deviceID, err := uuid.Parse(cn[1]) - if err != nil { - return "", fmt.Errorf("invalid subject common name %v: %w", cert.Subject.CommonName, err) - } - return deviceID.String(), nil -} - -func VerifyIdentityCertificate(cert *x509.Certificate) error { - // verify EKU manually - ekuHasClient := false - ekuHasServer := false - for _, eku := range cert.ExtKeyUsage { - if eku == x509.ExtKeyUsageClientAuth { - ekuHasClient = true - } - if eku == x509.ExtKeyUsageServerAuth { - ekuHasServer = true - } - } - if !ekuHasClient { - return fmt.Errorf("not contains ExtKeyUsageClientAuth") - } - if !ekuHasServer { - return fmt.Errorf("not contains ExtKeyUsageServerAuth") - } - _, err := GetDeviceIDFromIdentityCertificate(cert) - if err != nil { - return err - } - - return nil -} - func NewClient(conn ClientConn) *Client { return &Client{conn: conn} } diff --git a/pkg/net/coap/verify.go b/pkg/net/coap/verify.go new file mode 100644 index 00000000..274773f6 --- /dev/null +++ b/pkg/net/coap/verify.go @@ -0,0 +1,120 @@ +// ************************************************************************ +// Copyright (C) 2022 plgd.dev, s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ************************************************************************ + +package coap + +import ( + "crypto/x509" + "encoding/asn1" + "fmt" + "strings" + + "github.com/google/uuid" +) + +var ExtendedKeyUsage_IDENTITY_CERTIFICATE = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44924, 1, 6} + +func verifyOcfEKU(cert *x509.Certificate) error { + hasOcfID := false + for _, eku := range cert.UnknownExtKeyUsage { + if eku.Equal(ExtendedKeyUsage_IDENTITY_CERTIFICATE) { + hasOcfID = true + break + } + } + if !hasOcfID { + return fmt.Errorf("certificate does not contain ExtKeyUsage with OCF ID(1.3.6.1.4.1.44924.1.6") + } + return nil +} + +func verifyEKU(cert *x509.Certificate, requireClient, requireServer, requireOcfId bool) error { + hasClient := false + hasServer := false + for _, eku := range cert.ExtKeyUsage { + if eku == x509.ExtKeyUsageClientAuth { + hasClient = true + continue + } + if eku == x509.ExtKeyUsageServerAuth { + hasServer = true + continue + } + } + if requireClient && !hasClient { + return fmt.Errorf("certificate does not contain ExtKeyUsageClientAuth") + } + if requireServer && !hasServer { + return fmt.Errorf("certificate does not contain ExtKeyUsageServerAuth") + } + + if !requireOcfId { + return nil + } + return verifyOcfEKU(cert) +} + +func getUUIDFromSubjectCommonName(cert *x509.Certificate) (uuid.UUID, error) { + cn := strings.Split(cert.Subject.CommonName, ":") + if len(cn) != 2 { + return uuid.UUID{}, fmt.Errorf("invalid subject common name: %v", cert.Subject.CommonName) + } + if strings.ToLower(cn[0]) != "uuid" { + return uuid.UUID{}, fmt.Errorf("invalid subject common name %v: 'uuid' - not found", cert.Subject.CommonName) + } + id, err := uuid.Parse(cn[1]) + if err != nil { + return uuid.UUID{}, fmt.Errorf("invalid subject common name %v: %w", cert.Subject.CommonName, err) + } + return id, nil +} + +func GetDeviceIDFromIdentityCertificate(cert *x509.Certificate) (string, error) { + // verify EKU manually + if err := verifyEKU(cert, true, false, true); err != nil { + return "", err + } + deviceID, err := getUUIDFromSubjectCommonName(cert) + if err != nil { + return "", err + } + return deviceID.String(), nil +} + +func VerifyIdentityCertificate(cert *x509.Certificate) error { + if err := verifyEKU(cert, true, true, false); err != nil { + return err + } + if _, err := GetDeviceIDFromIdentityCertificate(cert); err != nil { + return err + } + return nil +} + +func VerifyCloudCertificate(cert *x509.Certificate, cloudID uuid.UUID) error { + if err := verifyEKU(cert, true, true, false); err != nil { + return err + } + id, err := getUUIDFromSubjectCommonName(cert) + if err != nil { + return err + } + + if id != cloudID { + return fmt.Errorf("invalid cloud certificate: invalid cloudID(%v)", id.String()) + } + return nil +} diff --git a/pkg/net/coap/verify_test.go b/pkg/net/coap/verify_test.go new file mode 100644 index 00000000..6d89c870 --- /dev/null +++ b/pkg/net/coap/verify_test.go @@ -0,0 +1,76 @@ +// ************************************************************************ +// Copyright (C) 2022 plgd.dev, s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ************************************************************************ + +package coap_test + +import ( + "crypto/ecdsa" + "crypto/x509" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/pkg/net/coap" + "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" + "github.com/stretchr/testify/require" +) + +func generateRootCA(t *testing.T, cfg generateCertificate.Configuration) ([]*x509.Certificate, *ecdsa.PrivateKey) { + key, err := cfg.GenerateKey() + require.NoError(t, err) + pem, err := generateCertificate.GenerateRootCA(cfg, key) + require.NoError(t, err) + certs, err := pkgX509.ParsePemCertificates(pem) + require.NoError(t, err) + return certs, key +} + +func TestVerifyIdentityCertificate(t *testing.T) { + cfg := generateCertificate.Configuration{ + ValidFor: time.Minute, + } + rootCA, rootCAKey := generateRootCA(t, cfg) + + key, err := cfg.GenerateKey() + require.NoError(t, err) + id := uuid.New().String() + certPem, err := generateCertificate.GenerateIdentityCert(cfg, id, key, rootCA, rootCAKey) + require.NoError(t, err) + cert, err := pkgX509.ParsePemCertificates(certPem) + require.NoError(t, err) + + err = coap.VerifyIdentityCertificate(cert[0]) + require.NoError(t, err) +} + +func TestVerifyCloudCertificate(t *testing.T) { + cfg := generateCertificate.Configuration{ + ValidFor: time.Minute, + } + rootCA, rootCAKey := generateRootCA(t, cfg) + + key, err := cfg.GenerateKey() + require.NoError(t, err) + cloudID := uuid.New() + certPem, err := generateCertificate.GenerateIdentityCert(cfg, cloudID.String(), key, rootCA, rootCAKey) + require.NoError(t, err) + cert, err := pkgX509.ParsePemCertificates(certPem) + require.NoError(t, err) + + err = coap.VerifyCloudCertificate(cert[0], cloudID) + require.NoError(t, err) +} diff --git a/pkg/security/generateCertificate/generateIntermediateCA.go b/pkg/security/generateCertificate/generateIntermediateCA.go index e21a1bf3..1b06d3f2 100644 --- a/pkg/security/generateCertificate/generateIntermediateCA.go +++ b/pkg/security/generateCertificate/generateIntermediateCA.go @@ -7,7 +7,7 @@ import ( "fmt" "math/big" - "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" ) func newCert(cfg Configuration) (*x509.Certificate, error) { @@ -57,5 +57,5 @@ func GenerateIntermediateCA(cfg Configuration, privateKey *ecdsa.PrivateKey, sig if err != nil { return nil, err } - return security.CreatePemChain(signerCA, der) + return pkgX509.CreatePemChain(signerCA, der) } diff --git a/pkg/security/generateCertificate/generateIntermediateCA_test.go b/pkg/security/generateCertificate/generateIntermediateCA_test.go index 3914b733..36fb5e4b 100644 --- a/pkg/security/generateCertificate/generateIntermediateCA_test.go +++ b/pkg/security/generateCertificate/generateIntermediateCA_test.go @@ -5,7 +5,7 @@ import ( "crypto/x509" "testing" - "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" "github.com/stretchr/testify/require" ) @@ -14,7 +14,7 @@ func generateRootCA(t *testing.T, cfg Configuration) ([]*x509.Certificate, *ecds require.NoError(t, err) cert, err := GenerateRootCA(cfg, privateKey) require.NoError(t, err) - crt, err := security.ParseX509FromPEM(cert) + crt, err := pkgX509.ParsePemCertificates(cert) require.NoError(t, err) return crt, privateKey } diff --git a/pkg/security/signer/signer.go b/pkg/security/signer/signer.go index d4f4f719..0207fe17 100644 --- a/pkg/security/signer/signer.go +++ b/pkg/security/signer/signer.go @@ -28,7 +28,7 @@ import ( "time" "github.com/plgd-dev/device/v2/pkg/net/coap" - "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" ) type OCFIdentityCertificate struct { @@ -105,5 +105,5 @@ func (s *OCFIdentityCertificate) Sign(_ context.Context, csr []byte) (signedCsr if err != nil { return } - return security.CreatePemChain(s.caCert, signedCsr) + return pkgX509.CreatePemChain(s.caCert, signedCsr) } diff --git a/pkg/security/signer/signer_test.go b/pkg/security/signer/signer_test.go index ae91a163..2d6065b9 100644 --- a/pkg/security/signer/signer_test.go +++ b/pkg/security/signer/signer_test.go @@ -14,7 +14,7 @@ import ( "github.com/google/uuid" "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" "github.com/plgd-dev/device/v2/pkg/security/signer" - "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" "github.com/stretchr/testify/require" ) @@ -28,9 +28,9 @@ func TestOCFIdentityCertificateSign(t *testing.T) { type args struct { csr []byte } - caCert, err := security.LoadX509(os.Getenv("ROOT_CA_CRT")) + caCert, err := pkgX509.ReadPemCertificates(os.Getenv("ROOT_CA_CRT")) require.NoError(t, err) - caKey, err := security.LoadX509PrivateKey(os.Getenv("ROOT_CA_KEY")) + caKey, err := pkgX509.ReadPemEcdsaPrivateKey(os.Getenv("ROOT_CA_KEY")) require.NoError(t, err) priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) diff --git a/pkg/security/x509/parse.go b/pkg/security/x509/parse.go new file mode 100644 index 00000000..93964319 --- /dev/null +++ b/pkg/security/x509/parse.go @@ -0,0 +1,108 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package x509 + +import ( + "crypto/ecdsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" +) + +// ParsePemCertificates parses x509 certificates from PEM format +func ParsePemCertificates(pemBlock []byte) ([]*x509.Certificate, error) { + data := pemBlock + var cas []*x509.Certificate + for { + certDERBlock, tmp := pem.Decode(data) + if certDERBlock == nil { + return nil, fmt.Errorf("cannot decode pem block") + } + certs, err := x509.ParseCertificates(certDERBlock.Bytes) + if err != nil { + return nil, err + } + cas = append(cas, certs...) + if len(tmp) == 0 { + break + } + data = tmp + } + return cas, nil +} + +// ReadPemCertificates reads certificates from file in PEM format +func ReadPemCertificates(path string) ([]*x509.Certificate, error) { + certPEMBlock, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return nil, err + } + return ParsePemCertificates(certPEMBlock) +} + +// ParsePemEcdsaPrivateKey parses private key from PEM format +func ParsePemEcdsaPrivateKey(pemBlock []byte) (*ecdsa.PrivateKey, error) { + derBlock, _ := pem.Decode(pemBlock) + if derBlock == nil { + return nil, fmt.Errorf("cannot decode pem block") + } + + if key, err := x509.ParsePKCS8PrivateKey(derBlock.Bytes); err == nil { + switch key := key.(type) { + case *ecdsa.PrivateKey: + return key, nil + default: + return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping") + } + } + + if key, err := x509.ParseECPrivateKey(derBlock.Bytes); err == nil { + return key, nil + } + + return nil, fmt.Errorf("failed to parse private key") +} + +// ReadPemEcdsaPrivateKey loads private key from file in PEM format +func ReadPemEcdsaPrivateKey(path string) (*ecdsa.PrivateKey, error) { + certPEMBlock, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return nil, err + } + return ParsePemEcdsaPrivateKey(certPEMBlock) +} + +// ParseCertificates parses the CA chain certificates from the DER data. +func ParseCertificates(cert *tls.Certificate) ([]*x509.Certificate, error) { + caChain := make([]*x509.Certificate, 0, 4) + for _, derBytes := range cert.Certificate { + ca, err := x509.ParseCertificates(derBytes) + if err != nil { + return nil, err + } + caChain = append(caChain, ca...) + } + if len(caChain) == 0 { + return nil, fmt.Errorf("no certificates") + } + return caChain, nil +} diff --git a/pkg/security/x509/parse_test.go b/pkg/security/x509/parse_test.go new file mode 100644 index 00000000..0442610b --- /dev/null +++ b/pkg/security/x509/parse_test.go @@ -0,0 +1,168 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package x509_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "os" + "testing" + + "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" + "github.com/stretchr/testify/require" +) + +func generateRootCA(t *testing.T, cfg generateCertificate.Configuration) ([]*x509.Certificate, *ecdsa.PrivateKey) { + key, err := cfg.GenerateKey() + require.NoError(t, err) + pem, err := generateCertificate.GenerateRootCA(cfg, key) + require.NoError(t, err) + certs, err := pkgX509.ParsePemCertificates(pem) + require.NoError(t, err) + return certs, key +} + +func TestParsePemCertificates(t *testing.T) { + generateRootCA(t, generateCertificate.Configuration{}) +} + +func TestParsePemCertificatesChain(t *testing.T) { + cfg := generateCertificate.Configuration{} + ca, caKey := generateRootCA(t, generateCertificate.Configuration{}) + + key, err := cfg.GenerateKey() + require.NoError(t, err) + got, err := generateCertificate.GenerateIntermediateCA(cfg, key, ca, caKey) + require.NoError(t, err) + _, err = pkgX509.ParsePemCertificates(got) + require.NoError(t, err) +} + +func encodeKeyToPem(t *testing.T, key *ecdsa.PrivateKey) []byte { + b, err := x509.MarshalECPrivateKey(key) + require.NoError(t, err) + block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + pem := pem.EncodeToMemory(block) + return pem +} + +func TestParsePemCertificates_Fail(t *testing.T) { + // invalid input + _, err := pkgX509.ParsePemCertificates(nil) + require.Error(t, err) + + // pem encoded key instead of certificate + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + pem := encodeKeyToPem(t, key) + _, err = pkgX509.ParsePemCertificates(pem) + require.Error(t, err) +} + +func TestReadPemCertificates(t *testing.T) { + cfg := generateCertificate.Configuration{} + key, err := cfg.GenerateKey() + require.NoError(t, err) + pem, err := generateCertificate.GenerateRootCA(cfg, key) + require.NoError(t, err) + + testFilePath := "./test.pem" + err = os.WriteFile(testFilePath, pem, 0o600) + require.NoError(t, err) + defer func() { + _ = os.Remove(testFilePath) + }() + + _, err = pkgX509.ReadPemCertificates(testFilePath) + require.NoError(t, err) +} + +func TestReadPemCertificates_Fail(t *testing.T) { + _, err := pkgX509.ReadPemCertificates("") + require.Error(t, err) +} + +func TestParsePemEcdsaPrivateKey(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + keyPem := encodeKeyToPem(t, key) + _, err = pkgX509.ParsePemEcdsaPrivateKey(keyPem) + require.NoError(t, err) + + pkcsDer, err := x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: pkcsDer} + pkcsPem := pem.EncodeToMemory(block) + _, err = pkgX509.ParsePemEcdsaPrivateKey(pkcsPem) + require.NoError(t, err) +} + +func TestParsePemEcdsaPrivateKey_Fail(t *testing.T) { + _, err := pkgX509.ParsePemEcdsaPrivateKey(nil) + require.Error(t, err) + + cfg := generateCertificate.Configuration{} + ecKey, err := cfg.GenerateKey() + require.NoError(t, err) + ecPem, err := generateCertificate.GenerateRootCA(cfg, ecKey) + require.NoError(t, err) + _, err = pkgX509.ParsePemEcdsaPrivateKey(ecPem) + require.Error(t, err) + + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + pkcsDer, err := x509.MarshalPKCS8PrivateKey(rsaKey) + require.NoError(t, err) + block := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcsDer} + pkcsPem := pem.EncodeToMemory(block) + _, err = pkgX509.ParsePemEcdsaPrivateKey(pkcsPem) + require.Error(t, err) +} + +func TestReadPemEcdsaPrivateKey(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + keyPem := encodeKeyToPem(t, key) + + testFilePath := "./test.pem" + err = os.WriteFile(testFilePath, keyPem, 0o600) + require.NoError(t, err) + defer func() { + _ = os.Remove(testFilePath) + }() + + _, err = pkgX509.ReadPemEcdsaPrivateKey(testFilePath) + require.NoError(t, err) +} + +func TestReadPemEcdsaPrivateKey_Fail(t *testing.T) { + _, err := pkgX509.ReadPemEcdsaPrivateKey("") + require.Error(t, err) +} + +func TestParseCertificates_Fail(t *testing.T) { + _, err := pkgX509.ParseCertificates(&tls.Certificate{}) + require.Error(t, err) +} diff --git a/pkg/security/x509/write.go b/pkg/security/x509/write.go new file mode 100644 index 00000000..5f00b2f6 --- /dev/null +++ b/pkg/security/x509/write.go @@ -0,0 +1,50 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package x509 + +import ( + "bytes" + "crypto/x509" + "encoding/pem" +) + +// CreatePemChain creates chain of PEM certificates. +func CreatePemChain(intermedateCAs []*x509.Certificate, cert []byte) ([]byte, error) { + buf := bytes.NewBuffer(make([]byte, 0, 2048)) + + // encode cert + err := pem.Encode(buf, &pem.Block{ + Type: "CERTIFICATE", Bytes: cert, + }) + if err != nil { + return nil, err + } + + // encode intermediates + for _, ca := range intermedateCAs { + err := pem.Encode(buf, &pem.Block{ + Type: "CERTIFICATE", Bytes: ca.Raw, + }) + if err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} diff --git a/pkg/security/x509/write_test.go b/pkg/security/x509/write_test.go new file mode 100644 index 00000000..27e1ae0a --- /dev/null +++ b/pkg/security/x509/write_test.go @@ -0,0 +1,46 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package x509_test + +import ( + "testing" + + "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" + "github.com/stretchr/testify/require" +) + +func TestCreatePemChain(t *testing.T) { + cfg := generateCertificate.Configuration{} + caKey, err := cfg.GenerateKey() + require.NoError(t, err) + caPem, err := generateCertificate.GenerateRootCA(cfg, caKey) + require.NoError(t, err) + ca, err := pkgX509.ParsePemCertificates(caPem) + require.NoError(t, err) + key, err := cfg.GenerateKey() + require.NoError(t, err) + intermediatePem, err := generateCertificate.GenerateIntermediateCA(cfg, key, ca, caKey) + require.NoError(t, err) + intermediate, err := pkgX509.ParsePemCertificates(intermediatePem) + require.NoError(t, err) + + _, err = pkgX509.CreatePemChain(intermediate, caPem) + require.NoError(t, err) +} diff --git a/sonar-project.properties b/sonar-project.properties index 4b4f4c5d..cb2e7bf6 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -22,4 +22,4 @@ sonar.cpd.exclusions=cmd/**/*.go # exludes from code coverage report sonar.go.coverage.reportPaths=coverage/**/*coverage*.txt -sonar.coverage.exclusions=bridge/test/**/*.go,test/**/*.go,client/app/app.go,cmd/ocfclient/ocfclient.go,**/main.go,**/*.pb.go,**/*.pb.gw.go,**/*.js,**/*.py +sonar.coverage.exclusions=bridge/test/**/*.go,test/**/*.go,client/app/app.go,cmd/**/*.go,**/main.go,**/*.pb.go,**/*.pb.gw.go,**/*.js,**/*.py diff --git a/test/coap-gateway/service/refreshToken.go b/test/coap-gateway/service/refreshToken.go index 18d054f5..231846c0 100644 --- a/test/coap-gateway/service/refreshToken.go +++ b/test/coap-gateway/service/refreshToken.go @@ -1,12 +1,30 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + package service import ( "fmt" + "github.com/plgd-dev/device/v2/pkg/codec/cbor" "github.com/plgd-dev/device/v2/pkg/ocf/cloud" coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/plgd-dev/go-coap/v3/mux" - "github.com/plgd-dev/kit/v2/codec/cbor" ) func refreshTokenPostHandler(req *mux.Message, client *Client) { diff --git a/test/coap-gateway/service/resourceDirectory.go b/test/coap-gateway/service/resourceDirectory.go index 0e668c67..8fd61ca3 100644 --- a/test/coap-gateway/service/resourceDirectory.go +++ b/test/coap-gateway/service/resourceDirectory.go @@ -25,10 +25,10 @@ import ( "strconv" "strings" + "github.com/plgd-dev/device/v2/pkg/codec/cbor" "github.com/plgd-dev/device/v2/pkg/ocf/cloud" coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/plgd-dev/go-coap/v3/mux" - "github.com/plgd-dev/kit/v2/codec/cbor" ) // fixHref ensures that href starts with "/" and does not end with "/". diff --git a/test/coap-gateway/service/signIn.go b/test/coap-gateway/service/signIn.go index 3ec9b23b..f1a4e532 100644 --- a/test/coap-gateway/service/signIn.go +++ b/test/coap-gateway/service/signIn.go @@ -21,10 +21,10 @@ package service import ( "fmt" + "github.com/plgd-dev/device/v2/pkg/codec/cbor" "github.com/plgd-dev/device/v2/pkg/ocf/cloud" coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/plgd-dev/go-coap/v3/mux" - "github.com/plgd-dev/kit/v2/codec/cbor" ) // https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.session.swagger.json diff --git a/test/coap-gateway/service/signUp.go b/test/coap-gateway/service/signUp.go index be7509eb..ee7df5ff 100644 --- a/test/coap-gateway/service/signUp.go +++ b/test/coap-gateway/service/signUp.go @@ -21,10 +21,10 @@ package service import ( "fmt" + "github.com/plgd-dev/device/v2/pkg/codec/cbor" "github.com/plgd-dev/device/v2/pkg/ocf/cloud" coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/plgd-dev/go-coap/v3/mux" - "github.com/plgd-dev/kit/v2/codec/cbor" ) // https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.account.swagger.json diff --git a/test/config.go b/test/config.go index 8cd83e0f..a518cdf3 100644 --- a/test/config.go +++ b/test/config.go @@ -28,6 +28,7 @@ import ( "time" "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/acl" "github.com/plgd-dev/device/v2/schema/ael" @@ -50,7 +51,6 @@ import ( "github.com/plgd-dev/device/v2/schema/softwareupdate" "github.com/plgd-dev/device/v2/schema/sp" testTypes "github.com/plgd-dev/device/v2/test/resource/types" - "github.com/plgd-dev/kit/v2/security" ) var ( @@ -224,11 +224,11 @@ func GenerateIdentityCert(deviceID string) tls.Certificate { if err != nil { log.Fatal(err) } - signerCA, err := security.ParseX509FromPEM(RootCACrt) + signerCA, err := pkgX509.ParsePemCertificates(RootCACrt) if err != nil { log.Fatal(err) } - signerCAKey, err := security.LoadX509PrivateKey(os.Getenv("ROOT_CA_KEY")) + signerCAKey, err := pkgX509.ReadPemEcdsaPrivateKey(os.Getenv("ROOT_CA_KEY")) if err != nil { log.Fatal(err) } diff --git a/test/ocfbridge/main.go b/test/ocfbridge/main.go index 25cb97f0..928b22c6 100644 --- a/test/ocfbridge/main.go +++ b/test/ocfbridge/main.go @@ -1,16 +1,19 @@ package main import ( + "crypto/tls" + "crypto/x509" "fmt" - "log" "os" "os/signal" "syscall" "github.com/google/uuid" "github.com/plgd-dev/device/v2/bridge/device" + "github.com/plgd-dev/device/v2/bridge/device/cloud" "github.com/plgd-dev/device/v2/bridge/net" "github.com/plgd-dev/device/v2/bridge/service" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" ) const ( @@ -31,6 +34,46 @@ func testConfig() service.Config { } } +func getCloudTLS() (cloud.CAPool, *tls.Certificate, error) { + caPath := os.Getenv("CA_POOL") + fmt.Printf("Loading CA(%s)\n", caPath) + ca, err := pkgX509.ReadPemCertificates(caPath) + if err != nil { + return cloud.CAPool{}, nil, fmt.Errorf("cannot load ca: %w", err) + } + caPool := cloud.MakeCAPool(func() []*x509.Certificate { + return ca + }, false) + + certPath := os.Getenv("CERT_FILE") + keyPath := os.Getenv("KEY_FILE") + if keyPath != "" && certPath != "" { + fmt.Printf("Loading certificate(%s) and key(%s)\n", certPath, keyPath) + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return cloud.CAPool{}, nil, fmt.Errorf("cannot load cert: %w", err) + } + return caPool, &cert, nil + } + return caPool, nil, nil +} + +func handleSignals(s *service.Service) { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + for sig := range sigCh { + switch sig { + case syscall.SIGINT: + os.Exit(0) + return + case syscall.SIGTERM: + _ = s.Shutdown() + return + } + } +} + func main() { cfg := testConfig() if err := cfg.Validate(); err != nil { @@ -40,9 +83,22 @@ func main() { if err != nil { panic(err) } + + opts := []device.Option{} + caPool, cert, errC := getCloudTLS() + if errC != nil { + panic(errC) + } + opts = append(opts, device.WithCAPool(caPool)) + if cert != nil { + opts = append(opts, device.WithGetCertificates(func(string) []tls.Certificate { + return []tls.Certificate{*cert} + })) + } + for i := 0; i < numGeneratedBridgedDevices; i++ { newDevice := func(id uuid.UUID, piid uuid.UUID) service.Device { - d := device.New(device.Config{ + d, errD := device.New(device.Config{ Name: fmt.Sprintf("bridged-device-%d", i), ResourceTypes: []string{"oic.d.virtual"}, ID: id, @@ -50,12 +106,14 @@ func main() { MaxMessageSize: cfg.API.CoAP.MaxMessageSize, Cloud: device.CloudConfig{ Enabled: true, + Config: cloud.Config{ + CloudID: os.Getenv("CLOUD_SID"), + }, }, - }, nil, func() map[string]interface{} { - return map[string]interface{}{ - "my-property": "my-value", - } - }) + }, opts...) + if errD != nil { + panic(errD) + } return d } d, ok := s.CreateDevice(uuid.New(), newDevice) @@ -64,27 +122,11 @@ func main() { } } - // Signal handling. go func() { - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - for sig := range sigCh { - log.Printf("Trapped \"%v\" signal\n", sig) - switch sig { - case syscall.SIGINT: - log.Println("Exiting...") - os.Exit(0) - return - case syscall.SIGTERM: - _ = s.Shutdown() - return - } - } + handleSignals(s) }() - err = s.Serve() - if err != nil { + if err = s.Serve(); err != nil { panic(err) } } diff --git a/test/test.go b/test/test.go index 25982401..70492ef2 100644 --- a/test/test.go +++ b/test/test.go @@ -30,11 +30,11 @@ import ( "github.com/plgd-dev/device/v2/client/core" "github.com/plgd-dev/device/v2/pkg/security/signer" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/device/v2/test/resource/types" - "github.com/plgd-dev/kit/v2/log" "github.com/stretchr/testify/require" ) @@ -196,7 +196,7 @@ func FindDeviceIP(ctx context.Context, deviceName string, ipType IPType) (string } defer func() { if errC := device.Close(ctx); errC != nil { - log.Errorf("FindDeviceIP: %w", errC) + fmt.Printf("cannot close device: %v\n", errC) } }() return getDeviceIP(device, ipType) @@ -219,7 +219,7 @@ func FindDeviceEndpoints(ctx context.Context, deviceName string, ipType IPType) } defer func() { if errC := device.Close(ctx); errC != nil { - log.Errorf("FindDeviceEndpoints: %w", errC) + fmt.Printf("cannot close device: %v\n", errC) } }() return device.GetEndpoints(), nil @@ -289,16 +289,20 @@ func CheckResourceLinks(t *testing.T, expected, actual schema.ResourceLinks) { require.Empty(t, expLinks) } -func CloudID() string { +func CloudSID() string { return os.Getenv("CLOUD_SID") } -func GetRootCertificate(t *testing.T) tls.Certificate { +func GetRootCA(t *testing.T) []*x509.Certificate { certPath := os.Getenv("ROOT_CA_CRT") require.NotEmpty(t, certPath) - keyPath := os.Getenv("ROOT_CA_KEY") - require.NotEmpty(t, keyPath) - ca, err := tls.LoadX509KeyPair(certPath, keyPath) + cas, err := pkgX509.ReadPemCertificates(certPath) + require.NoError(t, err) + return cas +} + +func GetMfgCertificate(t *testing.T) tls.Certificate { + ca, err := tls.X509KeyPair(MfgCert, MfgKey) require.NoError(t, err) return ca }