Skip to content

Commit

Permalink
Merge pull request #114 from golang-fips/ed25519
Browse files Browse the repository at this point in the history
Support Ed25519
  • Loading branch information
qmuntal authored Oct 13, 2023
2 parents 78dacc3 + b762fb6 commit f9fc081
Show file tree
Hide file tree
Showing 5 changed files with 381 additions and 3 deletions.
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

0 comments on commit f9fc081

Please sign in to comment.