Skip to content

Commit

Permalink
bridge: use secured TLS communication
Browse files Browse the repository at this point in the history
  • Loading branch information
Danielius1922 authored and Daniel Adam committed Feb 1, 2024
1 parent 43fee4e commit afd0638
Show file tree
Hide file tree
Showing 45 changed files with 1,189 additions and 249 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 33 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"; \
Expand All @@ -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
67 changes: 51 additions & 16 deletions bridge/device/cloud/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package cloud
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
goSync "sync"
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
}
Expand Down
3 changes: 2 additions & 1 deletion bridge/device/cloud/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions bridge/device/cloud/options.go
Original file line number Diff line number Diff line change
@@ -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
}
}
59 changes: 59 additions & 0 deletions bridge/device/cloud/security.go
Original file line number Diff line number Diff line change
@@ -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
}
42 changes: 42 additions & 0 deletions bridge/device/cloud/security_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit afd0638

Please sign in to comment.