Skip to content

Commit 3048556

Browse files
authored
Merge pull request #519 from smallstep/mariano/mackms-ecdh
Add support for ECDH exchange using MacKMS
2 parents 06565ee + bbba371 commit 3048556

File tree

3 files changed

+356
-0
lines changed

3 files changed

+356
-0
lines changed

internal/darwin/security/security_darwin.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ var (
100100
KSecKeyAlgorithmRSASignatureDigestPSSSHA256 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA256
101101
KSecKeyAlgorithmRSASignatureDigestPSSSHA384 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA384
102102
KSecKeyAlgorithmRSASignatureDigestPSSSHA512 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA512
103+
KSecKeyAlgorithmECDHKeyExchangeStandard = C.kSecKeyAlgorithmECDHKeyExchangeStandard
103104
)
104105

105106
type SecAccessControlCreateFlags = C.SecAccessControlCreateFlags
@@ -271,6 +272,28 @@ func SecCertificateCreateWithData(certData *cf.DataRef) (*SecCertificateRef, err
271272
}, nil
272273
}
273274

275+
func SecKeyCreateWithData(keyData *cf.DataRef, attributes *cf.DictionaryRef) (*SecKeyRef, error) {
276+
var cerr C.CFErrorRef
277+
keyRef := C.SecKeyCreateWithData(C.CFDataRef(keyData.Value), C.CFDictionaryRef(attributes.Value), &cerr)
278+
if err := goCFErrorRef(cerr); err != nil {
279+
return nil, err
280+
}
281+
return &SecKeyRef{
282+
Value: keyRef,
283+
}, nil
284+
}
285+
286+
func SecKeyCopyKeyExchangeResult(privateKey *SecKeyRef, algorithm SecKeyAlgorithm, publicKey *SecKeyRef, parameters *cf.DictionaryRef) (*cf.DataRef, error) {
287+
var cerr C.CFErrorRef
288+
dataRef := C.SecKeyCopyKeyExchangeResult(privateKey.Value, algorithm, publicKey.Value, C.CFDictionaryRef(parameters.Value), &cerr)
289+
if err := goCFErrorRef(cerr); err != nil {
290+
return nil, err
291+
}
292+
return &cf.DataRef{
293+
Value: cf.CFDataRef(dataRef),
294+
}, nil
295+
}
296+
274297
func SecCopyErrorMessageString(status C.OSStatus) *cf.StringRef {
275298
s := C.SecCopyErrorMessageString(status, nil)
276299
return &cf.StringRef{

kms/mackms/signer.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ package mackms
2222

2323
import (
2424
"crypto"
25+
"crypto/ecdh"
2526
"crypto/ecdsa"
27+
"crypto/elliptic"
2628
"crypto/rsa"
2729
"fmt"
2830
"io"
@@ -110,3 +112,104 @@ func getSecKeyAlgorithm(pub crypto.PublicKey, opts crypto.SignerOpts) (security.
110112
return 0, fmt.Errorf("unsupported key type %T", pub)
111113
}
112114
}
115+
116+
// ECDH extends [Signer] with ECDH exchange method.
117+
//
118+
// # Experimental
119+
//
120+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
121+
// release.
122+
type ECDH struct {
123+
*Signer
124+
}
125+
126+
// ECDH performs an ECDH exchange and returns the shared secret. The private key
127+
// and public key must use the same curve.
128+
//
129+
// For NIST curves, this performs ECDH as specified in SEC 1, Version 2.0,
130+
// Section 3.3.1, and returns the x-coordinate encoded according to SEC 1,
131+
// Version 2.0, Section 2.3.5. The result is never the point at infinity.
132+
//
133+
// # Experimental
134+
//
135+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
136+
// release.
137+
func (e *ECDH) ECDH(pub *ecdh.PublicKey) ([]byte, error) {
138+
key, err := getPrivateKey(e.Signer.keyAttributes)
139+
if err != nil {
140+
return nil, fmt.Errorf("mackms ECDH failed: %w", err)
141+
}
142+
defer key.Release()
143+
144+
pubData, err := cf.NewData(pub.Bytes())
145+
if err != nil {
146+
return nil, fmt.Errorf("mackms ECDH failed: %w", err)
147+
}
148+
defer pubData.Release()
149+
150+
pubDict, err := cf.NewDictionary(cf.Dictionary{
151+
security.KSecAttrKeyType: security.KSecAttrKeyTypeECSECPrimeRandom,
152+
security.KSecAttrKeyClass: security.KSecAttrKeyClassPublic,
153+
})
154+
if err != nil {
155+
return nil, fmt.Errorf("mackms ECDH failed: %w", err)
156+
}
157+
defer pubDict.Release()
158+
159+
pubRef, err := security.SecKeyCreateWithData(pubData, pubDict)
160+
if err != nil {
161+
return nil, fmt.Errorf("macOS SecKeyCreateWithData failed: %w", err)
162+
}
163+
defer pubRef.Release()
164+
165+
sharedSecret, err := security.SecKeyCopyKeyExchangeResult(key, security.KSecKeyAlgorithmECDHKeyExchangeStandard, pubRef, &cf.DictionaryRef{})
166+
if err != nil {
167+
return nil, fmt.Errorf("macOS SecKeyCopyKeyExchangeResult failed: %w", err)
168+
}
169+
defer sharedSecret.Release()
170+
171+
return sharedSecret.Bytes(), nil
172+
}
173+
174+
// Curve returns the [ecdh.Curve] of the key. If the key is not an ECDSA key it
175+
// will return nil.
176+
//
177+
// # Experimental
178+
//
179+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
180+
// release.
181+
func (e *ECDH) Curve() ecdh.Curve {
182+
pub, ok := e.Signer.pub.(*ecdsa.PublicKey)
183+
if !ok {
184+
return nil
185+
}
186+
switch pub.Curve {
187+
case elliptic.P256():
188+
return ecdh.P256()
189+
case elliptic.P384():
190+
return ecdh.P384()
191+
case elliptic.P521():
192+
return ecdh.P521()
193+
default:
194+
return nil
195+
}
196+
}
197+
198+
// PublicKey returns the [ecdh.PublicKey] representation of the key. If the key
199+
// is not an ECDSA or it cannot be converted it will return nil.
200+
//
201+
// # Experimental
202+
//
203+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
204+
// release.
205+
func (e *ECDH) PublicKey() *ecdh.PublicKey {
206+
pub, ok := e.Signer.pub.(*ecdsa.PublicKey)
207+
if !ok {
208+
return nil
209+
}
210+
ecdhPub, err := pub.ECDH()
211+
if err != nil {
212+
return nil
213+
}
214+
return ecdhPub
215+
}

kms/mackms/signer_test.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
//go:build darwin && cgo && !nomackms
2+
3+
// Copyright (c) Smallstep Labs, Inc.
4+
// Copyright (c) Meta Platforms, Inc. and affiliates.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
7+
// use this file except in compliance with the License. You may obtain a copy of
8+
// the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
// License for the specific language governing permissions and limitations under
16+
// the License.
17+
//
18+
// Part of this code is based on
19+
// https://github.com/facebookincubator/sks/blob/183e7561ecedc71992f23b2d37983d2948391f4c/macos/macos.go
20+
21+
package mackms
22+
23+
import (
24+
"crypto"
25+
"crypto/ecdh"
26+
"crypto/ecdsa"
27+
"crypto/elliptic"
28+
"crypto/rand"
29+
"testing"
30+
31+
"github.com/stretchr/testify/assert"
32+
"github.com/stretchr/testify/require"
33+
"go.step.sm/crypto/kms/apiv1"
34+
)
35+
36+
func createKey(t *testing.T, name string, sa apiv1.SignatureAlgorithm) *apiv1.CreateKeyResponse {
37+
t.Helper()
38+
39+
kms := &MacKMS{}
40+
resp, err := kms.CreateKey(&apiv1.CreateKeyRequest{
41+
Name: "mackms:label=" + name,
42+
SignatureAlgorithm: sa,
43+
})
44+
require.NoError(t, err)
45+
t.Cleanup(func() {
46+
assert.NoError(t, kms.DeleteKey(&apiv1.DeleteKeyRequest{
47+
Name: resp.Name,
48+
}))
49+
})
50+
return resp
51+
}
52+
53+
func TestECDH_ECDH(t *testing.T) {
54+
goP256, err := ecdh.P256().GenerateKey(rand.Reader)
55+
require.NoError(t, err)
56+
goP384, err := ecdh.P384().GenerateKey(rand.Reader)
57+
require.NoError(t, err)
58+
goP521, err := ecdh.P521().GenerateKey(rand.Reader)
59+
require.NoError(t, err)
60+
goX25519, err := ecdh.X25519().GenerateKey(rand.Reader)
61+
require.NoError(t, err)
62+
63+
kms := &MacKMS{}
64+
p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256)
65+
s256, err := kms.CreateSigner(&p256.CreateSignerRequest)
66+
require.NoError(t, err)
67+
p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384)
68+
s384, err := kms.CreateSigner(&p384.CreateSignerRequest)
69+
require.NoError(t, err)
70+
p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512)
71+
s521, err := kms.CreateSigner(&p521.CreateSignerRequest)
72+
require.NoError(t, err)
73+
74+
type fields struct {
75+
Signer *Signer
76+
}
77+
type args struct {
78+
pub *ecdh.PublicKey
79+
}
80+
tests := []struct {
81+
name string
82+
fields fields
83+
args args
84+
wantFunc func(t *testing.T, got []byte)
85+
assertion assert.ErrorAssertionFunc
86+
}{
87+
{"ok P256", fields{s256.(*Signer)}, args{goP256.PublicKey()}, func(t *testing.T, got []byte) {
88+
pub, ok := s256.Public().(*ecdsa.PublicKey)
89+
require.True(t, ok)
90+
ecdhPub, err := pub.ECDH()
91+
require.NoError(t, err)
92+
sharedSecret, err := goP256.ECDH(ecdhPub)
93+
require.NoError(t, err)
94+
assert.Equal(t, sharedSecret, got)
95+
}, assert.NoError},
96+
{"ok P384", fields{s384.(*Signer)}, args{goP384.PublicKey()}, func(t *testing.T, got []byte) {
97+
pub, ok := s384.Public().(*ecdsa.PublicKey)
98+
require.True(t, ok)
99+
ecdhPub, err := pub.ECDH()
100+
require.NoError(t, err)
101+
sharedSecret, err := goP384.ECDH(ecdhPub)
102+
require.NoError(t, err)
103+
assert.Equal(t, sharedSecret, got)
104+
}, assert.NoError},
105+
{"ok P521", fields{s521.(*Signer)}, args{goP521.PublicKey()}, func(t *testing.T, got []byte) {
106+
pub, ok := s521.Public().(*ecdsa.PublicKey)
107+
require.True(t, ok)
108+
ecdhPub, err := pub.ECDH()
109+
require.NoError(t, err)
110+
sharedSecret, err := goP521.ECDH(ecdhPub)
111+
require.NoError(t, err)
112+
assert.Equal(t, sharedSecret, got)
113+
}, assert.NoError},
114+
{"fail missing", fields{&Signer{
115+
keyAttributes: &keyAttributes{tag: DefaultTag, label: t.Name() + "-missing"},
116+
}}, args{goP256.PublicKey()}, func(t *testing.T, got []byte) {
117+
assert.Nil(t, got)
118+
}, assert.Error},
119+
{"fail SecKeyCreateWithData", fields{s256.(*Signer)}, args{goX25519.PublicKey()}, func(t *testing.T, got []byte) {
120+
assert.Nil(t, got)
121+
}, assert.Error},
122+
{"fail SecKeyCopyKeyExchangeResult", fields{s256.(*Signer)}, args{goP384.PublicKey()}, func(t *testing.T, got []byte) {
123+
assert.Nil(t, got)
124+
}, assert.Error},
125+
}
126+
for _, tt := range tests {
127+
t.Run(tt.name, func(t *testing.T) {
128+
e := &ECDH{
129+
Signer: tt.fields.Signer,
130+
}
131+
got, err := e.ECDH(tt.args.pub)
132+
tt.assertion(t, err)
133+
tt.wantFunc(t, got)
134+
})
135+
}
136+
}
137+
138+
func TestECDH_Curve(t *testing.T) {
139+
kms := &MacKMS{}
140+
p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256)
141+
s256, err := kms.CreateSigner(&p256.CreateSignerRequest)
142+
require.NoError(t, err)
143+
p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384)
144+
s384, err := kms.CreateSigner(&p384.CreateSignerRequest)
145+
require.NoError(t, err)
146+
p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512)
147+
s521, err := kms.CreateSigner(&p521.CreateSignerRequest)
148+
require.NoError(t, err)
149+
150+
rsaKey := createKey(t, t.Name()+"-rsa", apiv1.SHA256WithRSA)
151+
rsaSigmer, err := kms.CreateSigner(&rsaKey.CreateSignerRequest)
152+
require.NoError(t, err)
153+
154+
p224, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
155+
require.NoError(t, err)
156+
157+
type fields struct {
158+
Signer *Signer
159+
}
160+
tests := []struct {
161+
name string
162+
fields fields
163+
want ecdh.Curve
164+
}{
165+
{"P256", fields{s256.(*Signer)}, ecdh.P256()},
166+
{"P384", fields{s384.(*Signer)}, ecdh.P384()},
167+
{"P521", fields{s521.(*Signer)}, ecdh.P521()},
168+
{"P224", fields{&Signer{pub: p224.Public()}}, nil},
169+
{"RSA", fields{rsaSigmer.(*Signer)}, nil},
170+
}
171+
for _, tt := range tests {
172+
t.Run(tt.name, func(t *testing.T) {
173+
e := &ECDH{
174+
Signer: tt.fields.Signer,
175+
}
176+
assert.Equal(t, tt.want, e.Curve())
177+
})
178+
}
179+
}
180+
181+
func TestECDH_PublicKey(t *testing.T) {
182+
kms := &MacKMS{}
183+
p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256)
184+
s256, err := kms.CreateSigner(&p256.CreateSignerRequest)
185+
require.NoError(t, err)
186+
p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384)
187+
s384, err := kms.CreateSigner(&p384.CreateSignerRequest)
188+
require.NoError(t, err)
189+
p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512)
190+
s521, err := kms.CreateSigner(&p521.CreateSignerRequest)
191+
require.NoError(t, err)
192+
193+
rsaKey := createKey(t, t.Name()+"-rsa", apiv1.SHA256WithRSA)
194+
rsaSigmer, err := kms.CreateSigner(&rsaKey.CreateSignerRequest)
195+
require.NoError(t, err)
196+
197+
p224, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
198+
require.NoError(t, err)
199+
200+
mustPublicKey := func(k crypto.PublicKey) *ecdh.PublicKey {
201+
pub, ok := k.(*ecdsa.PublicKey)
202+
require.True(t, ok)
203+
ecdhPub, err := pub.ECDH()
204+
require.NoError(t, err)
205+
return ecdhPub
206+
}
207+
208+
type fields struct {
209+
Signer *Signer
210+
}
211+
tests := []struct {
212+
name string
213+
fields fields
214+
want *ecdh.PublicKey
215+
}{
216+
{"P256", fields{s256.(*Signer)}, mustPublicKey(p256.PublicKey)},
217+
{"P384", fields{s384.(*Signer)}, mustPublicKey(p384.PublicKey)},
218+
{"P521", fields{s521.(*Signer)}, mustPublicKey(p521.PublicKey)},
219+
{"P224", fields{&Signer{pub: p224.Public()}}, nil},
220+
{"RSA", fields{rsaSigmer.(*Signer)}, nil},
221+
}
222+
for _, tt := range tests {
223+
t.Run(tt.name, func(t *testing.T) {
224+
e := &ECDH{
225+
Signer: tt.fields.Signer,
226+
}
227+
assert.Equal(t, tt.want, e.PublicKey())
228+
})
229+
}
230+
}

0 commit comments

Comments
 (0)