-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkeys.go
More file actions
170 lines (154 loc) · 5.72 KB
/
Copy pathkeys.go
File metadata and controls
170 lines (154 loc) · 5.72 KB
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
package spindle
import (
"crypto/ed25519"
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"fmt"
"github.com/flynn/noise"
"golang.org/x/crypto/curve25519"
)
// DHLen is the length of an X25519 public or private key in bytes.
const DHLen = 32
// SigningKey is an Ed25519 keypair used for signatures that sit above the
// channel: firmware attestation on the device side, audit-log heads on the
// controller side.
//
// It is deliberately not part of the handshake. Under Noise_IK both peers are
// authenticated by their static X25519 keys through the DH pattern itself, so a
// signature over the transcript would be redundant. Keeping the signing key
// separate means compromise of one does not imply compromise of the other.
type SigningKey struct {
Priv ed25519.PrivateKey
Pub ed25519.PublicKey
}
// NewSigningKey generates a fresh Ed25519 keypair.
func NewSigningKey() (*SigningKey, error) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("spindle: generate signing key: %w", err)
}
return &SigningKey{Priv: priv, Pub: pub}, nil
}
// Sign produces a detached Ed25519 signature over msg.
func (k *SigningKey) Sign(msg []byte) []byte {
if k == nil || len(k.Priv) == 0 {
return nil
}
return ed25519.Sign(k.Priv, msg)
}
// ControllerIdentity is the controlling side's long-term identity.
//
// Static is the X25519 keypair that devices pin at provisioning time; it is the
// controller's handshake identity. Signing is optional and used only for
// out-of-channel signatures such as audit-log heads.
type ControllerIdentity struct {
Static noise.DHKey
Signing *SigningKey
}
// NewControllerIdentity generates a controller identity with both a static
// X25519 keypair and an Ed25519 signing keypair. Callers persist all of it,
// with private material encrypted at rest.
func NewControllerIdentity() (*ControllerIdentity, error) {
static, err := CipherSuite.GenerateKeypair(rand.Reader)
if err != nil {
return nil, fmt.Errorf("spindle: generate controller static: %w", err)
}
signing, err := NewSigningKey()
if err != nil {
return nil, err
}
return &ControllerIdentity{Static: static, Signing: signing}, nil
}
// NewControllerIdentityFrom rebuilds a controller identity from stored key
// material: a 32-byte X25519 static private key and an Ed25519 signing key.
//
// It exists so callers can load an identity from their own storage without
// importing the underlying Noise package to construct a DHKey.
func NewControllerIdentityFrom(staticPriv []byte, signPriv ed25519.PrivateKey) (*ControllerIdentity, error) {
if len(staticPriv) != DHLen {
return nil, fmt.Errorf("spindle: static private key must be %d bytes, got %d", DHLen, len(staticPriv))
}
pub, err := curve25519.X25519(staticPriv, curve25519.Basepoint)
if err != nil {
return nil, fmt.Errorf("spindle: derive static public: %w", err)
}
id := &ControllerIdentity{
Static: noise.DHKey{
Private: append([]byte(nil), staticPriv...),
Public: pub,
},
}
if len(signPriv) > 0 {
if len(signPriv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("spindle: signing key must be %d bytes, got %d", ed25519.PrivateKeySize, len(signPriv))
}
id.Signing = &SigningKey{
Priv: signPriv,
Pub: signPriv.Public().(ed25519.PublicKey),
}
}
return id, nil
}
// DeviceKeys is a device's long-term identity.
//
// Static is the X25519 keypair whose public half the controller pins in its
// device registry. Signing is optional; deployments that require firmware
// attestation must provision one during commissioning.
type DeviceKeys struct {
Static noise.DHKey
Signing *SigningKey
}
// NewDeviceKeys generates a fresh device static keypair. It does not generate a
// signing key — call NewSigningKey separately when attestation is required, so
// that deployments which do not use it never carry the key material.
func NewDeviceKeys() (*DeviceKeys, error) {
static, err := CipherSuite.GenerateKeypair(rand.Reader)
if err != nil {
return nil, fmt.Errorf("spindle: generate device static: %w", err)
}
return &DeviceKeys{Static: static}, nil
}
// NewDeviceKeysFrom rebuilds device keys from a stored 32-byte X25519 private
// key, deriving the public half.
func NewDeviceKeysFrom(staticPriv []byte) (*DeviceKeys, error) {
if len(staticPriv) != DHLen {
return nil, fmt.Errorf("spindle: static private key must be %d bytes, got %d", DHLen, len(staticPriv))
}
pub, err := curve25519.X25519(staticPriv, curve25519.Basepoint)
if err != nil {
return nil, fmt.Errorf("spindle: derive static public: %w", err)
}
return &DeviceKeys{Static: noise.DHKey{
Private: append([]byte(nil), staticPriv...),
Public: pub,
}}, nil
}
// ParseDeviceKeys reconstructs a device keypair from hex-encoded material, as
// carried in a provisioning file. It verifies that pub actually derives from
// priv, so a corrupted or mismatched provisioning file fails at load rather
// than as an unexplained handshake failure in the field.
func ParseDeviceKeys(privHex, pubHex string) (*DeviceKeys, error) {
priv, err := hex.DecodeString(privHex)
if err != nil {
return nil, fmt.Errorf("spindle: decode private key: %w", err)
}
pub, err := hex.DecodeString(pubHex)
if err != nil {
return nil, fmt.Errorf("spindle: decode public key: %w", err)
}
if len(priv) != DHLen {
return nil, fmt.Errorf("spindle: private key must be %d bytes, got %d", DHLen, len(priv))
}
if len(pub) != DHLen {
return nil, fmt.Errorf("spindle: public key must be %d bytes, got %d", DHLen, len(pub))
}
derived, err := curve25519.X25519(priv, curve25519.Basepoint)
if err != nil {
return nil, fmt.Errorf("spindle: derive public key: %w", err)
}
if subtle.ConstantTimeCompare(derived, pub) != 1 {
return nil, ErrKeyMismatch
}
return &DeviceKeys{Static: noise.DHKey{Private: priv, Public: pub}}, nil
}