diff --git a/crypto/ed25519.go b/crypto/ed25519.go new file mode 100644 index 000000000..a6630ad79 --- /dev/null +++ b/crypto/ed25519.go @@ -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 +} diff --git a/crypto/ed25519_test.go b/crypto/ed25519_test.go new file mode 100644 index 000000000..027c4697a --- /dev/null +++ b/crypto/ed25519_test.go @@ -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)) +} diff --git a/docs-src/content/functions/crypto.yml b/docs-src/content/functions/crypto.yml index f7640baf3..3e5754fbb 100644 --- a/docs-src/content/functions/crypto.yml +++ b/docs-src/content/functions/crypto.yml @@ -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: | diff --git a/docs/content/functions/crypto.md b/docs/content/functions/crypto.md index 1720d5055..9f6393f17 100644 --- a/docs/content/functions/crypto.md +++ b/docs/content/functions/crypto.md @@ -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 diff --git a/funcs/crypto.go b/funcs/crypto.go index 41c752c78..e3628fadf 100644 --- a/funcs/crypto.go +++ b/funcs/crypto.go @@ -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" @@ -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) { diff --git a/funcs/crypto_test.go b/funcs/crypto_test.go index 90f6bdff5..4d1b56608 100644 --- a/funcs/crypto_test.go +++ b/funcs/crypto_test.go @@ -2,6 +2,7 @@ package funcs import ( "context" + "encoding/base64" "strconv" "strings" "testing" @@ -167,6 +168,55 @@ func TestECDSADerivePublicKey(t *testing.T) { "-----END PUBLIC KEY-----\n")) } +func TestEd25519GenerateKey(t *testing.T) { + c := testCryptoNS() + key, err := c.Ed25519GenerateKey() + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(key, + "-----BEGIN PRIVATE KEY-----")) + assert.True(t, strings.HasSuffix(key, + "-----END PRIVATE KEY-----\n")) +} + +func TestEd25519GenerateKeyFromSeed(t *testing.T) { + c := testCryptoNS() + enc := "" + seed := "" + _, err := c.Ed25519GenerateKeyFromSeed(enc, seed) + assert.Error(t, err) + + enc = "base64" + seed = "0000000000000000000000000000000" // 31 bytes, instead of wanted 32. + _, err = c.Ed25519GenerateKeyFromSeed(enc, seed) + assert.Error(t, err) + + seed += "0" // 32 bytes. + b64seed := base64.StdEncoding.EncodeToString([]byte(seed)) + key, err := c.Ed25519GenerateKeyFromSeed(enc, b64seed) + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(key, + "-----BEGIN PRIVATE KEY-----")) + assert.True(t, strings.HasSuffix(key, + "-----END PRIVATE KEY-----\n")) +} + +func TestEd25519DerivePublicKey(t *testing.T) { + c := testCryptoNS() + + _, err := c.Ed25519DerivePublicKey("") + assert.Error(t, err) + + key, _ := c.Ed25519GenerateKey() + pub, err := c.Ed25519DerivePublicKey(key) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(pub, + "-----BEGIN PUBLIC KEY-----")) + assert.True(t, strings.HasSuffix(pub, + "-----END PUBLIC KEY-----\n")) +} + func TestRSACrypt(t *testing.T) { t.Parallel()