• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / iot-manager / 1427008803

26 Aug 2024 08:20AM UTC coverage: 87.172% (-0.4%) from 87.577%
1427008803

push

gitlab-ci

web-flow
Merge pull request #298 from alfrunes/MEN-7478

fix(iot-core): Incosistent serialization format for device private key

42 of 54 new or added lines in 2 files covered. (77.78%)

6 existing lines in 1 file now uncovered.

3255 of 3734 relevant lines covered (87.17%)

11.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.05
/client/iotcore/client.go
1
// Copyright 2024 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package iotcore
16

17
import (
18
        "context"
19
        "crypto/ecdsa"
20
        "encoding/json"
21
        "errors"
22
        "fmt"
23
        "net/http"
24
        "strings"
25
        "time"
26

27
        "github.com/aws/aws-sdk-go-v2/aws"
28
        awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
29
        "github.com/aws/aws-sdk-go-v2/config"
30
        "github.com/aws/aws-sdk-go-v2/credentials"
31
        "github.com/aws/aws-sdk-go-v2/service/iot"
32
        "github.com/aws/aws-sdk-go-v2/service/iot/types"
33
        "github.com/aws/aws-sdk-go-v2/service/iotdataplane"
34

35
        "github.com/mendersoftware/iot-manager/crypto"
36
        "github.com/mendersoftware/iot-manager/model"
37
)
38

39
var (
40
        ErrDeviceNotFound            = errors.New("device not found")
41
        ErrDeviceIncosistent         = errors.New("device is not consistent")
42
        ErrThingPrincipalNotDetached = errors.New(
43
                "giving up on waiting for Thing principal being detached")
44
)
45

46
const (
47
        endpointType = "iot:Data-ATS"
48
        // wait for Detach Thing Principal Operation
49
        // check 5 times every 2 seconds, which will give us 10s wait
50
        detachThingPrincipalWaitSleep      = 2 * time.Second
51
        detachThingPrincipalWaitMaxRetries = 4 // looks like 4, but it is 5, we're counting from 0
52
)
53

54
//nolint:lll
55
//go:generate ../../utils/mockgen.sh
56
type Client interface {
57
        GetDeviceShadow(ctx context.Context, creds model.AWSCredentials, id string) (*DeviceShadow, error)
58
        UpdateDeviceShadow(
59
                ctx context.Context,
60
                creds model.AWSCredentials,
61
                deviceID string,
62
                update DeviceShadowUpdate,
63
        ) (*DeviceShadow, error)
64
        GetDevice(ctx context.Context, creds model.AWSCredentials, deviceID string) (*Device, error)
65
        UpsertDevice(ctx context.Context, creds model.AWSCredentials, deviceID string, device *Device, policy string) (*Device, error)
66
        DeleteDevice(ctx context.Context, creds model.AWSCredentials, deviceID string) error
67
}
68

69
type client struct{}
70

71
func NewClient() Client {
11✔
72
        return &client{}
11✔
73
}
11✔
74

75
func getAWSConfig(creds model.AWSCredentials) (*aws.Config, error) {
88✔
76
        err := creds.Validate()
88✔
77
        if err != nil {
88✔
78
                return nil, err
×
79
        }
×
80

81
        appCreds := credentials.NewStaticCredentialsProvider(
88✔
82
                *creds.AccessKeyID,
88✔
83
                string(*creds.SecretAccessKey),
88✔
84
                "",
88✔
85
        )
88✔
86
        cfg, err := config.LoadDefaultConfig(context.TODO(),
88✔
87
                config.WithRegion(*creds.Region),
88✔
88
                config.WithCredentialsProvider(appCreds),
88✔
89
        )
88✔
90
        return &cfg, err
88✔
91
}
92

93
func (c *client) GetDevice(
94
        ctx context.Context,
95
        creds model.AWSCredentials,
96
        deviceID string,
97
) (*Device, error) {
56✔
98
        cfg, err := getAWSConfig(creds)
56✔
99
        if err != nil {
56✔
100
                return nil, err
×
101
        }
×
102
        svc := iot.NewFromConfig(*cfg)
56✔
103

56✔
104
        resp, err := svc.DescribeThing(ctx,
56✔
105
                &iot.DescribeThingInput{
56✔
106
                        ThingName: aws.String(deviceID),
56✔
107
                })
56✔
108

56✔
109
        var device *Device
56✔
110
        var respListThingPrincipals *iot.ListThingPrincipalsOutput
56✔
111
        if err == nil {
93✔
112
                device = &Device{
37✔
113
                        ID:      *resp.ThingId,
37✔
114
                        Name:    *resp.ThingName,
37✔
115
                        Version: resp.Version,
37✔
116
                        Status:  StatusDisabled,
37✔
117
                }
37✔
118
                respListThingPrincipals, err = svc.ListThingPrincipals(ctx,
37✔
119
                        &iot.ListThingPrincipalsInput{
37✔
120
                                ThingName: aws.String(deviceID),
37✔
121
                        })
37✔
122
        }
37✔
123

124
        if err == nil {
93✔
125
                if len(respListThingPrincipals.Principals) > 1 {
37✔
126
                        err = ErrDeviceIncosistent
×
127
                }
×
128
        }
129

130
        if err == nil {
93✔
131
                for _, principal := range respListThingPrincipals.Principals {
74✔
132
                        parts := strings.Split(principal, "/")
37✔
133
                        certificateID := parts[len(parts)-1]
37✔
134

37✔
135
                        cert, err := svc.DescribeCertificate(ctx, &iot.DescribeCertificateInput{
37✔
136
                                CertificateId: aws.String(certificateID),
37✔
137
                        })
37✔
138
                        if err != nil {
37✔
139
                                return nil, err
×
140
                        }
×
141
                        device.CertificateID = certificateID
37✔
142
                        if cert.CertificateDescription.Status == types.CertificateStatusActive {
53✔
143
                                device.Status = StatusEnabled
16✔
144
                        }
16✔
145
                }
146
        }
147

148
        var notFoundErr *types.ResourceNotFoundException
56✔
149
        if errors.As(err, &notFoundErr) {
74✔
150
                err = ErrDeviceNotFound
18✔
151
        }
18✔
152

153
        return device, err
56✔
154
}
155

156
func (c *client) UpsertDevice(ctx context.Context,
157
        creds model.AWSCredentials,
158
        deviceID string,
159
        device *Device,
160
        policy string,
161
) (*Device, error) {
12✔
162
        cfg, err := getAWSConfig(creds)
12✔
163
        if err != nil {
12✔
164
                return nil, err
×
165
        }
×
166
        svc := iot.NewFromConfig(*cfg)
12✔
167

12✔
168
        awsDevice, err := c.GetDevice(ctx, creds, deviceID)
12✔
169
        if err == nil && awsDevice != nil {
16✔
170
                cert, err := svc.DescribeCertificate(ctx, &iot.DescribeCertificateInput{
4✔
171
                        CertificateId: aws.String(awsDevice.CertificateID),
4✔
172
                })
4✔
173
                if err == nil {
8✔
174
                        newStatus := types.CertificateStatusInactive
4✔
175
                        awsDevice.Status = StatusDisabled
4✔
176
                        if device.Status == StatusEnabled {
7✔
177
                                newStatus = types.CertificateStatusActive
3✔
178
                                awsDevice.Status = StatusEnabled
3✔
179
                        }
3✔
180

181
                        if cert.CertificateDescription.Status != newStatus {
8✔
182
                                paramsUpdateCertificate := &iot.UpdateCertificateInput{
4✔
183
                                        CertificateId: aws.String(awsDevice.CertificateID),
4✔
184
                                        NewStatus:     types.CertificateStatus(newStatus),
4✔
185
                                }
4✔
186
                                _, err = svc.UpdateCertificate(ctx, paramsUpdateCertificate)
4✔
187
                        }
4✔
188
                }
189
                return awsDevice, err
4✔
190
        } else if !errors.Is(err, ErrDeviceNotFound) {
8✔
NEW
191
                return nil, fmt.Errorf("unexpected error getting the device: %w", err)
×
UNCOV
192
        }
×
193

194
        var privKey *ecdsa.PrivateKey
8✔
195
        privKey, err = crypto.NewPrivateKey()
8✔
196
        if err != nil {
8✔
NEW
197
                return nil, fmt.Errorf("failed to generate key for device: %w", err)
×
UNCOV
198
        }
×
199

200
        var csr []byte
8✔
201
        csr, err = crypto.NewCertificateSigningRequest(deviceID, privKey)
8✔
202
        if err != nil {
8✔
NEW
203
                return nil, fmt.Errorf("error creating certificate signing request: %w", err)
×
NEW
204
        }
×
205

206
        var resp *iot.CreateThingOutput
8✔
207
        resp, err = svc.CreateThing(ctx,
8✔
208
                &iot.CreateThingInput{
8✔
209
                        ThingName: aws.String(deviceID),
8✔
210
                })
8✔
211
        if err != nil {
8✔
NEW
212
                return nil, fmt.Errorf("failed to create Thing: %w", err)
×
UNCOV
213
        }
×
214

215
        var respCert *iot.CreateCertificateFromCsrOutput
8✔
216
        respCert, err = svc.CreateCertificateFromCsr(ctx,
8✔
217
                &iot.CreateCertificateFromCsrInput{
8✔
218
                        CertificateSigningRequest: aws.String(string(csr)),
8✔
219
                        SetAsActive:               *aws.Bool(device.Status == StatusEnabled),
8✔
220
                })
8✔
221
        if err != nil {
8✔
NEW
222
                return nil, err
×
UNCOV
223
        }
×
224

225
        endpointOutput, err := svc.DescribeEndpoint(ctx, &iot.DescribeEndpointInput{
8✔
226
                EndpointType: aws.String(endpointType),
8✔
227
        })
8✔
228
        if err != nil {
8✔
229
                return nil, err
×
230
        }
×
231

232
        _, err = svc.AttachPolicy(ctx,
8✔
233
                &iot.AttachPolicyInput{
8✔
234
                        PolicyName: aws.String(policy),
8✔
235
                        Target:     respCert.CertificateArn,
8✔
236
                })
8✔
237
        if err != nil {
8✔
NEW
238
                return nil, fmt.Errorf("failed to attach device certificate policy: %w", err)
×
UNCOV
239
        }
×
240

241
        _, err = svc.AttachThingPrincipal(ctx,
8✔
242
                &iot.AttachThingPrincipalInput{
8✔
243
                        Principal: respCert.CertificateArn,
8✔
244
                        ThingName: aws.String(deviceID),
8✔
245
                })
8✔
246
        if err != nil {
8✔
NEW
247
                return nil, fmt.Errorf("failed to attach thing principal: %w", err)
×
UNCOV
248
        }
×
249

250
        var deviceResp *Device
8✔
251
        pkeyPEM, err := crypto.PrivateKeyToPem(privKey)
8✔
252
        if err != nil {
8✔
NEW
253
                return nil, fmt.Errorf("failed to serialize private key: %w", err)
×
NEW
254
        }
×
255
        deviceResp = &Device{
8✔
256
                ID:          *resp.ThingId,
8✔
257
                Name:        *resp.ThingName,
8✔
258
                Status:      device.Status,
8✔
259
                PrivateKey:  string(pkeyPEM),
8✔
260
                Certificate: *respCert.CertificatePem,
8✔
261
                Endpoint:    endpointOutput.EndpointAddress,
8✔
262
        }
8✔
263
        return deviceResp, err
8✔
264
}
265

266
func (c *client) DeleteDevice(
267
        ctx context.Context,
268
        creds model.AWSCredentials,
269
        deviceID string,
270
) error {
12✔
271
        cfg, err := getAWSConfig(creds)
12✔
272
        if err != nil {
12✔
273
                return err
×
274
        }
×
275
        svc := iot.NewFromConfig(*cfg)
12✔
276

12✔
277
        respDescribe, err := svc.DescribeThing(ctx,
12✔
278
                &iot.DescribeThingInput{
12✔
279
                        ThingName: aws.String(deviceID),
12✔
280
                })
12✔
281

12✔
282
        var respListThingPrincipals *iot.ListThingPrincipalsOutput
12✔
283
        if err == nil {
21✔
284
                respListThingPrincipals, err = svc.ListThingPrincipals(ctx,
9✔
285
                        &iot.ListThingPrincipalsInput{
9✔
286
                                ThingName: aws.String(deviceID),
9✔
287
                        })
9✔
288
        }
9✔
289

290
        if err == nil {
21✔
291
                for _, principal := range respListThingPrincipals.Principals {
18✔
292
                        _, err := svc.DetachThingPrincipal(ctx,
9✔
293
                                &iot.DetachThingPrincipalInput{
9✔
294
                                        Principal: aws.String(principal),
9✔
295
                                        ThingName: aws.String(deviceID),
9✔
296
                                })
9✔
297
                        var certificateID string
9✔
298
                        if err == nil {
18✔
299
                                parts := strings.SplitAfter(principal, "/")
9✔
300
                                certificateID = parts[len(parts)-1]
9✔
301

9✔
302
                                _, err = svc.UpdateCertificate(ctx,
9✔
303
                                        &iot.UpdateCertificateInput{
9✔
304
                                                CertificateId: aws.String(certificateID),
9✔
305
                                                NewStatus:     types.CertificateStatusInactive,
9✔
306
                                        })
9✔
307
                        }
9✔
308
                        if err == nil {
18✔
309
                                _, err = svc.DeleteCertificate(ctx,
9✔
310
                                        &iot.DeleteCertificateInput{
9✔
311
                                                CertificateId: aws.String(certificateID),
9✔
312
                                                ForceDelete:   *aws.Bool(true),
9✔
313
                                        })
9✔
314
                        }
9✔
315
                        if err != nil {
9✔
316
                                break
×
317
                        }
318
                }
319
        }
320

321
        if err == nil && len(respListThingPrincipals.Principals) > 0 {
21✔
322
                // wait for DetachThingPrincipal operation to complete
9✔
323
                // this operation is asynchronous, so wait couple of seconds
9✔
324
                for retries := 0; retries <= detachThingPrincipalWaitMaxRetries; retries++ {
54✔
325
                        respListThingPrincipals, err = svc.ListThingPrincipals(ctx,
45✔
326
                                &iot.ListThingPrincipalsInput{
45✔
327
                                        ThingName: aws.String(deviceID),
45✔
328
                                })
45✔
329
                        if err != nil {
45✔
330
                                break
×
331
                        }
332
                        if len(respListThingPrincipals.Principals) > 0 {
50✔
333
                                time.Sleep(detachThingPrincipalWaitSleep)
5✔
334
                        }
5✔
335
                }
336
                // Thing Principle still not detached; return error
337
                if respListThingPrincipals != nil && len(respListThingPrincipals.Principals) > 0 {
10✔
338
                        return ErrThingPrincipalNotDetached
1✔
339
                }
1✔
340
        }
341

342
        if err == nil {
19✔
343
                _, err = svc.DeleteThing(ctx,
8✔
344
                        &iot.DeleteThingInput{
8✔
345
                                ThingName:       aws.String(deviceID),
8✔
346
                                ExpectedVersion: aws.Int64(respDescribe.Version),
8✔
347
                        })
8✔
348
        }
8✔
349

350
        if err != nil {
14✔
351
                var notFoundErr *types.ResourceNotFoundException
3✔
352
                if errors.As(err, &notFoundErr) {
5✔
353
                        err = ErrDeviceNotFound
2✔
354
                }
2✔
355
                return err
3✔
356
        }
357

358
        return err
8✔
359
}
360

361
func (c *client) GetDeviceShadow(
362
        ctx context.Context,
363
        creds model.AWSCredentials,
364
        deviceID string,
365
) (*DeviceShadow, error) {
6✔
366
        cfg, err := getAWSConfig(creds)
6✔
367
        if err != nil {
6✔
368
                return nil, err
×
369
        }
×
370
        svc := iotdataplane.NewFromConfig(*cfg)
6✔
371
        shadow, err := svc.GetThingShadow(
6✔
372
                ctx,
6✔
373
                &iotdataplane.GetThingShadowInput{
6✔
374
                        ThingName: aws.String(deviceID),
6✔
375
                },
6✔
376
        )
6✔
377
        if err != nil {
10✔
378
                var httpResponseErr *awshttp.ResponseError
4✔
379
                if errors.As(err, &httpResponseErr) {
8✔
380
                        if httpResponseErr.HTTPStatusCode() == http.StatusNotFound {
8✔
381
                                _, errDevice := c.GetDevice(ctx, creds, deviceID)
4✔
382
                                if errDevice == ErrDeviceNotFound {
6✔
383
                                        err = ErrDeviceNotFound
2✔
384
                                } else {
4✔
385
                                        return &DeviceShadow{
2✔
386
                                                Payload: model.DeviceState{
2✔
387
                                                        Desired:  map[string]interface{}{},
2✔
388
                                                        Reported: make(map[string]interface{}),
2✔
389
                                                },
2✔
390
                                        }, nil
2✔
391
                                }
2✔
392
                        }
393
                }
394
                return nil, err
2✔
395
        }
396
        var devShadow DeviceShadow
2✔
397
        err = json.Unmarshal(shadow.Payload, &devShadow)
2✔
398
        if err != nil {
2✔
399
                return nil, err
×
400
        }
×
401
        return &devShadow, nil
2✔
402
}
403

404
func (c *client) UpdateDeviceShadow(
405
        ctx context.Context,
406
        creds model.AWSCredentials,
407
        deviceID string,
408
        update DeviceShadowUpdate,
409
) (*DeviceShadow, error) {
2✔
410
        cfg, err := getAWSConfig(creds)
2✔
411
        if err != nil {
2✔
412
                return nil, err
×
413
        }
×
414
        svc := iotdataplane.NewFromConfig(*cfg)
2✔
415
        payloadUpdate, err := json.Marshal(update)
2✔
416
        if err != nil {
2✔
417
                return nil, err
×
418
        }
×
419
        updated, err := svc.UpdateThingShadow(
2✔
420
                ctx,
2✔
421
                &iotdataplane.UpdateThingShadowInput{
2✔
422
                        Payload:   payloadUpdate,
2✔
423
                        ThingName: aws.String(deviceID),
2✔
424
                },
2✔
425
        )
2✔
426
        if err != nil {
2✔
427
                var httpResponseErr *awshttp.ResponseError
×
428
                if errors.As(err, &httpResponseErr) {
×
429
                        if httpResponseErr.HTTPStatusCode() == http.StatusNotFound {
×
430
                                err = ErrDeviceNotFound
×
431
                        }
×
432
                }
433
                return nil, err
×
434
        }
435
        var shadow DeviceShadow
2✔
436
        err = json.Unmarshal(updated.Payload, &shadow)
2✔
437
        if err != nil {
2✔
438
                return nil, err
×
439
        }
×
440
        return &shadow, nil
2✔
441
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc