Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Ed25519 #114

Merged
merged 10 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cgo_go122.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ package openssl
// functions that are known to allocate.
#cgo noescape go_openssl_EVP_PKEY_derive
#cgo nocallback go_openssl_EVP_PKEY_derive
#cgo noescape go_openssl_EVP_PKEY_get_raw_public_key
#cgo nocallback go_openssl_EVP_PKEY_get_raw_public_key
#cgo noescape go_openssl_EVP_PKEY_get_raw_private_key
#cgo nocallback go_openssl_EVP_PKEY_get_raw_private_key
#cgo noescape go_openssl_EVP_DigestSign
#cgo nocallback go_openssl_EVP_DigestSign
*/
import "C"
173 changes: 173 additions & 0 deletions ed25519.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//go:build !cmd_go_bootstrap

package openssl

// #include "goopenssl.h"
import "C"
import (
"errors"
"strconv"
"sync"
"unsafe"
)

const (
// publicKeySizeEd25519 is the size, in bytes, of public keys as used in crypto/ed25519.
publicKeySizeEd25519 = 32
// privateKeySizeEd25519 is the size, in bytes, of private keys as used in crypto/ed25519.
privateKeySizeEd25519 = 64
// signatureSizeEd25519 is the size, in bytes, of signatures generated and verified by crypto/ed25519.
signatureSizeEd25519 = 64
// seedSizeEd25519 is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032.
seedSizeEd25519 = 32
)

// TODO: Add support for Ed25519ph and Ed25519ctx when OpenSSL supports them,
// which will probably be in 3.2.0 (https://github.com/openssl/openssl/issues/20418).

var (
onceSupportsEd25519 sync.Once
supportsEd25519 bool
)

// SupportsEd25519 returns true if the current OpenSSL version supports
// GenerateKeyEd25519, NewKeyFromSeedEd25519, SignEd25519 and VerifyEd25519.
func SupportsEd25519() bool {
onceSupportsEd25519.Do(func() {
switch vMajor {
case 1:
supportsEd25519 = version1_1_1_or_above()
case 3:
name := C.CString("ED25519")
defer C.free(unsafe.Pointer(name))
sig := C.go_openssl_EVP_SIGNATURE_fetch(nil, name, nil)
if sig != nil {
C.go_openssl_EVP_SIGNATURE_free(sig)
supportsEd25519 = true
}
}
})
return supportsEd25519
}

// GenerateKeyEd25519 generates a public/private key pair.
func GenerateKeyEd25519() (pub, priv []byte, err error) {
pkey, err := generateEVPPKey(C.GO_EVP_PKEY_ED25519, 0, "")
if err != nil {
return nil, nil, err
}
defer C.go_openssl_EVP_PKEY_free(pkey)
priv = make([]byte, privateKeySizeEd25519)
err = extractPKEYPrivEd25519(pkey, priv)
if err != nil {
return nil, nil, err
}
pub = make([]byte, publicKeySizeEd25519)
copy(pub, priv[seedSizeEd25519:])
return pub, priv, nil
}

// NewKeyFromSeedEd25519 calculates a private key from a seed. It will panic if
// len(seed) is not [SeedSize]. This function is provided for interoperability
// with RFC 8032. RFC 8032's private keys correspond to seeds in this
// package.
func NewKeyFromSeedEd25519(seed []byte) (priv []byte, err error) {
qmuntal marked this conversation as resolved.
Show resolved Hide resolved
// Outline the function body so that the returned key can be stack-allocated.
priv = make([]byte, privateKeySizeEd25519)
err = newKeyFromSeedEd25519(priv, seed)
if err != nil {
return nil, err
}
return priv, err
}

func newKeyFromSeedEd25519(priv []byte, seed []byte) error {
if len(seed) != seedSizeEd25519 {
panic("ed25519: bad seed length: " + strconv.Itoa(len(seed)))
}
pkey := C.go_openssl_EVP_PKEY_new_raw_private_key(C.GO_EVP_PKEY_ED25519, nil, base(seed), C.size_t(len(seed)))
if pkey == nil {
return newOpenSSLError("EVP_PKEY_new_raw_private_key")
}
defer C.go_openssl_EVP_PKEY_free(pkey)
return extractPKEYPrivEd25519(pkey, priv)
}

func extractPKEYPrivEd25519(pkey C.GO_EVP_PKEY_PTR, priv []byte) error {
pubSize, privSize := C.size_t(publicKeySizeEd25519), C.size_t(seedSizeEd25519)
if C.go_openssl_EVP_PKEY_get_raw_public_key(pkey, base(priv[seedSizeEd25519:]), &pubSize) != 1 {
return newOpenSSLError("EVP_PKEY_get_raw_public_key")
}
if pubSize != publicKeySizeEd25519 {
panic("ed25519: bad public key length: " + strconv.Itoa(int(pubSize)))
}
if C.go_openssl_EVP_PKEY_get_raw_private_key(pkey, base(priv), &privSize) != 1 {
return newOpenSSLError("EVP_PKEY_get_raw_private_key")
}
if privSize != seedSizeEd25519 {
panic("ed25519: bad private key length: " + strconv.Itoa(int(privSize)))
}
return nil
}

// SignEd25519 signs the message with priv and returns a signature.
func SignEd25519(priv, message []byte) (sig []byte, err error) {
Copy link
Collaborator

@ueno ueno Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike ECDSA, this API takes a private key as bytes and internally reconstructs the corresponding EVP_PKEY, which I suspect would prevent using non-extractable keys such as ones on PKCS#11. Why not defining a dedicated key type around EVP_PKEY, like PrivateKeyECDSA?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I've updated the implementation to use PrivateKeyEd25519 and PublicKeyEd25519 instead of raw byte slices.

// Outline the function body so that the returned key can be stack-allocated.
sig = make([]byte, signatureSizeEd25519)
err = signEd25519(sig, priv, message)
if err != nil {
return nil, err
}
return sig, err
}

func signEd25519(sig, priv, message []byte) error {
if len(priv) != privateKeySizeEd25519 {
panic("ed25519: bad private key length: " + strconv.Itoa(len(priv)))
}
pkey := C.go_openssl_EVP_PKEY_new_raw_private_key(C.GO_EVP_PKEY_ED25519, nil, base(priv[:seedSizeEd25519]), seedSizeEd25519)
if pkey == nil {
return newOpenSSLError("EVP_PKEY_new_raw_private_key")
}
defer C.go_openssl_EVP_PKEY_free(pkey)
ctx := C.go_openssl_EVP_MD_CTX_new()
if ctx == nil {
return newOpenSSLError("EVP_MD_CTX_new")
}
defer C.go_openssl_EVP_MD_CTX_free(ctx)
if C.go_openssl_EVP_DigestSignInit(ctx, nil, nil, nil, pkey) != 1 {
return newOpenSSLError("EVP_DigestSignInit")
}
siglen := C.size_t(signatureSizeEd25519)
if C.go_openssl_EVP_DigestSign(ctx, base(sig), &siglen, base(message), C.size_t(len(message))) != 1 {
return newOpenSSLError("EVP_DigestSign")
}
if siglen != signatureSizeEd25519 {
panic("ed25519: bad signature length: " + strconv.Itoa(int(siglen)))
}
return nil
}

// VerifyEd25519 reports whether sig is a valid signature of message by pub.
func VerifyEd25519(pub, message, sig []byte) error {
if len(pub) != publicKeySizeEd25519 {
panic("ed25519: bad public key length: " + strconv.Itoa(len(pub)))
}
pkey := C.go_openssl_EVP_PKEY_new_raw_public_key(C.GO_EVP_PKEY_ED25519, nil, base(pub), publicKeySizeEd25519)
if pkey == nil {
return newOpenSSLError("EVP_PKEY_new_raw_public_key")
}
defer C.go_openssl_EVP_PKEY_free(pkey)
ctx := C.go_openssl_EVP_MD_CTX_new()
if ctx == nil {
return newOpenSSLError("EVP_MD_CTX_new")
}
defer C.go_openssl_EVP_MD_CTX_free(ctx)
if C.go_openssl_EVP_DigestVerifyInit(ctx, nil, nil, nil, pkey) != 1 {
return newOpenSSLError("EVP_DigestVerifyInit")
}
if C.go_openssl_EVP_DigestVerify(ctx, base(sig), C.size_t(len(sig)), base(message), C.size_t(len(message))) != 1 {
return errors.New("ed25519: invalid signature")
}
return nil
}
135 changes: 135 additions & 0 deletions ed25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package openssl_test

import (
"bytes"
"crypto/ed25519"
"testing"

"github.com/golang-fips/openssl/v2"
)

func TestNewKeyFromSeedEd25519(t *testing.T) {
if !openssl.SupportsEd25519() {
t.Skip("Ed25519 not supported")
}
seed := bytes.Repeat([]byte{0x01}, ed25519.SeedSize)
priv, err := openssl.NewKeyFromSeedEd25519(seed)
if err != nil {
t.Fatal(err)
}
priv2 := ed25519.NewKeyFromSeed(seed)
if !bytes.Equal(priv, priv2) {
t.Errorf("private key mismatch")
}
}

func TestEd25519SignVerify(t *testing.T) {
if !openssl.SupportsEd25519() {
t.Skip("Ed25519 not supported")
}
public, private, err := openssl.GenerateKeyEd25519()
if err != nil {
t.Fatal(err)
}
message := []byte("test message")
sig, err := openssl.SignEd25519(private, message)
if err != nil {
t.Fatal(err)
}
if sig2 := ed25519.Sign(private, message); !bytes.Equal(sig, sig2) {
t.Errorf("signature mismatch")
}
if openssl.VerifyEd25519(public, message, sig) != nil {
t.Errorf("valid signature rejected")
}
wrongMessage := []byte("wrong message")
if openssl.VerifyEd25519(public, wrongMessage, sig) == nil {
t.Errorf("signature of different message accepted")
}
}

func TestEd25519Malleability(t *testing.T) {
if !openssl.SupportsEd25519() {
t.Skip("Ed25519 not supported")
}
// https://tools.ietf.org/html/rfc8032#section-5.1.7 adds an additional test
// that s be in [0, order). This prevents someone from adding a multiple of
// order to s and obtaining a second valid signature for the same message.
msg := []byte{0x54, 0x65, 0x73, 0x74}
sig := []byte{
0x7c, 0x38, 0xe0, 0x26, 0xf2, 0x9e, 0x14, 0xaa, 0xbd, 0x05, 0x9a,
0x0f, 0x2d, 0xb8, 0xb0, 0xcd, 0x78, 0x30, 0x40, 0x60, 0x9a, 0x8b,
0xe6, 0x84, 0xdb, 0x12, 0xf8, 0x2a, 0x27, 0x77, 0x4a, 0xb0, 0x67,
0x65, 0x4b, 0xce, 0x38, 0x32, 0xc2, 0xd7, 0x6f, 0x8f, 0x6f, 0x5d,
0xaf, 0xc0, 0x8d, 0x93, 0x39, 0xd4, 0xee, 0xf6, 0x76, 0x57, 0x33,
0x36, 0xa5, 0xc5, 0x1e, 0xb6, 0xf9, 0x46, 0xb3, 0x1d,
}
publicKey := []byte{
0x7d, 0x4d, 0x0e, 0x7f, 0x61, 0x53, 0xa6, 0x9b, 0x62, 0x42, 0xb5,
0x22, 0xab, 0xbe, 0xe6, 0x85, 0xfd, 0xa4, 0x42, 0x0f, 0x88, 0x34,
0xb1, 0x08, 0xc3, 0xbd, 0xae, 0x36, 0x9e, 0xf5, 0x49, 0xfa,
}

if openssl.VerifyEd25519(publicKey, msg, sig) == nil {
t.Fatal("non-canonical signature accepted")
}
}

func BenchmarkEd25519GenerateKey(b *testing.B) {
if !openssl.SupportsEd25519() {
b.Skip("Ed25519 not supported")
}
for i := 0; i < b.N; i++ {
_, _, err := openssl.GenerateKeyEd25519()
if err != nil {
b.Fatal(err)
}
}
}

func BenchmarkEd25519NewKeyFromSeed(b *testing.B) {
if !openssl.SupportsEd25519() {
b.Skip("Ed25519 not supported")
}
seed := make([]byte, ed25519.SeedSize)
for i := 0; i < b.N; i++ {
_, err := openssl.NewKeyFromSeedEd25519(seed)
if err != nil {
b.Fatal(err)
}
}
}

func BenchmarkEd25519Signing(b *testing.B) {
if !openssl.SupportsEd25519() {
b.Skip("Ed25519 not supported")
}
_, priv, err := openssl.GenerateKeyEd25519()
if err != nil {
b.Fatal(err)
}
message := []byte("Hello, world!")
b.ResetTimer()
for i := 0; i < b.N; i++ {
openssl.SignEd25519(priv, message)
}
}

func BenchmarkEd25519Verification(b *testing.B) {
if !openssl.SupportsEd25519() {
b.Skip("Ed25519 not supported")
}
pub, priv, err := openssl.GenerateKeyEd25519()
if err != nil {
b.Fatal(err)
}
message := []byte("Hello, world!")
signature, err := openssl.SignEd25519(priv, message)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
openssl.VerifyEd25519(pub, message, signature)
}
}
2 changes: 1 addition & 1 deletion evp.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func cryptoHashToMD(ch crypto.Hash) (md C.GO_EVP_MD_PTR) {
}

func generateEVPPKey(id C.int, bits int, curve string) (C.GO_EVP_PKEY_PTR, error) {
if (bits == 0 && curve == "") || (bits != 0 && curve != "") {
if bits != 0 && curve != "" {
return nil, fail("incorrect generateEVPPKey parameters")
}
ctx := C.go_openssl_EVP_PKEY_CTX_new_id(id, nil)
Expand Down
10 changes: 10 additions & 0 deletions shims.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum {
GO_EVP_PKEY_EC = 408,
GO_EVP_PKEY_TLS1_PRF = 1021,
GO_EVP_PKEY_HKDF = 1036,
GO_EVP_PKEY_ED25519 = 1087,
/* This is defined differently in OpenSSL 3 (1 << 11), but in our
* code it is only used in OpenSSL 1.
*/
Expand Down Expand Up @@ -106,6 +107,7 @@ typedef void* GO_EVP_MAC_CTX_PTR;
typedef void* GO_OSSL_PARAM_BLD_PTR;
typedef void* GO_OSSL_PARAM_PTR;
typedef void* GO_CRYPTO_THREADID_PTR;
typedef void* GO_EVP_SIGNATURE_PTR;

// #include <openssl/md5.h>
typedef void* GO_MD5_CTX_PTR;
Expand Down Expand Up @@ -205,10 +207,12 @@ DEFINEFUNC(int, EVP_DigestInit_ex, (GO_EVP_MD_CTX_PTR ctx, const GO_EVP_MD_PTR t
DEFINEFUNC(int, EVP_DigestInit, (GO_EVP_MD_CTX_PTR ctx, const GO_EVP_MD_PTR type), (ctx, type)) \
DEFINEFUNC(int, EVP_DigestUpdate, (GO_EVP_MD_CTX_PTR ctx, const void *d, size_t cnt), (ctx, d, cnt)) \
DEFINEFUNC(int, EVP_DigestFinal, (GO_EVP_MD_CTX_PTR ctx, unsigned char *md, unsigned int *s), (ctx, md, s)) \
DEFINEFUNC_1_1_1(int, EVP_DigestSign, (GO_EVP_MD_CTX_PTR ctx, unsigned char *sigret, size_t *siglen, const unsigned char *tbs, size_t tbslen), (ctx, sigret, siglen, tbs, tbslen)) \
DEFINEFUNC(int, EVP_DigestSignInit, (GO_EVP_MD_CTX_PTR ctx, GO_EVP_PKEY_CTX_PTR *pctx, const GO_EVP_MD_PTR type, GO_ENGINE_PTR e, GO_EVP_PKEY_PTR pkey), (ctx, pctx, type, e, pkey)) \
DEFINEFUNC(int, EVP_DigestSignFinal, (GO_EVP_MD_CTX_PTR ctx, unsigned char *sig, size_t *siglen), (ctx, sig, siglen)) \
DEFINEFUNC(int, EVP_DigestVerifyInit, (GO_EVP_MD_CTX_PTR ctx, GO_EVP_PKEY_CTX_PTR *pctx, const GO_EVP_MD_PTR type, GO_ENGINE_PTR e, GO_EVP_PKEY_PTR pkey), (ctx, pctx, type, e, pkey)) \
DEFINEFUNC(int, EVP_DigestVerifyFinal, (GO_EVP_MD_CTX_PTR ctx, const unsigned char *sig, size_t siglen), (ctx, sig, siglen)) \
DEFINEFUNC_1_1_1(int, EVP_DigestVerify, (GO_EVP_MD_CTX_PTR ctx, const unsigned char *sigret, size_t siglen, const unsigned char *tbs, size_t tbslen), (ctx, sigret, siglen, tbs, tbslen)) \
DEFINEFUNC_LEGACY_1_0(int, MD5_Init, (GO_MD5_CTX_PTR c), (c)) \
DEFINEFUNC_LEGACY_1_0(int, MD5_Update, (GO_MD5_CTX_PTR c, const void *data, size_t len), (c, data, len)) \
DEFINEFUNC_LEGACY_1_0(int, MD5_Final, (unsigned char *md, GO_MD5_CTX_PTR c), (md, c)) \
Expand Down Expand Up @@ -261,6 +265,8 @@ DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_aes_256_gcm, (void), ()) \
DEFINEFUNC(void, EVP_CIPHER_CTX_free, (GO_EVP_CIPHER_CTX_PTR arg0), (arg0)) \
DEFINEFUNC(int, EVP_CIPHER_CTX_ctrl, (GO_EVP_CIPHER_CTX_PTR ctx, int type, int arg, void *ptr), (ctx, type, arg, ptr)) \
DEFINEFUNC(GO_EVP_PKEY_PTR, EVP_PKEY_new, (void), ()) \
DEFINEFUNC_1_1_1(GO_EVP_PKEY_PTR, EVP_PKEY_new_raw_private_key, (int type, GO_ENGINE_PTR e, const unsigned char *key, size_t keylen), (type, e, key, keylen)) \
DEFINEFUNC_1_1_1(GO_EVP_PKEY_PTR, EVP_PKEY_new_raw_public_key, (int type, GO_ENGINE_PTR e, const unsigned char *key, size_t keylen), (type, e, key, keylen)) \
/* EVP_PKEY_size and EVP_PKEY_get_bits pkey parameter is const since OpenSSL 1.1.1. */ \
/* Exclude it from headercheck tool when using previous OpenSSL versions. */ \
/*check:from=1.1.1*/ DEFINEFUNC_RENAMED_3_0(int, EVP_PKEY_get_size, EVP_PKEY_size, (const GO_EVP_PKEY_PTR pkey), (pkey)) \
Expand Down Expand Up @@ -355,4 +361,8 @@ DEFINEFUNC(int, PKCS5_PBKDF2_HMAC, (const char *pass, int passlen, const unsigne
DEFINEFUNC_3_0(int, EVP_PKEY_CTX_set_tls1_prf_md, (GO_EVP_PKEY_CTX_PTR arg0, const GO_EVP_MD_PTR arg1), (arg0, arg1)) \
DEFINEFUNC_3_0(int, EVP_PKEY_CTX_set1_tls1_prf_secret, (GO_EVP_PKEY_CTX_PTR arg0, const unsigned char *arg1, int arg2), (arg0, arg1, arg2)) \
DEFINEFUNC_3_0(int, EVP_PKEY_CTX_add1_tls1_prf_seed, (GO_EVP_PKEY_CTX_PTR arg0, const unsigned char *arg1, int arg2), (arg0, arg1, arg2)) \
DEFINEFUNC_1_1_1(int, EVP_PKEY_get_raw_public_key, (const GO_EVP_PKEY_PTR pkey, unsigned char *pub, size_t *len), (pkey, pub, len)) \
DEFINEFUNC_1_1_1(int, EVP_PKEY_get_raw_private_key, (const GO_EVP_PKEY_PTR pkey, unsigned char *priv, size_t *len), (pkey, priv, len)) \
DEFINEFUNC_3_0(GO_EVP_SIGNATURE_PTR, EVP_SIGNATURE_fetch, (GO_OSSL_LIB_CTX_PTR ctx, const char *algorithm, const char *properties), (ctx, algorithm, properties)) \
DEFINEFUNC_3_0(void, EVP_SIGNATURE_free, (GO_EVP_SIGNATURE_PTR signature), (signature)) \