From 7fa196ddb428181a23b0be085063a83bd3073cdf Mon Sep 17 00:00:00 2001 From: Jozef Kralik Date: Wed, 31 Jan 2024 16:33:27 +0000 Subject: [PATCH] bridge: implement Credential Resource for CA Configuration during onboarding --- bridge/device/cloud/manager.go | 38 ++- bridge/device/cloud/manager_test.go | 7 +- bridge/device/cloud/options.go | 7 + bridge/device/cloud/security.go | 4 +- bridge/device/config.go | 7 + bridge/device/credential/config.go | 23 ++ bridge/device/credential/manager.go | 172 ++++++++++ .../credential/manager_internal_test.go | 313 ++++++++++++++++++ bridge/device/credential/security.go | 70 ++++ bridge/device/credential/security_test.go | 54 +++ bridge/device/device.go | 56 +++- bridge/getDevices_test.go | 6 +- bridge/getResource_test.go | 4 +- bridge/net/network.go | 25 +- bridge/observeResource_test.go | 2 +- bridge/onboardDevice_test.go | 53 ++- bridge/resources/cloud/resource.go | 2 +- .../resources/secure/credential/resource.go | 44 +++ bridge/service/service.go | 3 +- bridge/test/test.go | 20 +- bridge/updateResource_test.go | 2 +- cmd/ocfbridge/config.go | 11 +- cmd/ocfbridge/config.yaml | 8 +- cmd/ocfbridge/main.go | 22 +- schema/credential/credential.go | 46 +-- schema/link.go | 10 +- schema/link_test.go | 3 +- test/signer.go | 4 +- test/test.go | 12 +- 29 files changed, 936 insertions(+), 92 deletions(-) create mode 100644 bridge/device/credential/config.go create mode 100644 bridge/device/credential/manager.go create mode 100644 bridge/device/credential/manager_internal_test.go create mode 100644 bridge/device/credential/security.go create mode 100644 bridge/device/credential/security_test.go create mode 100644 bridge/resources/secure/credential/resource.go diff --git a/bridge/device/cloud/manager.go b/bridge/device/cloud/manager.go index 483157a2..62805b2b 100644 --- a/bridge/device/cloud/manager.go +++ b/bridge/device/cloud/manager.go @@ -24,7 +24,7 @@ import ( "crypto/x509" "fmt" "log" - goSync "sync" + "sync" "time" "github.com/google/uuid" @@ -50,6 +50,7 @@ import ( type ( GetLinksFilteredBy func(endpoints schema.Endpoints, deviceIDfilter uuid.UUID, resourceTypesFitler []string, policyBitMaskFitler schema.BitMask) (links schema.ResourceLinks) GetCertificates func(deviceID string) []tls.Certificate + RemoveCloudCAs func(cloudID ...string) ) type Config struct { @@ -62,18 +63,25 @@ type Config struct { CloudURL string } +type CAPoolGetter = interface { + IsValid() bool + GetPool() (*x509.CertPool, error) +} + type Manager struct { handler net.RequestHandler getLinks GetLinksFilteredBy maxMessageSize uint32 deviceID uuid.UUID save func() - caPool CAPool - getCertificates func(deviceID string) []tls.Certificate + caPool CAPoolGetter + getCertificates GetCertificates + removeCloudCAs RemoveCloudCAs private struct { - mutex goSync.Mutex - cfg Configuration + mutex sync.Mutex + cfg Configuration + previousCloudIDs []string } creds ocfCloud.CoapSignUpResponse @@ -84,7 +92,7 @@ type Manager struct { trigger chan bool } -func New(deviceID uuid.UUID, save func(), handler net.RequestHandler, getLinks GetLinksFilteredBy, caPool CAPool, opts ...Option) (*Manager, error) { +func New(deviceID uuid.UUID, save func(), handler net.RequestHandler, getLinks GetLinksFilteredBy, caPool CAPoolGetter, opts ...Option) (*Manager, error) { if !caPool.IsValid() { return nil, fmt.Errorf("invalid ca pool") } @@ -93,6 +101,9 @@ func New(deviceID uuid.UUID, save func(), handler net.RequestHandler, getLinks G getCertificates: func(string) []tls.Certificate { return nil }, + removeCloudCAs: func(...string) { + // do nothing + }, } for _, opt := range opts { opt(&o) @@ -108,6 +119,7 @@ func New(deviceID uuid.UUID, save func(), handler net.RequestHandler, getLinks G save: save, caPool: caPool, getCertificates: o.getCertificates, + removeCloudCAs: o.removeCloudCAs, } c.private.cfg.ProvisioningStatus = cloud.ProvisioningStatus_UNINITIALIZED return c, nil @@ -168,6 +180,7 @@ func (c *Manager) resetCredentials(ctx context.Context, signOff bool) { log.Printf("cannot close connection: %v\n", err) } c.save() + c.removePreviousCloudIDs() } func (c *Manager) cleanup() { @@ -216,9 +229,22 @@ func (c *Manager) Post(request *net.Request) (*pool.Message, error) { return resources.CreateResponseContent(request.Context(), currentCfg, codes.Changed) } +func (c *Manager) popPreviousCloudIDs() []string { + c.private.mutex.Lock() + defer c.private.mutex.Unlock() + previousCloudIDs := c.private.previousCloudIDs + c.private.previousCloudIDs = nil + return previousCloudIDs +} + +func (c *Manager) removePreviousCloudIDs() { + c.removeCloudCAs(c.popPreviousCloudIDs()...) +} + func (c *Manager) setCloudConfiguration(cfg cloud.ConfigurationUpdateRequest) { c.private.mutex.Lock() defer c.private.mutex.Unlock() + c.private.previousCloudIDs = append(c.private.previousCloudIDs, c.private.cfg.CloudID) c.private.cfg.AuthorizationProvider = cfg.AuthorizationProvider c.private.cfg.CloudID = cfg.CloudID c.private.cfg.URL = cfg.URL diff --git a/bridge/device/cloud/manager_test.go b/bridge/device/cloud/manager_test.go index aaed3f05..93218363 100644 --- a/bridge/device/cloud/manager_test.go +++ b/bridge/device/cloud/manager_test.go @@ -58,7 +58,7 @@ func TestProvisioningOnDeviceRestart(t *testing.T) { _ = s1.Shutdown() }) deviceID := uuid.New().String() - d1 := bridgeTest.NewBridgedDevice(t, s1, true, deviceID) + d1 := bridgeTest.NewBridgedDevice(t, s1, deviceID, true, false) s1Shutdown := bridgeTest.RunBridgeService(s1) t.Cleanup(func() { _ = s1Shutdown() @@ -106,6 +106,9 @@ func TestProvisioningOnDeviceRestart(t *testing.T) { require.Equal(t, cloudCfg.ProvisioningStatus, cloudSchema.ProvisioningStatus_REGISTERED) // sign off - d2.UnregisterFromCloud() + cloudManager := d2.GetCloudManager() + if cloudManager != nil { + cloudManager.Unregister() + } require.Equal(t, 1, ch.WaitForSignOff(time.Second*20)) } diff --git a/bridge/device/cloud/options.go b/bridge/device/cloud/options.go index 39a9dd5f..3a2b957a 100644 --- a/bridge/device/cloud/options.go +++ b/bridge/device/cloud/options.go @@ -21,6 +21,7 @@ package cloud type OptionsCfg struct { maxMessageSize uint32 getCertificates GetCertificates + removeCloudCAs RemoveCloudCAs } type Option func(*OptionsCfg) @@ -38,3 +39,9 @@ func WithGetCertificates(getCertificates GetCertificates) Option { o.getCertificates = getCertificates } } + +func WithRemoveCloudCAs(removeCloudCA RemoveCloudCAs) Option { + return func(o *OptionsCfg) { + o.removeCloudCAs = removeCloudCA + } +} diff --git a/bridge/device/cloud/security.go b/bridge/device/cloud/security.go index d812f706..a30a1aff 100644 --- a/bridge/device/cloud/security.go +++ b/bridge/device/cloud/security.go @@ -37,11 +37,11 @@ func MakeCAPool(getCAPool GetCAPool, useSystemCAPool bool) CAPool { } } -func (c *CAPool) IsValid() bool { +func (c CAPool) IsValid() bool { return c.useSystemCAPool || (c.getCAPool != nil && c.getCAPool() != nil) } -func (c *CAPool) GetPool() (*x509.CertPool, error) { +func (c CAPool) GetPool() (*x509.CertPool, error) { var pool *x509.CertPool if c.useSystemCAPool { systemPool, err := x509.SystemCertPool() diff --git a/bridge/device/config.go b/bridge/device/config.go index 8d63bfef..c8fad9ce 100644 --- a/bridge/device/config.go +++ b/bridge/device/config.go @@ -23,6 +23,7 @@ import ( "github.com/google/uuid" "github.com/plgd-dev/device/v2/bridge/device/cloud" + "github.com/plgd-dev/device/v2/bridge/device/credential" ) type CloudConfig struct { @@ -30,6 +31,11 @@ type CloudConfig struct { cloud.Config } +type CredentialConfig struct { + Enabled bool + credential.Config +} + type Config struct { ID uuid.UUID Name string @@ -37,6 +43,7 @@ type Config struct { ResourceTypes []string MaxMessageSize uint32 Cloud CloudConfig + Credential CredentialConfig } func (cfg *Config) Validate() error { diff --git a/bridge/device/credential/config.go b/bridge/device/credential/config.go new file mode 100644 index 00000000..486b5920 --- /dev/null +++ b/bridge/device/credential/config.go @@ -0,0 +1,23 @@ +/**************************************************************************** + * + * 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 credential + +import "github.com/plgd-dev/device/v2/schema/credential" + +type Config = credential.CredentialResponse diff --git a/bridge/device/credential/manager.go b/bridge/device/credential/manager.go new file mode 100644 index 00000000..7daa41d7 --- /dev/null +++ b/bridge/device/credential/manager.go @@ -0,0 +1,172 @@ +/**************************************************************************** + * + * 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 credential + +import ( + "crypto/x509" + + "github.com/plgd-dev/device/v2/bridge/net" + "github.com/plgd-dev/device/v2/bridge/resources" + "github.com/plgd-dev/device/v2/pkg/codec/cbor" + 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/interfaces" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/pkg/sync" +) + +type Manager struct { + credentials *sync.Map[int, credential.Credential] + save func() +} + +func New(save func()) *Manager { + if save == nil { + save = func() { + // do nothing + } + } + return &Manager{ + save: save, + credentials: sync.NewMap[int, credential.Credential](), + } +} + +func (m *Manager) GetCAPool() []*x509.Certificate { + certs := make([]*x509.Certificate, 0, m.credentials.Length()) + m.credentials.Range(func(key int, value credential.Credential) bool { + if value.Type != credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE { + return true + } + if value.Usage != credential.CredentialUsage_TRUST_CA && value.Usage != credential.CredentialUsage_MFG_TRUST_CA { + return true + } + cas, err := pkgX509.ParsePemCertificates(value.PublicData.Data()) + if err != nil { + return true + } + certs = append(certs, cas...) + return true + }) + return certs +} + +func (m *Manager) getNextID() int { + var id int + m.credentials.Range(func(key int, value credential.Credential) bool { + if key > id { + id = key + } + return true + }) + return id + 1 +} + +func (m *Manager) add(c credential.Credential) { + for { + _, loaded := m.credentials.LoadOrStore(c.ID, c) + if !loaded { + return + } + c.ID = m.getNextID() + } +} + +func (m *Manager) AddOrReplaceCredential(c credential.Credential) { + if c.Type == credential.CredentialType_EMPTY { + return + } + if c.ID != 0 { + // replace + m.credentials.Store(c.ID, c) + return + } + // add + m.add(c) +} + +func (m *Manager) AddOrReplaceCredentials(ca ...credential.Credential) { + for _, c := range ca { + m.AddOrReplaceCredential(c) + } +} + +func (m *Manager) RemoveCredentials(ids ...int) { + for _, id := range ids { + m.credentials.Delete(id) + } +} + +func (m *Manager) RemoveCredentialsBySubjects(subjects ...string) { + m.credentials.Range(func(key int, value credential.Credential) bool { + for _, subject := range subjects { + if value.Subject == subject { + m.credentials.Delete(key) + } + } + return true + }) +} + +func (m *Manager) ClearCredentials() { + _ = m.credentials.LoadAndDeleteAll() +} + +func (m *Manager) getRep(privateData bool) credential.CredentialResponse { + cas := m.credentials.CopyData() + creds := credential.CredentialResponse{ + Interfaces: []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_RW}, + ResourceTypes: []string{credential.ResourceType}, + Credentials: make([]credential.Credential, 0, len(cas)), + } + for _, cred := range cas { + if !privateData { + // remove private data + cred.PrivateData = nil + } + creds.Credentials = append(creds.Credentials, cred) + } + return creds +} + +func (m *Manager) Get(request *net.Request) (*pool.Message, error) { + creds := m.getRep(false) + return resources.CreateResponseContent(request.Context(), creds, codes.Content) +} + +func (m *Manager) Post(request *net.Request) (*pool.Message, error) { + var cfg credential.CredentialUpdateRequest + err := cbor.ReadFrom(request.Body(), &cfg) + if err != nil { + return resources.CreateResponseBadRequest(request.Context(), err) + } + m.AddOrReplaceCredentials(cfg.Credentials...) + m.save() + creds := m.getRep(false) + return resources.CreateResponseContent(request.Context(), creds, codes.Changed) +} + +func (m *Manager) ExportConfig() Config { + return m.getRep(true) +} + +func (m *Manager) Close() { + m.ClearCredentials() +} diff --git a/bridge/device/credential/manager_internal_test.go b/bridge/device/credential/manager_internal_test.go new file mode 100644 index 00000000..b5c28c10 --- /dev/null +++ b/bridge/device/credential/manager_internal_test.go @@ -0,0 +1,313 @@ +/**************************************************************************** + * + * 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 credential + +import ( + "bytes" + "context" + "testing" + + "github.com/plgd-dev/device/v2/bridge/net" + "github.com/plgd-dev/device/v2/pkg/codec/cbor" + "github.com/plgd-dev/device/v2/schema/credential" + "github.com/plgd-dev/device/v2/test" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/stretchr/testify/require" +) + +func TestGetCAPool(t *testing.T) { + m := New(func() {}) + + // Add some credentials to the manager + cred1 := credential.Credential{ + Type: credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE, + Usage: credential.CredentialUsage_TRUST_CA, + PublicData: &credential.CredentialPublicData{ + DataInternal: test.GetRootCApem(t), + }, + } + cred2 := credential.Credential{ + Type: credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE, + Usage: credential.CredentialUsage_MFG_TRUST_CA, + PublicData: &credential.CredentialPublicData{ + DataInternal: test.GetRootCApem(t), + }, + } + cred3 := credential.Credential{ + Type: credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE, + Usage: credential.CredentialUsage_CERT, + PublicData: &credential.CredentialPublicData{ + DataInternal: test.GetRootCApem(t), + }, + } + m.credentials.Store(1, cred1) + m.credentials.Store(2, cred2) + m.credentials.Store(3, cred3) + + // Call the GetCAPool function + certs := m.GetCAPool() + + // Verify the result + require.Len(t, certs, 2) +} + +func TestAddOrReplaceCredential(t *testing.T) { + m := New(func() {}) + + // Test adding a new credential + cred1 := credential.Credential{ + ID: 1, + Subject: "subject", + Type: credential.CredentialType_PIN_OR_PASSWORD, + } + m.AddOrReplaceCredential(cred1) + _, ok := m.credentials.Load(cred1.ID) + require.True(t, ok) + + // Test replacing an existing credential + cred2 := credential.Credential{ + ID: 1, + Subject: "subject1", + Type: credential.CredentialType_ASYMMETRIC_SIGNING, + } + m.AddOrReplaceCredential(cred2) + c, ok := m.credentials.Load(cred2.ID) + require.True(t, ok) + require.Equal(t, cred2, c) + + // Test adding an empty credential + cred3 := credential.Credential{ + ID: 0, + Type: credential.CredentialType_EMPTY, + } + m.AddOrReplaceCredential(cred3) + _, ok = m.credentials.Load(cred3.ID) + require.False(t, ok) +} + +func TestAddOrReplaceCredentials(t *testing.T) { + m := New(func() {}) // Create an instance of the Manager struct + + // Create some test credentials + cred1 := credential.Credential{ + Subject: "subject1", + Type: credential.CredentialType_PIN_OR_PASSWORD, + } + cred2 := credential.Credential{ + Subject: "subject2", + Type: credential.CredentialType_PIN_OR_PASSWORD, + } + cred3 := credential.Credential{ + Subject: "subject3", + Type: credential.CredentialType_EMPTY, + } + + // Add the credentials to the manager + m.AddOrReplaceCredentials(cred1, cred2, cred3) + + // Verify that the credentials were added correctly + require.Equal(t, 2, m.credentials.Length()) +} + +func TestRemoveCredentials(t *testing.T) { + m := New(func() {}) + + // Add some credentials to the Manager + m.credentials.Store(1, credential.Credential{ + Subject: "subject1", + Type: credential.CredentialType_PIN_OR_PASSWORD, + }) + m.credentials.Store(2, credential.Credential{ + Subject: "subject2", + Type: credential.CredentialType_PIN_OR_PASSWORD, + }) + m.credentials.Store(3, credential.Credential{ + Subject: "subject3", + Type: credential.CredentialType_PIN_OR_PASSWORD, + }) + + // Remove credentials with IDs 1 and 3 + m.RemoveCredentials(1, 3) + + // Check if the credentials were removed + _, ok := m.credentials.Load(1) + require.False(t, ok, "Credential with ID 1 should be removed") + _, ok = m.credentials.Load(3) + require.False(t, ok, "Credential with ID 3 should be removed") + + // Check if the credential with ID 2 still exists + _, ok = m.credentials.Load(2) + require.True(t, ok, "Credential with ID 2 should still exist") +} + +func TestRemoveCredentialsBySubjects(t *testing.T) { + // Create a new instance of the Manager + m := New(func() {}) + + // Add some credentials to the Manager + cred1 := credential.Credential{Subject: "subject1", Type: credential.CredentialType_PIN_OR_PASSWORD} + cred2 := credential.Credential{Subject: "subject2", Type: credential.CredentialType_PIN_OR_PASSWORD} + cred3 := credential.Credential{Subject: "subject3", Type: credential.CredentialType_PIN_OR_PASSWORD} + m.credentials.Store(1, cred1) + m.credentials.Store(2, cred2) + m.credentials.Store(3, cred3) + + // Call the RemoveCredentialsBySubjects method with some subjects + m.RemoveCredentialsBySubjects("subject1", "subject3") + + // Check that the credentials with the specified subjects have been removed + _, ok1 := m.credentials.Load(1) + _, ok2 := m.credentials.Load(2) + _, ok3 := m.credentials.Load(3) + require.False(t, ok1, "Credential with subject 'subject1' should have been removed") + require.True(t, ok2, "Credential with subject 'subject2' should not have been removed") + require.False(t, ok3, "Credential with subject 'subject3' should have been removed") +} + +func TestClearCredentials(t *testing.T) { + m := New(func() {}) // Create an instance of the Manager struct + + // Add some credentials to the Manager + cred1 := credential.Credential{Subject: "subject1", Type: credential.CredentialType_PIN_OR_PASSWORD} + cred2 := credential.Credential{Subject: "subject2", Type: credential.CredentialType_PIN_OR_PASSWORD} + cred3 := credential.Credential{Subject: "subject3", Type: credential.CredentialType_PIN_OR_PASSWORD} + m.credentials.Store(1, cred1) + m.credentials.Store(2, cred2) + m.credentials.Store(3, cred3) + + // Call the ClearCredentials method + m.ClearCredentials() + + require.Equal(t, 0, m.credentials.Length()) +} + +func TestGetCredential(t *testing.T) { + m := New(func() {}) // Create an instance of the Manager struct + + // Add some credentials to the Manager + cred1 := credential.Credential{Subject: "subject1", Type: credential.CredentialType_PIN_OR_PASSWORD, PrivateData: &credential.CredentialPrivateData{DataInternal: []byte("private data")}} + cred2 := credential.Credential{Subject: "subject2", Type: credential.CredentialType_PIN_OR_PASSWORD} + cred3 := credential.Credential{Subject: "subject3", Type: credential.CredentialType_PIN_OR_PASSWORD} + m.credentials.Store(1, cred1) + m.credentials.Store(2, cred2) + m.credentials.Store(3, cred3) + + msg := pool.NewMessage(context.Background()) + msg.SetCode(codes.GET) + err := msg.SetPath(credential.ResourceURI) + require.NoError(t, err) + msg.SetToken([]byte{0x01}) + + // Create a mock request + request := &net.Request{ + Message: msg, + } + + // Call the Get method + response, err := m.Get(request) + + // Check if the response and error are as expected + require.NoError(t, err) + require.NotNil(t, response) + require.Equal(t, codes.Content, response.Code()) + require.NotNil(t, response.Body()) + + var cred credential.CredentialResponse + err = cbor.ReadFrom(response.Body(), &cred) + require.NoError(t, err) + require.Len(t, cred.Credentials, 3) + for _, c := range cred.Credentials { + require.Nil(t, c.PrivateData) + } +} + +func TestPostCredential(t *testing.T) { + m := New(func() {}) // Create an instance of the Manager struct + + msg := pool.NewMessage(context.Background()) + msg.SetCode(codes.POST) + err := msg.SetPath(credential.ResourceURI) + require.NoError(t, err) + msg.SetToken([]byte{0x01}) + + updBody := credential.CredentialUpdateRequest{ + Credentials: []credential.Credential{ + { + Subject: "subject1", + Type: credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE, + Usage: credential.CredentialUsage_TRUST_CA, + PublicData: &credential.CredentialPublicData{ + DataInternal: test.GetRootCApem(t), + Encoding: credential.CredentialPublicDataEncoding_PEM, + }, + }, + }, + } + body, err := cbor.Encode(updBody) + require.NoError(t, err) + msg.SetBody(bytes.NewReader(body)) + + // Create a mock request + req := &net.Request{ + Message: msg, + } + + // Call the Post method + resp, err := m.Post(req) + require.NoError(t, err) + + // Verify the response + require.NotNil(t, resp) + require.Equal(t, codes.Changed, resp.Code()) + + // Verify the credentials were added or replaced + creds := m.getRep(false) + require.Len(t, creds.Credentials, 1) + require.Equal(t, "subject1", creds.Credentials[0].Subject) +} + +func TestExportConfig(t *testing.T) { + m := New(func() {}) // Create an instance of the Manager struct + + // Add some credentials to the Manager + cred1 := credential.Credential{Subject: "subject1", Type: credential.CredentialType_PIN_OR_PASSWORD, PrivateData: &credential.CredentialPrivateData{DataInternal: []byte("private data")}} + m.credentials.Store(1, cred1) + + // Call the ExportConfig function + config := m.ExportConfig() + + // Assert that the returned config is not nil + require.Len(t, config.Credentials, 1) + require.Equal(t, "subject1", config.Credentials[0].Subject) + require.NotNil(t, config.Credentials[0].PrivateData) + require.NotNil(t, config.Credentials[0].PrivateData.DataInternal) +} + +func TestClose(t *testing.T) { + m := New(func() {}) // Create an instance of the Manager struct + + // Add some credentials to the Manager + cred1 := credential.Credential{Subject: "subject1", Type: credential.CredentialType_PIN_OR_PASSWORD, PrivateData: &credential.CredentialPrivateData{DataInternal: []byte("private data")}} + m.credentials.Store(1, cred1) + + // Call the Close function + m.Close() + require.Equal(t, 0, m.credentials.Length()) +} diff --git a/bridge/device/credential/security.go b/bridge/device/credential/security.go new file mode 100644 index 00000000..bb87840a --- /dev/null +++ b/bridge/device/credential/security.go @@ -0,0 +1,70 @@ +/**************************************************************************** + * + * 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 credential + +import ( + "crypto/x509" +) + +type GetCAPool func() []*x509.Certificate + +type CAPoolGetter = interface { + IsValid() bool + GetPool() (*x509.CertPool, error) +} + +type CAPool struct { + origCAPool CAPoolGetter + getCAPool GetCAPool +} + +func MakeCAPool(caPool CAPoolGetter, getCAPool GetCAPool) CAPool { + return CAPool{ + getCAPool: getCAPool, + origCAPool: caPool, + } +} + +func (c CAPool) IsValid() bool { + if c.getCAPool != nil { + return true + } + if c.origCAPool == nil { + return false + } + return c.origCAPool.IsValid() +} + +func (c CAPool) GetPool() (*x509.CertPool, error) { + pool := x509.NewCertPool() + if c.origCAPool != nil && c.origCAPool.IsValid() { + p, err := c.origCAPool.GetPool() + if err != nil { + return nil, err + } + pool = p + } + if c.getCAPool == nil { + return pool, nil + } + for _, ca := range c.getCAPool() { + pool.AddCert(ca) + } + return pool, nil +} diff --git a/bridge/device/credential/security_test.go b/bridge/device/credential/security_test.go new file mode 100644 index 00000000..921b2de4 --- /dev/null +++ b/bridge/device/credential/security_test.go @@ -0,0 +1,54 @@ +/**************************************************************************** + * + * 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 credential_test + +import ( + "crypto/x509" + "testing" + + "github.com/plgd-dev/device/v2/bridge/device/cloud" + "github.com/plgd-dev/device/v2/bridge/device/credential" + "github.com/stretchr/testify/require" +) + +func TestGetPool(t *testing.T) { + getCAPool := func() []*x509.Certificate { + return []*x509.Certificate{{}} + } + + credPool1 := credential.MakeCAPool(nil, getCAPool) + require.True(t, credPool1.IsValid()) + _, err := credPool1.GetPool() + require.NoError(t, err) + + cloudCAPool1 := cloud.MakeCAPool(getCAPool, true) + require.True(t, cloudCAPool1.IsValid()) + credPool2 := credential.MakeCAPool(cloudCAPool1, nil) + require.True(t, credPool2.IsValid()) + _, err = credPool2.GetPool() + require.NoError(t, err) + + credPool3 := credential.MakeCAPool(cloudCAPool1, getCAPool) + require.True(t, credPool3.IsValid()) + _, err = credPool3.GetPool() + require.NoError(t, err) + + credPool4 := credential.MakeCAPool(nil, nil) + require.False(t, credPool4.IsValid()) +} diff --git a/bridge/device/device.go b/bridge/device/device.go index f46fe039..ecaab211 100644 --- a/bridge/device/device.go +++ b/bridge/device/device.go @@ -21,18 +21,22 @@ package device import ( "bytes" "context" + "crypto/x509" "fmt" "github.com/google/uuid" "github.com/plgd-dev/device/v2/bridge/device/cloud" + "github.com/plgd-dev/device/v2/bridge/device/credential" "github.com/plgd-dev/device/v2/bridge/net" "github.com/plgd-dev/device/v2/bridge/resources" cloudResource "github.com/plgd-dev/device/v2/bridge/resources/cloud" resourcesDevice "github.com/plgd-dev/device/v2/bridge/resources/device" "github.com/plgd-dev/device/v2/bridge/resources/discovery" "github.com/plgd-dev/device/v2/bridge/resources/maintenance" + credentialResource "github.com/plgd-dev/device/v2/bridge/resources/secure/credential" "github.com/plgd-dev/device/v2/schema" cloudSchema "github.com/plgd-dev/device/v2/schema/cloud" + credentialSchema "github.com/plgd-dev/device/v2/schema/credential" plgdDevice "github.com/plgd-dev/device/v2/schema/device" maintenanceSchema "github.com/plgd-dev/device/v2/schema/maintenance" plgdResources "github.com/plgd-dev/device/v2/schema/resources" @@ -55,10 +59,11 @@ type Resource interface { } type Device struct { - cfg Config - resources *sync.Map[string, Resource] - cloudManager *cloud.Manager - onDeviceUpdated func(d *Device) + cfg Config + resources *sync.Map[string, Resource] + cloudManager *cloud.Manager + credentialManager *credential.Manager + onDeviceUpdated func(d *Device) } func (d *Device) GetID() uuid.UUID { @@ -81,17 +86,29 @@ func (d *Device) ExportConfig() Config { cfg := d.cfg if d.cloudManager != nil { cfg.Cloud.Config = d.cloudManager.ExportConfig() + } else { + cfg.Cloud.Enabled = false + } + if d.credentialManager != nil { + cfg.Credential.Config = d.credentialManager.ExportConfig() + } else { + cfg.Credential.Enabled = false } return cfg } type OnDeviceUpdated func(d *Device) +type CAPoolGetter interface { + IsValid() bool + GetPool() (*x509.CertPool, error) +} + type OptionsCfg struct { onDeviceUpdated OnDeviceUpdated getAdditionalProperties resourcesDevice.GetAdditionalPropertiesForResponseFunc getCertificates cloud.GetCertificates - caPool cloud.CAPool + caPool CAPoolGetter } type Option func(*OptionsCfg) @@ -114,7 +131,7 @@ func WithGetCertificates(getCertificates cloud.GetCertificates) Option { } } -func WithCAPool(caPool cloud.CAPool) Option { +func WithCAPool(caPool CAPoolGetter) Option { return func(o *OptionsCfg) { o.caPool = caPool } @@ -139,14 +156,22 @@ func New(cfg Config, opts ...Option) (*Device, error) { onDeviceUpdated: o.onDeviceUpdated, } + cloudOpts := []cloud.Option{cloud.WithMaxMessageSize(cfg.MaxMessageSize)} + if cfg.Credential.Enabled { + d.credentialManager = credential.New(func() { + d.onDeviceUpdated(d) + }) + d.AddResource(credentialResource.New(credentialSchema.ResourceURI, d.credentialManager)) + o.caPool = credential.MakeCAPool(o.caPool, d.credentialManager.GetCAPool) + cloudOpts = append(cloudOpts, cloud.WithRemoveCloudCAs(d.credentialManager.RemoveCredentialsBySubjects)) + } if cfg.Cloud.Enabled { - opts := []cloud.Option{cloud.WithMaxMessageSize(cfg.MaxMessageSize)} if o.getCertificates != nil { - opts = append(opts, cloud.WithGetCertificates(o.getCertificates)) + cloudOpts = append(cloudOpts, cloud.WithGetCertificates(o.getCertificates)) } cm, err := cloud.New(d.cfg.ID, func() { d.onDeviceUpdated(d) - }, d.HandleRequest, d.GetLinksFilteredBy, o.caPool, opts...) + }, d.HandleRequest, d.GetLinksFilteredBy, o.caPool, cloudOpts...) if err != nil { return nil, fmt.Errorf("cannot create cloud manager: %w", err) } @@ -162,7 +187,9 @@ func New(cfg Config, opts ...Option) (*Device, error) { d.AddResource(discoverResource) d.AddResource(maintenance.New(maintenanceSchema.ResourceURI, func() { - d.UnregisterFromCloud() + if d.cloudManager != nil { + d.cloudManager.Unregister() + } })) return d, nil @@ -178,10 +205,8 @@ func (d *Device) Init() { } } -func (d *Device) UnregisterFromCloud() { - if d.cloudManager != nil { - d.cloudManager.Unregister() - } +func (d *Device) GetCloudManager() *cloud.Manager { + return d.cloudManager } func (d *Device) Range(f func(resourceHref string, resource Resource) bool) { @@ -268,6 +293,9 @@ func (d *Device) Close() { if d.cloudManager != nil { d.cloudManager.Close() } + if d.credentialManager != nil { + d.credentialManager.Close() + } for _, resource := range d.resources.LoadAndDeleteAll() { resource.Close() } diff --git a/bridge/getDevices_test.go b/bridge/getDevices_test.go index c67e0ce8..6412ca8f 100644 --- a/bridge/getDevices_test.go +++ b/bridge/getDevices_test.go @@ -35,11 +35,11 @@ func TestGetDevices(t *testing.T) { _ = s.Shutdown() }) deviceID1 := uuid.New().String() - d1 := bridgeTest.NewBridgedDevice(t, s, false, deviceID1) + d1 := bridgeTest.NewBridgedDevice(t, s, deviceID1, false, false) deviceID2 := uuid.New().String() - d2 := bridgeTest.NewBridgedDevice(t, s, false, deviceID2) + d2 := bridgeTest.NewBridgedDevice(t, s, deviceID2, false, false) deviceID3 := uuid.New().String() - d3 := bridgeTest.NewBridgedDevice(t, s, false, deviceID3) + d3 := bridgeTest.NewBridgedDevice(t, s, deviceID3, false, false) defer func() { s.DeleteAndCloseDevice(d3.GetID()) s.DeleteAndCloseDevice(d2.GetID()) diff --git a/bridge/getResource_test.go b/bridge/getResource_test.go index 7b50415b..9140d72f 100644 --- a/bridge/getResource_test.go +++ b/bridge/getResource_test.go @@ -42,9 +42,9 @@ func TestGetResource(t *testing.T) { _ = s.Shutdown() }) deviceID1 := uuid.New().String() - d1 := bridgeTest.NewBridgedDevice(t, s, false, deviceID1) + d1 := bridgeTest.NewBridgedDevice(t, s, deviceID1, false, false) deviceID2 := uuid.New().String() - d2 := bridgeTest.NewBridgedDevice(t, s, false, deviceID2) + d2 := bridgeTest.NewBridgedDevice(t, s, deviceID2, false, false) defer func() { s.DeleteAndCloseDevice(d2.GetID()) s.DeleteAndCloseDevice(d1.GetID()) diff --git a/bridge/net/network.go b/bridge/net/network.go index 9c4d0bce..3aaac853 100644 --- a/bridge/net/network.go +++ b/bridge/net/network.go @@ -366,16 +366,31 @@ func New(cfg Config, handler RequestHandler) (*Net, error) { return n, nil } +func (n *Net) getNetwork(cm *net.ControlMessage, localHost, localPort string) string { + if cm != nil { + if cm.Dst.To4() == nil { + return UDP6 + } + return UDP4 + } + p := n.cfg.externalAddressesPort.filterByPort(localPort) + if len(p) == 1 { + return p[0].network + } + ip := gonet.ParseIP(localHost) + if ip.To4() == nil { + return UDP6 + } + return UDP4 +} + func (n *Net) GetEndpoints(cm *net.ControlMessage, localAddr string) schema.Endpoints { - _, localPort, err := gonet.SplitHostPort(localAddr) + localHost, localPort, err := gonet.SplitHostPort(localAddr) if err != nil { log.Printf("cannot get local address: %v", err) return nil } - network := UDP4 - if cm.Dst.To4() == nil { - network = UDP6 - } + network := n.getNetwork(cm, localHost, localPort) filteredByNetwork := n.cfg.externalAddressesPort.filterByNetwork(network) filtered := filteredByNetwork.filterByPort(localPort) if len(filtered) == 0 { diff --git a/bridge/observeResource_test.go b/bridge/observeResource_test.go index 42cd2a2d..9bfea93d 100644 --- a/bridge/observeResource_test.go +++ b/bridge/observeResource_test.go @@ -27,7 +27,7 @@ func TestObserveResource(t *testing.T) { t.Cleanup(func() { _ = s.Shutdown() }) - d := bridgeTest.NewBridgedDevice(t, s, false, uuid.New().String()) + d := bridgeTest.NewBridgedDevice(t, s, uuid.New().String(), false, false) defer func() { s.DeleteAndCloseDevice(d.GetID()) }() diff --git a/bridge/onboardDevice_test.go b/bridge/onboardDevice_test.go index 232db442..41d59890 100644 --- a/bridge/onboardDevice_test.go +++ b/bridge/onboardDevice_test.go @@ -20,11 +20,15 @@ package bridge_test import ( "context" + "crypto/x509" "testing" "time" "github.com/google/uuid" + "github.com/plgd-dev/device/v2/bridge/device" + "github.com/plgd-dev/device/v2/bridge/device/cloud" bridgeTest "github.com/plgd-dev/device/v2/bridge/test" + schemaCredential "github.com/plgd-dev/device/v2/schema/credential" "github.com/plgd-dev/device/v2/test" testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/require" @@ -36,10 +40,17 @@ func TestOnboardDevice(t *testing.T) { _ = s.Shutdown() }) deviceID := uuid.New().String() - d := bridgeTest.NewBridgedDevice(t, s, true, deviceID) + d := bridgeTest.NewBridgedDevice(t, s, deviceID, true, false) defer func() { s.DeleteAndCloseDevice(d.GetID()) }() + deviceIDwithoutCAPool := uuid.New().String() + deviceWithoutCAPool := bridgeTest.NewBridgedDevice(t, s, deviceIDwithoutCAPool, true, true, device.WithCAPool(cloud.MakeCAPool(func() []*x509.Certificate { + return nil + }, false))) + defer func() { + s.DeleteAndCloseDevice(deviceWithoutCAPool.GetID()) + }() cleanup := bridgeTest.RunBridgeService(s) defer func() { errC := cleanup() @@ -52,6 +63,7 @@ func TestOnboardDevice(t *testing.T) { authorizationCode string cloudURL string cloudID string + cloudCA []byte } tests := []struct { name string @@ -79,6 +91,17 @@ func TestOnboardDevice(t *testing.T) { cloudID: "cloudID", }, }, + { + name: "validWithCA", + args: args{ + deviceID: deviceIDwithoutCAPool, + authorizationProvider: "authorizationProvider", + authorizationCode: "authorizationCode", + cloudURL: "coaps+tcp://test:5684", + cloudID: "cloudID", + cloudCA: test.GetRootCApem(t), + }, + }, } c, err := testClient.NewTestSecureClientWithBridgeSupport() @@ -93,6 +116,26 @@ func TestOnboardDevice(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ttCtx, ttCancel := context.WithTimeout(ctx, time.Second) defer ttCancel() + if len(tt.args.cloudCA) > 0 { + err = c.UpdateResource(ctx, tt.args.deviceID, schemaCredential.ResourceURI, schemaCredential.CredentialUpdateRequest{ + Credentials: []schemaCredential.Credential{ + { + Subject: tt.args.cloudID, + Type: schemaCredential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE, + Usage: schemaCredential.CredentialUsage_TRUST_CA, + PublicData: &schemaCredential.CredentialPublicData{ + DataInternal: tt.args.cloudCA, + Encoding: schemaCredential.CredentialPublicDataEncoding_PEM, + }, + }, + }, + }, nil) + require.NoError(t, err) + var res schemaCredential.CredentialResponse + err = c.GetResource(ctx, tt.args.deviceID, schemaCredential.ResourceURI, &res) + require.NoError(t, err) + require.Len(t, res.Credentials, 1) + } 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) @@ -101,6 +144,14 @@ func TestOnboardDevice(t *testing.T) { require.NoError(t, err) err = c.OffboardDevice(ttCtx, tt.args.deviceID) require.NoError(t, err) + if len(tt.args.cloudCA) > 0 { + // remove CA is async + time.Sleep(time.Second) + var res schemaCredential.CredentialResponse + err = c.GetResource(ctx, tt.args.deviceID, schemaCredential.ResourceURI, &res) + require.NoError(t, err) + require.Len(t, res.Credentials, 0) + } }) } } diff --git a/bridge/resources/cloud/resource.go b/bridge/resources/cloud/resource.go index fdc074b8..f5df1c4e 100644 --- a/bridge/resources/cloud/resource.go +++ b/bridge/resources/cloud/resource.go @@ -37,7 +37,7 @@ type Manager interface { func New(uri string, m Manager) *Resource { d := &Resource{} - d.Resource = resources.NewResource(uri, m.Get, m.Post, []string{plgdCloud.ResourceType}, []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_R}) + d.Resource = resources.NewResource(uri, m.Get, m.Post, []string{plgdCloud.ResourceType}, []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_RW}) // don't publish cloud resource to cloud d.PolicyBitMask &= ^resources.PublishToCloud return d diff --git a/bridge/resources/secure/credential/resource.go b/bridge/resources/secure/credential/resource.go new file mode 100644 index 00000000..bedd5745 --- /dev/null +++ b/bridge/resources/secure/credential/resource.go @@ -0,0 +1,44 @@ +/**************************************************************************** + * + * 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 credential + +import ( + "github.com/plgd-dev/device/v2/bridge/net" + "github.com/plgd-dev/device/v2/bridge/resources" + "github.com/plgd-dev/device/v2/schema/credential" + "github.com/plgd-dev/device/v2/schema/interfaces" + "github.com/plgd-dev/go-coap/v3/message/pool" +) + +type Resource struct { + *resources.Resource +} + +type Manager interface { + Get(req *net.Request) (*pool.Message, error) + Post(req *net.Request) (*pool.Message, error) +} + +func New(uri string, m Manager) *Resource { + d := &Resource{} + d.Resource = resources.NewResource(uri, m.Get, m.Post, []string{credential.ResourceType}, []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_RW}) + // don't publish credential resource to cloud + d.PolicyBitMask &= ^resources.PublishToCloud + return d +} diff --git a/bridge/service/service.go b/bridge/service/service.go index 96090c3d..01dc6360 100644 --- a/bridge/service/service.go +++ b/bridge/service/service.go @@ -23,6 +23,7 @@ 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/resources/discovery" @@ -55,7 +56,7 @@ type Device interface { CloseAndDeleteResource(resourceHref string) bool GetResource(resourceHref string) (device.Resource, bool) - UnregisterFromCloud() // unregister device from cloud + GetCloudManager() *cloud.Manager } type Service struct { diff --git a/bridge/test/test.go b/bridge/test/test.go index b7f72fe3..d2578cf3 100644 --- a/bridge/test/test.go +++ b/bridge/test/test.go @@ -61,7 +61,7 @@ func RunBridgeService(s *service.Service) func() error { return cleanup } -func NewBridgedDeviceWithConfig(t *testing.T, s *service.Service, cfg device.Config) service.Device { +func NewBridgedDeviceWithConfig(t *testing.T, s *service.Service, cfg device.Config, opts ...device.Option) service.Device { newDevice := func(di uuid.UUID, piid uuid.UUID) service.Device { cfg.ID = di cfg.ProtocolIndependentID = piid @@ -69,9 +69,12 @@ func NewBridgedDeviceWithConfig(t *testing.T, s *service.Service, cfg device.Con 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 { + deviceOpts := []device.Option{device.WithCAPool(caPool), device.WithGetCertificates(func(deviceID string) []tls.Certificate { return []tls.Certificate{test.GetMfgCertificate(t)} - })) + })} + // allow to override default options + deviceOpts = append(deviceOpts, opts...) + dev, err := device.New(cfg, deviceOpts...) require.NoError(t, err) return dev } @@ -81,23 +84,24 @@ func NewBridgedDeviceWithConfig(t *testing.T, s *service.Service, cfg device.Con return d } -func makeDeviceConfig(id uuid.UUID, cloudEnabled bool) device.Config { +func makeDeviceConfig(id uuid.UUID, cloudEnabled bool, credentialEnabled bool) device.Config { cfg := device.Config{ ID: id, Name: "bridged-device", ResourceTypes: []string{"oic.d.virtual"}, MaxMessageSize: 1024 * 256, } + cfg.Cloud.Enabled = cloudEnabled if cloudEnabled { - cfg.Cloud.Enabled = true cfg.Cloud.CloudID = test.CloudSID() } + cfg.Credential.Enabled = credentialEnabled return cfg } -func NewBridgedDevice(t *testing.T, s *service.Service, cloudEnabled bool, id string) service.Device { +func NewBridgedDevice(t *testing.T, s *service.Service, id string, cloudEnabled bool, credentialEnabled bool, opts ...device.Option) service.Device { u, err := uuid.Parse(id) require.NoError(t, err) - cfg := makeDeviceConfig(u, cloudEnabled) - return NewBridgedDeviceWithConfig(t, s, cfg) + cfg := makeDeviceConfig(u, cloudEnabled, credentialEnabled) + return NewBridgedDeviceWithConfig(t, s, cfg, opts...) } diff --git a/bridge/updateResource_test.go b/bridge/updateResource_test.go index 0fb8362a..d0f92947 100644 --- a/bridge/updateResource_test.go +++ b/bridge/updateResource_test.go @@ -56,7 +56,7 @@ func TestUpdateResource(t *testing.T) { t.Cleanup(func() { _ = s.Shutdown() }) - d := bridgeTest.NewBridgedDevice(t, s, false, uuid.New().String()) + d := bridgeTest.NewBridgedDevice(t, s, uuid.New().String(), false, false) defer func() { s.DeleteAndCloseDevice(d.GetID()) }() diff --git a/cmd/ocfbridge/config.go b/cmd/ocfbridge/config.go index 8887678b..2ce29e8d 100644 --- a/cmd/ocfbridge/config.go +++ b/cmd/ocfbridge/config.go @@ -28,6 +28,10 @@ type CloudConfig struct { TLS TLSConfig `yaml:"tls" json:"tls"` } +type CredentialConfig struct { + Enabled bool `yaml:"enabled" json:"enabled" description:"enable credential manager"` +} + func (c *CloudConfig) Validate() error { if c.Enabled { return c.TLS.Validate() @@ -37,9 +41,10 @@ func (c *CloudConfig) Validate() error { type Config struct { service.Config `yaml:",inline"` - Cloud CloudConfig `yaml:"cloud" json:"cloud"` - NumGeneratedBridgedDevices int `yaml:"numGeneratedBridgedDevices"` - NumResourcesPerDevice int `yaml:"numResourcesPerDevice"` + Cloud CloudConfig `yaml:"cloud" json:"cloud"` + Credential CredentialConfig `yaml:"credential" json:"credential"` + 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 2f45c4e4..74afe297 100644 --- a/cmd/ocfbridge/config.yaml +++ b/cmd/ocfbridge/config.yaml @@ -7,6 +7,8 @@ apis: - "[::1]:15683" maxMessageSize: 2097152 cloud: - enabled: false -numGeneratedBridgedDevices: 100 -numResourcesPerDevice: 256 + enabled: true +credential: + enabled: true +numGeneratedBridgedDevices: 3 +numResourcesPerDevice: 16 \ No newline at end of file diff --git a/cmd/ocfbridge/main.go b/cmd/ocfbridge/main.go index e73e1e62..a9c03fb7 100644 --- a/cmd/ocfbridge/main.go +++ b/cmd/ocfbridge/main.go @@ -147,10 +147,17 @@ 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) +func getCloudTLS(cfg CloudConfig, credentialEnabled bool) (cloud.CAPool, *tls.Certificate, error) { + var ca []*x509.Certificate + var err error + if cfg.TLS.CAPoolPath == "" && !credentialEnabled { + return cloud.CAPool{}, nil, fmt.Errorf("cannot load ca: caPoolPath is empty") + } + if cfg.TLS.CAPoolPath != "" { + ca, err = pkgX509.ReadPemCertificates(cfg.TLS.CAPoolPath) + if err != nil { + return cloud.CAPool{}, nil, fmt.Errorf("cannot load ca('%v'): %w", cfg.TLS.CAPoolPath, err) + } } caPool := cloud.MakeCAPool(func() []*x509.Certificate { return ca @@ -162,7 +169,7 @@ func getCloudTLS(cfg CloudConfig) (cloud.CAPool, *tls.Certificate, error) { 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 cloud.CAPool{}, nil, fmt.Errorf("cannot load cert(%v, %v): %w", cfg.TLS.CertPath, cfg.TLS.KeyPath, err) } return caPool, &cert, nil } @@ -203,7 +210,7 @@ func main() { }), } if cfg.Cloud.Enabled { - caPool, cert, errC := getCloudTLS(cfg.Cloud) + caPool, cert, errC := getCloudTLS(cfg.Cloud, cfg.Credential.Enabled) if errC != nil { panic(errC) } @@ -226,6 +233,9 @@ func main() { Cloud: device.CloudConfig{ Enabled: cfg.Cloud.Enabled, }, + Credential: device.CredentialConfig{ + Enabled: cfg.Credential.Enabled, + }, }, opts...) if errD != nil { panic(errD) diff --git a/schema/credential/credential.go b/schema/credential/credential.go index 40b09985..c0f8ca75 100644 --- a/schema/credential/credential.go +++ b/schema/credential/credential.go @@ -31,17 +31,17 @@ const ( ) type Credential struct { - ID int `json:"credid,omitempty"` - Type CredentialType `json:"credtype"` - Subject string `json:"subjectuuid"` - Usage CredentialUsage `json:"credusage,omitempty"` - SupportedRefreshMethods []CredentialRefreshMethod `json:"crms,omitempty"` - OptionalData *CredentialOptionalData `json:"optionaldata,omitempty"` - Period string `json:"period,omitempty"` - PrivateData *CredentialPrivateData `json:"privatedata,omitempty"` - PublicData *CredentialPublicData `json:"publicdata,omitempty"` - RoleID *CredentialRoleID `json:"roleid,omitempty"` - Tag string `json:"tag,omitempty"` + ID int `json:"credid,omitempty" yaml:"id,omitempty"` + Type CredentialType `json:"credtype" yaml:"type"` + Subject string `json:"subjectuuid" yaml:"subject"` + Usage CredentialUsage `json:"credusage,omitempty" yaml:"usage,omitempty"` + SupportedRefreshMethods []CredentialRefreshMethod `json:"crms,omitempty" yaml:"supportedRefreshMethods,omitempty"` + OptionalData *CredentialOptionalData `json:"optionaldata,omitempty" yaml:"optionalData,omitempty"` + Period string `json:"period,omitempty" yaml:"period,omitempty"` + PrivateData *CredentialPrivateData `json:"privatedata,omitempty" yaml:"privateData,omitempty"` + PublicData *CredentialPublicData `json:"publicdata,omitempty" yaml:"publicData,omitempty"` + RoleID *CredentialRoleID `json:"roleid,omitempty" yaml:"roleID,omitempty"` + Tag string `json:"tag,omitempty" yaml:"tag,omitempty"` } type CredentialType uint8 @@ -117,9 +117,9 @@ const ( ) type CredentialOptionalData struct { - DataInternal interface{} `json:"data"` - Encoding CredentialOptionalDataEncoding `json:"encoding"` - IsRevoked bool `json:"revstat"` + DataInternal interface{} `json:"data" yaml:"data"` + Encoding CredentialOptionalDataEncoding `json:"encoding" yaml:"encoding"` + IsRevoked bool `json:"revstat" yaml:"isRevoked,omitempty"` } func toByte(v interface{}) []byte { @@ -181,8 +181,8 @@ const ( ) type CredentialPublicData struct { - DataInternal interface{} `json:"data"` - Encoding CredentialPublicDataEncoding `json:"encoding"` + DataInternal interface{} `json:"data" yaml:"data"` + Encoding CredentialPublicDataEncoding `json:"encoding" yaml:"encoding"` } func (c CredentialPublicData) Data() []byte { @@ -202,16 +202,16 @@ const ( ) type CredentialRoleID struct { - Authority string `json:"authority,omitempty"` - Role string `json:"role,omitempty"` + Authority string `json:"authority,omitempty" yaml:"authority,omitempty"` + Role string `json:"role,omitempty" yaml:"role,omitempty"` } type CredentialResponse struct { - ResourceOwner string `json:"rowneruuid"` - Interfaces []string `json:"if"` - ResourceTypes []string `json:"rt"` - Name string `json:"n"` - Credentials []Credential `json:"creds"` + ResourceOwner string `json:"rowneruuid" yaml:"resourceOwner,omitempty"` + Interfaces []string `json:"if,omitempty" yaml:"-"` + ResourceTypes []string `json:"rt,omitempty" yaml:"-"` + Name string `json:"n,omitempty" yaml:"name,omitempty"` + Credentials []Credential `json:"creds" yaml:"creds"` } type CredentialUpdateRequest struct { diff --git a/schema/link.go b/schema/link.go index 131430bc..8100cef3 100644 --- a/schema/link.go +++ b/schema/link.go @@ -50,12 +50,12 @@ type ResourceLinks []ResourceLink // https://openconnectivity.org/specs/OCF_Core_Specification_v2.0.0.pdf type Policy struct { BitMask BitMask `json:"bm"` - UDPPort uint16 `json:"port"` - TCPPort uint16 `json:"x.org.iotivity.tcp"` - TCPTLSPort uint16 `json:"x.org.iotivity.tls"` + UDPPort uint16 `json:"port,omitempty"` + TCPPort uint16 `json:"x.org.iotivity.tcp,omitempty"` + TCPTLSPort uint16 `json:"x.org.iotivity.tls,omitempty"` // Secured is true if the resource is only available via an encrypted connection. - Secured bool `json:"sec"` + Secured *bool `json:"sec,omitempty"` } // Endpoint is defined on the line 2439 and 1892, Priority on 2434 of the Core specification: @@ -243,7 +243,7 @@ func (r ResourceLink) patchEndpoint(addr kitNet.Addr, deviceEndpoints Endpoints) } r.Endpoints = make([]Endpoint, 0, 4) if r.Policy.UDPPort != 0 { - if r.Policy.Secured { + if r.Policy.Secured != nil && *r.Policy.Secured { r.Endpoints = append(r.Endpoints, udpTlsEndpoint(addr.SetPort(r.Policy.UDPPort))) } else { r.Endpoints = append(r.Endpoints, udpEndpoint(addr.SetPort(r.Policy.UDPPort))) diff --git a/schema/link_test.go b/schema/link_test.go index 54c4f371..efa39c83 100644 --- a/schema/link_test.go +++ b/schema/link_test.go @@ -267,12 +267,13 @@ func TestResourceLinkPatchEndpoint(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + secured := tt.args.secured r := schema.ResourceLink{ Policy: &schema.Policy{ UDPPort: 5683, TCPPort: 5683, TCPTLSPort: 5684, - Secured: tt.args.secured, + Secured: &secured, }, } got := r.PatchEndpoint(tt.args.addr, nil) diff --git a/test/signer.go b/test/signer.go index 1fb68cee..c6cd2a07 100644 --- a/test/signer.go +++ b/test/signer.go @@ -7,11 +7,11 @@ import ( "time" "github.com/plgd-dev/device/v2/client/core" - "github.com/plgd-dev/kit/v2/security" + pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" ) func NewTestSigner() (core.CertificateSigner, error) { - identityIntermediateCA, err := security.ParseX509FromPEM(IdentityIntermediateCA) + identityIntermediateCA, err := pkgX509.ParsePemCertificates(IdentityIntermediateCA) if err != nil { return nil, err } diff --git a/test/test.go b/test/test.go index 70492ef2..b8cc6033 100644 --- a/test/test.go +++ b/test/test.go @@ -23,6 +23,7 @@ import ( "crypto/x509" "fmt" "os" + "path/filepath" "strings" "sync/atomic" "testing" @@ -293,10 +294,17 @@ func CloudSID() string { return os.Getenv("CLOUD_SID") } -func GetRootCA(t *testing.T) []*x509.Certificate { +func GetRootCApem(t *testing.T) []byte { certPath := os.Getenv("ROOT_CA_CRT") require.NotEmpty(t, certPath) - cas, err := pkgX509.ReadPemCertificates(certPath) + data, err := os.ReadFile(filepath.Clean(certPath)) + require.NoError(t, err) + return data +} + +func GetRootCA(t *testing.T) []*x509.Certificate { + certPem := GetRootCApem(t) + cas, err := pkgX509.ParsePemCertificates(certPem) require.NoError(t, err) return cas }