File: hardware_identity.go

package info (click to toggle)
snapd 2.74.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 81,428 kB
  • sloc: sh: 16,966; ansic: 16,788; python: 11,332; makefile: 1,897; exp: 190; awk: 58; xml: 22
file content (223 lines) | stat: -rw-r--r-- 6,705 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2025 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package asserts

import (
	"bytes"
	"crypto"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/rsa"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
	"errors"
	"fmt"
	"math/big"

	"golang.org/x/crypto/sha3"
)

// HardwareIdentity holds a hardware identity assertion, which is a statement
// that verifies the identity of a physical piece of hardware
type HardwareIdentity struct {
	assertionBase

	hardwareIDKeySha3384 string
	hardwareKey          crypto.PublicKey
}

// IssuerID returns the Snap Store account of the issuer and signer of the voucher.
// This can be used to ensure the voucher has originated from a suitable source
// (e.g. the manufacturer or brand).
func (h *HardwareIdentity) IssuerID() string {
	return h.HeaderString("issuer-id")
}

// Manufacturer returns the name of the device manufacturer.
func (h *HardwareIdentity) Manufacturer() string {
	return h.HeaderString("manufacturer")
}

// HardwareName returns the designation of the hardware device model.
func (h *HardwareIdentity) HardwareName() string {
	return h.HeaderString("hardware-name")
}

// HardwareID returns the identification of the individual hardware device.
// It is not called a serial number as there is no strict requirement that
// this value is the same as the serial number in a resulting serial assertion on the device.
func (h *HardwareIdentity) HardwareID() string {
	return h.HeaderString("hardware-id")
}

// HardwareIDKey returns hardware identity public key,
// which is the body of a parsable form (PEM) as defined by RFC7468ยง13.
func (h *HardwareIdentity) HardwareIDKey() crypto.PublicKey {
	return h.hardwareKey
}

// HardwareIDKeySha3384 returns the hash of the public key binary data encoded in the hardware-id-key header.
// This is included as it is used as part of the primary key for the assertion.
func (h *HardwareIdentity) HardwareIDKeySha3384() string {
	return h.hardwareIDKeySha3384
}

func assembleHardwareIdentity(assert assertionBase) (Assertion, error) {
	issuerID, err := checkStringMatches(assert.headers, "issuer-id", validAccountID)
	if err != nil {
		return nil, err
	}

	if issuerID != assert.HeaderString("authority-id") {
		return nil, errors.New("issuer id must match authority id")
	}

	_, err = checkNotEmptyString(assert.headers, "manufacturer")
	if err != nil {
		return nil, err
	}

	_, err = checkStringMatches(assert.headers, "hardware-name", validModel)
	if err != nil {
		return nil, err
	}

	_, err = checkNotEmptyString(assert.headers, "hardware-id")
	if err != nil {
		return nil, err
	}

	hardwareIDKey, err := checkNotEmptyString(assert.headers, "hardware-id-key")
	if err != nil {
		return nil, err
	}

	pubKey, err := checkStringIsPEM([]byte(hardwareIDKey))
	if err != nil {
		return nil, err
	}

	hardwareIDKeySha3384 := assert.HeaderString("hardware-id-key-sha3-384")

	hash := sha3.New384()
	hash.Write([]byte(hardwareIDKey))
	hashed := hash.Sum(nil)

	// no error can be returned because the hash is initialized beforehand
	hashedHardwareIDKey, _ := EncodeDigest(crypto.SHA3_384, hashed)

	if hardwareIDKeySha3384 != hashedHardwareIDKey {
		return nil, fmt.Errorf("hardware id key does not match provided hash")
	}

	return &HardwareIdentity{
		assertionBase:        assert,
		hardwareIDKeySha3384: hardwareIDKeySha3384,
		hardwareKey:          pubKey,
	}, nil
}

// checkStringIsPEM checks if string is the body of a parsable form (PEM).
// It assumes the BEGIN and END lines are omitted. The function returns a
// non-nil error if the string fails to be a PEM.
func checkStringIsPEM(data []byte) (crypto.PublicKey, error) {
	// add begin and end lines to PEM body
	var bb bytes.Buffer
	bb.WriteString("-----BEGIN PUBLIC KEY-----\n")
	bb.Write(data)
	bb.WriteString("\n-----END PUBLIC KEY-----\n")

	block, _ := pem.Decode(bb.Bytes())
	if block == nil {
		return nil, errors.New("no PEM block was found")
	}

	pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("cannot parse public key: %v", err)
	}

	return pubKey, nil
}

// VerifyNonceSignature checks the signature of a given nonce against the hardware id key.
// It is used by the model service to verify the request-id.
// It currently supports key with algorithms RSA, ECDSA, and ED25519.
// The hash algorithm used is also specified as a parameter.
func (h *HardwareIdentity) VerifyNonceSignature(nonce, signature []byte, hashAlg crypto.Hash) error {
	if !hashAlg.Available() {
		return fmt.Errorf("unsupported hash type: %s", hashAlg.String())
	}

	hash := hashAlg.New()

	hash.Write(nonce)
	hashed := hash.Sum(nil)

	switch keyType := h.hardwareKey.(type) {
	case *rsa.PublicKey:
		return verifySignatureWithRSAKey(hashed, signature, h.hardwareKey.(*rsa.PublicKey), hashAlg)
	case *ecdsa.PublicKey:
		return verifySignatureWithECDSAKey(hashed, signature, h.hardwareKey.(*ecdsa.PublicKey))
	case ed25519.PublicKey:
		return verifySignatureWithED25519Key(hashed, signature, h.hardwareKey.(ed25519.PublicKey))
	default:
		return fmt.Errorf("unsupported algorithm type: %T", keyType)
	}
}

func verifySignatureWithRSAKey(hashed, signature []byte, pubKey *rsa.PublicKey, hashAlg crypto.Hash) error {
	if err := rsa.VerifyPKCS1v15(pubKey, hashAlg, hashed, signature); err != nil {
		return fmt.Errorf("invalid signature: %v", err)
	}
	return nil
}

func verifySignatureWithECDSAKey(hashed, signature []byte, pubKey *ecdsa.PublicKey) error {
	// EcdsaSignature struct defines ASN.1 layout of ECDSA signature
	type EcdsaSignature struct {
		R, S *big.Int
	}

	var sig EcdsaSignature
	rest, err := asn1.Unmarshal(signature, &sig)
	if err != nil {
		return err
	}

	// Ensure all bytes were consumed
	if len(rest) > 0 {
		return errors.New("invalid signature: trailing bytes")
	}

	if !ecdsa.Verify(pubKey, hashed, sig.R, sig.S) {
		return errors.New("invalid signature")
	}

	return nil
}

func verifySignatureWithED25519Key(hashed, signature []byte, pubKey ed25519.PublicKey) error {
	if !ed25519.Verify(pubKey, hashed, signature) {
		return errors.New("invalid signature")
	}
	return nil
}