Skip to content

Commit

Permalink
Merge pull request #519 from smallstep/mariano/mackms-ecdh
Browse files Browse the repository at this point in the history
Add support for ECDH exchange using MacKMS
  • Loading branch information
maraino authored Jun 6, 2024
2 parents 06565ee + bbba371 commit 3048556
Show file tree
Hide file tree
Showing 3 changed files with 356 additions and 0 deletions.
23 changes: 23 additions & 0 deletions internal/darwin/security/security_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ var (
KSecKeyAlgorithmRSASignatureDigestPSSSHA256 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA256
KSecKeyAlgorithmRSASignatureDigestPSSSHA384 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA384
KSecKeyAlgorithmRSASignatureDigestPSSSHA512 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA512
KSecKeyAlgorithmECDHKeyExchangeStandard = C.kSecKeyAlgorithmECDHKeyExchangeStandard
)

type SecAccessControlCreateFlags = C.SecAccessControlCreateFlags
Expand Down Expand Up @@ -271,6 +272,28 @@ func SecCertificateCreateWithData(certData *cf.DataRef) (*SecCertificateRef, err
}, nil
}

func SecKeyCreateWithData(keyData *cf.DataRef, attributes *cf.DictionaryRef) (*SecKeyRef, error) {
var cerr C.CFErrorRef
keyRef := C.SecKeyCreateWithData(C.CFDataRef(keyData.Value), C.CFDictionaryRef(attributes.Value), &cerr)
if err := goCFErrorRef(cerr); err != nil {
return nil, err
}
return &SecKeyRef{
Value: keyRef,
}, nil
}

func SecKeyCopyKeyExchangeResult(privateKey *SecKeyRef, algorithm SecKeyAlgorithm, publicKey *SecKeyRef, parameters *cf.DictionaryRef) (*cf.DataRef, error) {
var cerr C.CFErrorRef
dataRef := C.SecKeyCopyKeyExchangeResult(privateKey.Value, algorithm, publicKey.Value, C.CFDictionaryRef(parameters.Value), &cerr)
if err := goCFErrorRef(cerr); err != nil {
return nil, err
}
return &cf.DataRef{
Value: cf.CFDataRef(dataRef),
}, nil
}

func SecCopyErrorMessageString(status C.OSStatus) *cf.StringRef {
s := C.SecCopyErrorMessageString(status, nil)
return &cf.StringRef{
Expand Down
103 changes: 103 additions & 0 deletions kms/mackms/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ package mackms

import (
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"fmt"
"io"
Expand Down Expand Up @@ -110,3 +112,104 @@ func getSecKeyAlgorithm(pub crypto.PublicKey, opts crypto.SignerOpts) (security.
return 0, fmt.Errorf("unsupported key type %T", pub)
}
}

// ECDH extends [Signer] with ECDH exchange method.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
// release.
type ECDH struct {
*Signer
}

// ECDH performs an ECDH exchange and returns the shared secret. The private key
// and public key must use the same curve.
//
// For NIST curves, this performs ECDH as specified in SEC 1, Version 2.0,
// Section 3.3.1, and returns the x-coordinate encoded according to SEC 1,
// Version 2.0, Section 2.3.5. The result is never the point at infinity.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
// release.
func (e *ECDH) ECDH(pub *ecdh.PublicKey) ([]byte, error) {
key, err := getPrivateKey(e.Signer.keyAttributes)
if err != nil {
return nil, fmt.Errorf("mackms ECDH failed: %w", err)
}
defer key.Release()

pubData, err := cf.NewData(pub.Bytes())
if err != nil {
return nil, fmt.Errorf("mackms ECDH failed: %w", err)
}
defer pubData.Release()

pubDict, err := cf.NewDictionary(cf.Dictionary{
security.KSecAttrKeyType: security.KSecAttrKeyTypeECSECPrimeRandom,
security.KSecAttrKeyClass: security.KSecAttrKeyClassPublic,
})
if err != nil {
return nil, fmt.Errorf("mackms ECDH failed: %w", err)
}
defer pubDict.Release()

pubRef, err := security.SecKeyCreateWithData(pubData, pubDict)
if err != nil {
return nil, fmt.Errorf("macOS SecKeyCreateWithData failed: %w", err)
}
defer pubRef.Release()

sharedSecret, err := security.SecKeyCopyKeyExchangeResult(key, security.KSecKeyAlgorithmECDHKeyExchangeStandard, pubRef, &cf.DictionaryRef{})
if err != nil {
return nil, fmt.Errorf("macOS SecKeyCopyKeyExchangeResult failed: %w", err)
}
defer sharedSecret.Release()

return sharedSecret.Bytes(), nil
}

// Curve returns the [ecdh.Curve] of the key. If the key is not an ECDSA key it
// will return nil.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
// release.
func (e *ECDH) Curve() ecdh.Curve {
pub, ok := e.Signer.pub.(*ecdsa.PublicKey)
if !ok {
return nil
}
switch pub.Curve {
case elliptic.P256():
return ecdh.P256()
case elliptic.P384():
return ecdh.P384()
case elliptic.P521():
return ecdh.P521()
default:
return nil
}
}

// PublicKey returns the [ecdh.PublicKey] representation of the key. If the key
// is not an ECDSA or it cannot be converted it will return nil.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
// release.
func (e *ECDH) PublicKey() *ecdh.PublicKey {
pub, ok := e.Signer.pub.(*ecdsa.PublicKey)
if !ok {
return nil
}
ecdhPub, err := pub.ECDH()
if err != nil {
return nil
}
return ecdhPub
}
230 changes: 230 additions & 0 deletions kms/mackms/signer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//go:build darwin && cgo && !nomackms

// Copyright (c) Smallstep Labs, Inc.
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// 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.
//
// Part of this code is based on
// https://github.com/facebookincubator/sks/blob/183e7561ecedc71992f23b2d37983d2948391f4c/macos/macos.go

package mackms

import (
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/kms/apiv1"
)

func createKey(t *testing.T, name string, sa apiv1.SignatureAlgorithm) *apiv1.CreateKeyResponse {
t.Helper()

kms := &MacKMS{}
resp, err := kms.CreateKey(&apiv1.CreateKeyRequest{
Name: "mackms:label=" + name,
SignatureAlgorithm: sa,
})
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, kms.DeleteKey(&apiv1.DeleteKeyRequest{
Name: resp.Name,
}))
})
return resp
}

func TestECDH_ECDH(t *testing.T) {
goP256, err := ecdh.P256().GenerateKey(rand.Reader)
require.NoError(t, err)
goP384, err := ecdh.P384().GenerateKey(rand.Reader)
require.NoError(t, err)
goP521, err := ecdh.P521().GenerateKey(rand.Reader)
require.NoError(t, err)
goX25519, err := ecdh.X25519().GenerateKey(rand.Reader)
require.NoError(t, err)

kms := &MacKMS{}
p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256)
s256, err := kms.CreateSigner(&p256.CreateSignerRequest)
require.NoError(t, err)
p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384)
s384, err := kms.CreateSigner(&p384.CreateSignerRequest)
require.NoError(t, err)
p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512)
s521, err := kms.CreateSigner(&p521.CreateSignerRequest)
require.NoError(t, err)

type fields struct {
Signer *Signer
}
type args struct {
pub *ecdh.PublicKey
}
tests := []struct {
name string
fields fields
args args
wantFunc func(t *testing.T, got []byte)
assertion assert.ErrorAssertionFunc
}{
{"ok P256", fields{s256.(*Signer)}, args{goP256.PublicKey()}, func(t *testing.T, got []byte) {
pub, ok := s256.Public().(*ecdsa.PublicKey)
require.True(t, ok)
ecdhPub, err := pub.ECDH()
require.NoError(t, err)
sharedSecret, err := goP256.ECDH(ecdhPub)
require.NoError(t, err)
assert.Equal(t, sharedSecret, got)
}, assert.NoError},
{"ok P384", fields{s384.(*Signer)}, args{goP384.PublicKey()}, func(t *testing.T, got []byte) {
pub, ok := s384.Public().(*ecdsa.PublicKey)
require.True(t, ok)
ecdhPub, err := pub.ECDH()
require.NoError(t, err)
sharedSecret, err := goP384.ECDH(ecdhPub)
require.NoError(t, err)
assert.Equal(t, sharedSecret, got)
}, assert.NoError},
{"ok P521", fields{s521.(*Signer)}, args{goP521.PublicKey()}, func(t *testing.T, got []byte) {
pub, ok := s521.Public().(*ecdsa.PublicKey)
require.True(t, ok)
ecdhPub, err := pub.ECDH()
require.NoError(t, err)
sharedSecret, err := goP521.ECDH(ecdhPub)
require.NoError(t, err)
assert.Equal(t, sharedSecret, got)
}, assert.NoError},
{"fail missing", fields{&Signer{
keyAttributes: &keyAttributes{tag: DefaultTag, label: t.Name() + "-missing"},
}}, args{goP256.PublicKey()}, func(t *testing.T, got []byte) {
assert.Nil(t, got)
}, assert.Error},
{"fail SecKeyCreateWithData", fields{s256.(*Signer)}, args{goX25519.PublicKey()}, func(t *testing.T, got []byte) {
assert.Nil(t, got)
}, assert.Error},
{"fail SecKeyCopyKeyExchangeResult", fields{s256.(*Signer)}, args{goP384.PublicKey()}, func(t *testing.T, got []byte) {
assert.Nil(t, got)
}, assert.Error},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &ECDH{
Signer: tt.fields.Signer,
}
got, err := e.ECDH(tt.args.pub)
tt.assertion(t, err)
tt.wantFunc(t, got)
})
}
}

func TestECDH_Curve(t *testing.T) {
kms := &MacKMS{}
p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256)
s256, err := kms.CreateSigner(&p256.CreateSignerRequest)
require.NoError(t, err)
p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384)
s384, err := kms.CreateSigner(&p384.CreateSignerRequest)
require.NoError(t, err)
p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512)
s521, err := kms.CreateSigner(&p521.CreateSignerRequest)
require.NoError(t, err)

rsaKey := createKey(t, t.Name()+"-rsa", apiv1.SHA256WithRSA)
rsaSigmer, err := kms.CreateSigner(&rsaKey.CreateSignerRequest)
require.NoError(t, err)

p224, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
require.NoError(t, err)

type fields struct {
Signer *Signer
}
tests := []struct {
name string
fields fields
want ecdh.Curve
}{
{"P256", fields{s256.(*Signer)}, ecdh.P256()},
{"P384", fields{s384.(*Signer)}, ecdh.P384()},
{"P521", fields{s521.(*Signer)}, ecdh.P521()},
{"P224", fields{&Signer{pub: p224.Public()}}, nil},
{"RSA", fields{rsaSigmer.(*Signer)}, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &ECDH{
Signer: tt.fields.Signer,
}
assert.Equal(t, tt.want, e.Curve())
})
}
}

func TestECDH_PublicKey(t *testing.T) {
kms := &MacKMS{}
p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256)
s256, err := kms.CreateSigner(&p256.CreateSignerRequest)
require.NoError(t, err)
p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384)
s384, err := kms.CreateSigner(&p384.CreateSignerRequest)
require.NoError(t, err)
p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512)
s521, err := kms.CreateSigner(&p521.CreateSignerRequest)
require.NoError(t, err)

rsaKey := createKey(t, t.Name()+"-rsa", apiv1.SHA256WithRSA)
rsaSigmer, err := kms.CreateSigner(&rsaKey.CreateSignerRequest)
require.NoError(t, err)

p224, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
require.NoError(t, err)

mustPublicKey := func(k crypto.PublicKey) *ecdh.PublicKey {
pub, ok := k.(*ecdsa.PublicKey)
require.True(t, ok)
ecdhPub, err := pub.ECDH()
require.NoError(t, err)
return ecdhPub
}

type fields struct {
Signer *Signer
}
tests := []struct {
name string
fields fields
want *ecdh.PublicKey
}{
{"P256", fields{s256.(*Signer)}, mustPublicKey(p256.PublicKey)},
{"P384", fields{s384.(*Signer)}, mustPublicKey(p384.PublicKey)},
{"P521", fields{s521.(*Signer)}, mustPublicKey(p521.PublicKey)},
{"P224", fields{&Signer{pub: p224.Public()}}, nil},
{"RSA", fields{rsaSigmer.(*Signer)}, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &ECDH{
Signer: tt.fields.Signer,
}
assert.Equal(t, tt.want, e.PublicKey())
})
}
}

0 comments on commit 3048556

Please sign in to comment.