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 all 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
8 changes: 6 additions & 2 deletions cgo_go122.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +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_EncryptUpdate
#cgo nocallback go_openssl_EVP_EncryptUpdate
#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"
216 changes: 216 additions & 0 deletions ed25519.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
//go:build !cmd_go_bootstrap

package openssl

// #include "goopenssl.h"
import "C"
import (
"errors"
"runtime"
"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
}

type PublicKeyEd25519 struct {
_pkey C.GO_EVP_PKEY_PTR
}

func (k *PublicKeyEd25519) finalize() {
C.go_openssl_EVP_PKEY_free(k._pkey)
}

func (k *PublicKeyEd25519) Bytes() ([]byte, error) {
defer runtime.KeepAlive(k)
pub := make([]byte, publicKeySizeEd25519)
if err := extractPKEYPubEd25519(k._pkey, pub); err != nil {
return nil, err
}
return pub, nil
}

type PrivateKeyEd25519 struct {
_pkey C.GO_EVP_PKEY_PTR
}

func (k *PrivateKeyEd25519) finalize() {
C.go_openssl_EVP_PKEY_free(k._pkey)
}

func (k *PrivateKeyEd25519) Bytes() ([]byte, error) {
defer runtime.KeepAlive(k)
priv := make([]byte, privateKeySizeEd25519)
if err := extractPKEYPrivEd25519(k._pkey, priv); err != nil {
return nil, err
}
return priv, nil
}

// GenerateKeyEd25519 generates a public/private key pair.
func GenerateKeyEd25519() (*PublicKeyEd25519, *PrivateKeyEd25519, error) {
pkeyPriv, err := generateEVPPKey(C.GO_EVP_PKEY_ED25519, 0, "")
if err != nil {
return nil, nil, err
}
pub := make([]byte, publicKeySizeEd25519)
if err := extractPKEYPubEd25519(pkeyPriv, pub); err != nil {
C.go_openssl_EVP_PKEY_free(pkeyPriv)
return nil, nil, err
}
pubk, err := NewPublicKeyEd25119(pub)
if err != nil {
C.go_openssl_EVP_PKEY_free(pkeyPriv)
return nil, nil, err
}
privk := &PrivateKeyEd25519{_pkey: pkeyPriv}
runtime.SetFinalizer(privk, (*PrivateKeyEd25519).finalize)
return pubk, privk, nil
}

func NewPrivateKeyEd25119(priv []byte) (*PrivateKeyEd25519, error) {
if len(priv) != privateKeySizeEd25519 {
panic("ed25519: bad private key length: " + strconv.Itoa(len(priv)))
}
return NewPrivateKeyEd25519FromSeed(priv[:seedSizeEd25519])
}

func NewPublicKeyEd25119(pub []byte) (*PublicKeyEd25519, 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), C.size_t(len(pub)))
if pkey == nil {
return nil, newOpenSSLError("EVP_PKEY_new_raw_public_key")
}
pubk := &PublicKeyEd25519{_pkey: pkey}
runtime.SetFinalizer(pubk, (*PublicKeyEd25519).finalize)
return pubk, nil
}

// NewPrivateKeyEd25519FromSeed calculates a private key from a seed. It will panic if
// len(seed) is not [SeedSize]. RFC 8032's private keys correspond to seeds in this
// package.
func NewPrivateKeyEd25519FromSeed(seed []byte) (*PrivateKeyEd25519, 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 nil, newOpenSSLError("EVP_PKEY_new_raw_private_key")
}
priv := &PrivateKeyEd25519{_pkey: pkey}
runtime.SetFinalizer(priv, (*PrivateKeyEd25519).finalize)
return priv, nil
}

func extractPKEYPubEd25519(pkey C.GO_EVP_PKEY_PTR, pub []byte) error {
pubSize := C.size_t(publicKeySizeEd25519)
if C.go_openssl_EVP_PKEY_get_raw_public_key(pkey, base(pub), &pubSize) != 1 {
return newOpenSSLError("EVP_PKEY_get_raw_public_key")
}
if pubSize != publicKeySizeEd25519 {
return errors.New("ed25519: bad public key length: " + strconv.Itoa(int(pubSize)))
}
return nil
}

func extractPKEYPrivEd25519(pkey C.GO_EVP_PKEY_PTR, priv []byte) error {
if err := extractPKEYPubEd25519(pkey, priv[seedSizeEd25519:]); err != nil {
return err
}
privSize := C.size_t(seedSizeEd25519)
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 {
return errors.New("ed25519: bad private key length: " + strconv.Itoa(int(privSize)))
}
return nil
}

// SignEd25519 signs the message with priv and returns a signature.
func SignEd25519(priv *PrivateKeyEd25519, message []byte) (sig []byte, err error) {
// Outline the function body so that the returned key can be stack-allocated.
sig = make([]byte, signatureSizeEd25519)
err = signEd25519(priv, sig, message)
if err != nil {
return nil, err
}
return sig, err
}

func signEd25519(priv *PrivateKeyEd25519, sig, message []byte) error {
defer runtime.KeepAlive(priv)
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, priv._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 {
return errors.New("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 *PublicKeyEd25519, message, sig []byte) error {
defer runtime.KeepAlive(pub)
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, pub._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
}
148 changes: 148 additions & 0 deletions ed25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
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.NewPrivateKeyEd25519FromSeed(seed)
if err != nil {
t.Fatal(err)
}
data, err := priv.Bytes()
if err != nil {
t.Fatal(err)
}
priv2 := ed25519.NewKeyFromSeed(seed)
if !bytes.Equal(data, []byte(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)
}
privData, err := private.Bytes()
if err != nil {
t.Fatal(err)
}
if sig2 := ed25519.Sign(privData, 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,
}

pub, err := openssl.NewPublicKeyEd25119(publicKey)
if err != nil {
t.Fatal(err)
}

if openssl.VerifyEd25519(pub, 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.NewPrivateKeyEd25519FromSeed(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
Loading