Skip to content

Commit

Permalink
Add support for Ed25519 (#1900)
Browse files Browse the repository at this point in the history
* Add support for Ed25519

* Amended naming and added additional testing

* Added changes from Dave's review

* Next review: Fixed casing on error messages for linter | Fixed version number

* Added Dave's suggestions in docs and updated built docs

* Final push from Dave's review | Wrap crypto example in docs in quotes
  • Loading branch information
horvski authored Dec 17, 2023
1 parent 36fb3f0 commit 914960f
Show file tree
Hide file tree
Showing 6 changed files with 371 additions and 0 deletions.
81 changes: 81 additions & 0 deletions crypto/ed25519.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package crypto

import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
)

// Ed25519GenerateKey returns a random PEM encoded Ed25519 Private Key.
func Ed25519GenerateKey() ([]byte, error) {
_, secret, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generateKey: %w", err)
}
return pemEncodeEdPrivateKey(secret)
}

// Ed25519GenerateKeyFromSeed returns a PEM encoded Ed25519 Private Key from
// `seed`. Returns error if len(seed) is not ed25519.SeedSize (32).
func Ed25519GenerateKeyFromSeed(seed []byte) ([]byte, error) {
if len(seed) != ed25519.SeedSize {
return nil, fmt.Errorf("generateKeyFromSeed: incorrect seed size - given: %d wanted %d", len(seed), ed25519.SeedSize)
}
return pemEncodeEdPrivateKey(ed25519.NewKeyFromSeed(seed))
}

// Ed25519DerivePublicKey returns an ed25519 Public Key from given PEM encoded
// `privatekey`.
func Ed25519DerivePublicKey(privatekey []byte) ([]byte, error) {
secret, err := ed25519DecodeFromPEM(privatekey)
if err != nil {
return nil, fmt.Errorf("ed25519DecodeFromPEM: could not decode private key: %w", err)
}
b, err := x509.MarshalPKIXPublicKey(secret.Public())
if err != nil {
return nil, fmt.Errorf("marshalPKIXPublicKey: failed to marshal PKIX public key: %w", err)
}
return pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: b,
}), nil
}

// pemEncodeEdPrivateKey is a convenience function for PEM encoding `secret`.
func pemEncodeEdPrivateKey(secret ed25519.PrivateKey) ([]byte, error) {
der, err := x509.MarshalPKCS8PrivateKey(secret)
if err != nil {
return nil, fmt.Errorf("marshalPKCS8PrivateKey: failed to marshal ed25519 private key: %w", err)
}
block := &pem.Block{
Type: "PRIVATE KEY",
Bytes: der,
}
buf := &bytes.Buffer{}
err = pem.Encode(buf, block)
if err != nil {
return nil, fmt.Errorf("encode: PEM encoding: %w", err)
}
return buf.Bytes(), nil
}

// ed25519DecodeFromPEM returns an ed25519.PrivateKey from given PEM encoded
// `privatekey`.
func ed25519DecodeFromPEM(privatekey []byte) (ed25519.PrivateKey, error) {
block, _ := pem.Decode(privatekey)
if block == nil {
return nil, fmt.Errorf("decode: failed to read key")
}
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parsePKCS8PrivateKey: invalid private key: %w", err)
}
secret, ok := priv.(ed25519.PrivateKey)
if !ok {
return nil, fmt.Errorf("ed25519DecodeFromPEM: invalid ed25519 Private Key - given type: %T", priv)
}
return secret, nil
}
47 changes: 47 additions & 0 deletions crypto/ed25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package crypto

import (
"crypto/ed25519"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEd25519GenerateKey(t *testing.T) {
key, err := Ed25519GenerateKey()
require.NoError(t, err)
assert.True(t, strings.HasPrefix(string(key),
"-----BEGIN PRIVATE KEY-----"))
assert.True(t, strings.HasSuffix(string(key),
"-----END PRIVATE KEY-----\n"))
}

func TestEd25519DerivePublicKey(t *testing.T) {
_, err := Ed25519DerivePublicKey(nil)
assert.Error(t, err)
_, err = Ed25519DerivePublicKey([]byte(`-----BEGIN FOO-----
-----END FOO-----`))
assert.Error(t, err)

priv, err := Ed25519GenerateKey()
require.NoError(t, err)
pub, err := Ed25519DerivePublicKey(priv)
require.NoError(t, err)
block, _ := pem.Decode(pub)
assert.True(t, block != nil)
secret, err := ed25519DecodeFromPEM(priv)
require.NoError(t, err)
p, err := x509.ParsePKIXPublicKey(block.Bytes)
require.NoError(t, err)
pubKey, ok := p.(ed25519.PublicKey)
assert.True(t, ok)
assert.True(t, fmt.Sprintf("%x", p) == fmt.Sprintf("%x", secret.Public()))
msg := []byte("ed25519")
sig := ed25519.Sign(secret, msg) // Panics
assert.True(t, ed25519.Verify(pubKey, msg, sig))
}
50 changes: 50 additions & 0 deletions docs-src/content/functions/crypto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,56 @@ funcs:
+hIz6+EUt/Db51awO7iCuRly5L4TZ+CnMAsIbtUOqsqwSQDtv0AclAuogmCst75o
aztsmrD79OXXnhUlURI=
-----END PUBLIC KEY-----
- name: crypto.Ed25519GenerateKey
experimental: true
# released: v4.0.0
description: |
Generate a new Ed25519 Private Key and output in
PEM-encoded PKCS#8 ASN.1 DER form.
examples:
- |
$ gomplate -i '{{ crypto.Ed25519GenerateKey }}'
-----BEGIN PRIVATE KEY-----
...
- name: crypto.Ed25519GenerateKeyFromSeed
experimental: true
# released: v4.0.0
description: |
Generate a new Ed25519 Private Key from a random seed and output in
PEM-encoded PKCS#8 ASN.1 DER form.
pipeline: true
arguments:
- name: encoding
required: true
description: the encoding that the seed is in (`hex` or `base64`)
- name: seed
required: true
description: the random seed encoded in either base64 or hex
examples:
- |
$ gomplate -i '{{ crypto.Ed25519GenerateKeyFromSeed "base64" "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=" }}'
-----BEGIN PRIVATE KEY-----
...
- name: crypto.Ed25519DerivePublicKey
experimental: true
# released: v4.0.0
description: |
Derive a public key from an Ed25519 private key and output in PKIX
ASN.1 DER form.
pipeline: true
arguments:
- name: key
required: true
description: the private key to derive a public key from
examples:
- |
$ gomplate -i '{{ crypto.Ed25519GenerateKey | crypto.Ed25519DerivePublicKey }}'
-----BEGIN PUBLIC KEY-----
...
- |
$ gomplate -d key=priv.pem -i '{{ crypto.Ed25519DerivePublicKey (include "key") }}'
-----BEGIN PUBLIC KEY-----
...PK
- name: crypto.PBKDF2
released: v2.3.0
description: |
Expand Down
94 changes: 94 additions & 0 deletions docs/content/functions/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,100 @@ aztsmrD79OXXnhUlURI=
-----END PUBLIC KEY-----
```

## `crypto.Ed25519GenerateKey`_(unreleased)_ _(experimental)_
**Unreleased:** _This function is in development, and not yet available in released builds of gomplate._
**Experimental:** This function is [_experimental_][experimental] and may be enabled with the [`--experimental`][experimental] flag.

[experimental]: ../config/#experimental

Generate a new Ed25519 Private Key and output in
PEM-encoded PKCS#8 ASN.1 DER form.

### Usage

```
crypto.Ed25519GenerateKey
```


### Examples

```console
$ gomplate -i '{{ crypto.Ed25519GenerateKey }}'
-----BEGIN PRIVATE KEY-----
...
```

## `crypto.Ed25519GenerateKeyFromSeed`_(unreleased)_ _(experimental)_
**Unreleased:** _This function is in development, and not yet available in released builds of gomplate._
**Experimental:** This function is [_experimental_][experimental] and may be enabled with the [`--experimental`][experimental] flag.

[experimental]: ../config/#experimental

Generate a new Ed25519 Private Key from a random seed and output in
PEM-encoded PKCS#8 ASN.1 DER form.

### Usage

```
crypto.Ed25519GenerateKeyFromSeed encoding seed
```
```
seed | crypto.Ed25519GenerateKeyFromSeed encoding
```

### Arguments

| name | description |
|------|-------------|
| `encoding` | _(required)_ the encoding that the seed is in (`hex` or `base64`) |
| `seed` | _(required)_ the random seed encoded in either base64 or hex |

### Examples

```console
$ gomplate -i '{{ crypto.Ed25519GenerateKeyFromSeed "base64" "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=" }}'
-----BEGIN PRIVATE KEY-----
...
```

## `crypto.Ed25519DerivePublicKey`_(unreleased)_ _(experimental)_
**Unreleased:** _This function is in development, and not yet available in released builds of gomplate._
**Experimental:** This function is [_experimental_][experimental] and may be enabled with the [`--experimental`][experimental] flag.

[experimental]: ../config/#experimental

Derive a public key from an Ed25519 private key and output in PKIX
ASN.1 DER form.

### Usage

```
crypto.Ed25519DerivePublicKey key
```
```
key | crypto.Ed25519DerivePublicKey
```

### Arguments

| name | description |
|------|-------------|
| `key` | _(required)_ the private key to derive a public key from |

### Examples

```console
$ gomplate -i '{{ crypto.Ed25519GenerateKey | crypto.Ed25519DerivePublicKey }}'
-----BEGIN PUBLIC KEY-----
...
```
```console
$ gomplate -d key=priv.pem -i '{{ crypto.Ed25519DerivePublicKey (include "key") }}'
-----BEGIN PUBLIC KEY-----
...PK
```

## `crypto.PBKDF2`

Run the Password-Based Key Derivation Function #2 as defined in
Expand Down
49 changes: 49 additions & 0 deletions funcs/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import (
"crypto/sha1" //nolint: gosec
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"unicode/utf8"

"golang.org/x/crypto/bcrypt"

Expand Down Expand Up @@ -287,6 +290,52 @@ func (f *CryptoFuncs) ECDSADerivePublicKey(privateKey string) (string, error) {
return string(out), err
}

// Ed25519GenerateKey -
// Experimental!
func (f *CryptoFuncs) Ed25519GenerateKey() (string, error) {
if err := checkExperimental(f.ctx); err != nil {
return "", err
}
out, err := crypto.Ed25519GenerateKey()
return string(out), err
}

// Ed25519GenerateKeyFromSeed -
// Experimental!
func (f *CryptoFuncs) Ed25519GenerateKeyFromSeed(encoding, seed string) (string, error) {
if err := checkExperimental(f.ctx); err != nil {
return "", err
}
if !utf8.ValidString(seed) {
return "", fmt.Errorf("given seed is not valid UTF-8") // Don't print out seed (private).
}
var seedB []byte
var err error
switch encoding {
case "base64":
seedB, err = base64.StdEncoding.DecodeString(seed)
case "hex":
seedB, err = hex.DecodeString(seed)
default:
return "", fmt.Errorf("invalid encoding given: %s - only 'hex' or 'base64' are valid options", encoding)
}
if err != nil {
return "", fmt.Errorf("could not decode given seed: %w", err)
}
out, err := crypto.Ed25519GenerateKeyFromSeed(seedB)
return string(out), err
}

// Ed25519DerivePublicKey -
// Experimental!
func (f *CryptoFuncs) Ed25519DerivePublicKey(privateKey string) (string, error) {
if err := checkExperimental(f.ctx); err != nil {
return "", err
}
out, err := crypto.Ed25519DerivePublicKey([]byte(privateKey))
return string(out), err
}

// EncryptAES -
// Experimental!
func (f *CryptoFuncs) EncryptAES(key string, args ...interface{}) ([]byte, error) {
Expand Down
Loading

0 comments on commit 914960f

Please sign in to comment.