diff --git a/userenckey/gen.go b/userenckey/gen.go
new file mode 100644
index 0000000000000000000000000000000000000000..f3cb4ceebec5f7b830829e1509f9346d32381c86
--- /dev/null
+++ b/userenckey/gen.go
@@ -0,0 +1,37 @@
+package userenckey
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/x509"
+	"encoding/pem"
+)
+
+func encodePublicKeyToPEM(pub *ecdsa.PublicKey) ([]byte, error) {
+	der, err := x509.MarshalPKIXPublicKey(pub)
+	if err != nil {
+		return nil, err
+	}
+	return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der}), nil
+}
+
+// GenerateKey generates a new ECDSA key pair, and returns the
+// PEM-encoded public and private key (in order).
+func GenerateKey() ([]byte, []byte, error) {
+	pkey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	privBytes, err := encodePrivateKeyToPEM(pkey)
+	if err != nil {
+		return nil, nil, err
+	}
+	pubBytes, err := encodePublicKeyToPEM(&pkey.PublicKey)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return pubBytes, privBytes, nil
+}
diff --git a/userenckey/gen_test.go b/userenckey/gen_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..1574845bb7fd995c256e2698389e0951d6af91f3
--- /dev/null
+++ b/userenckey/gen_test.go
@@ -0,0 +1,20 @@
+package userenckey
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestGenerateKey(t *testing.T) {
+	pub, priv, err := GenerateKey()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !bytes.HasPrefix(pub, []byte("-----BEGIN PUBLIC KEY-----")) {
+		t.Errorf("bad public key: %s", string(pub))
+	}
+	if !bytes.HasPrefix(priv, []byte("-----BEGIN PRIVATE KEY-----")) {
+		t.Errorf("bad private key: %s", string(priv))
+	}
+}
diff --git a/userenckey/pkcs8.go b/userenckey/pkcs8.go
new file mode 100644
index 0000000000000000000000000000000000000000..f7f61215b15bc33aa338aba28a37c699a85546b8
--- /dev/null
+++ b/userenckey/pkcs8.go
@@ -0,0 +1,17 @@
+// +build go1.10
+
+package userenckey
+
+import (
+	"crypto/ecdsa"
+	"encoding/pem"
+)
+
+// Encode a private key to PEM-encoded PKCS8.
+func encodePrivateKeyToPEM(priv *ecdsa.PrivateKey) ([]byte, error) {
+	der, err := x509.MarshalPKCS8PrivateKey(priv)
+	if err != nil {
+		return nil, err
+	}
+	return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil
+}
diff --git a/userenckey/pkcs8_compat.go b/userenckey/pkcs8_compat.go
new file mode 100644
index 0000000000000000000000000000000000000000..a63b6f8621a8910b16f0b774ef41b978f4287544
--- /dev/null
+++ b/userenckey/pkcs8_compat.go
@@ -0,0 +1,27 @@
+// +build !go1.10
+
+package userenckey
+
+import (
+	"bytes"
+	"crypto/ecdsa"
+	"crypto/x509"
+	"encoding/pem"
+	"os/exec"
+)
+
+// Encode a private key to PEM-encoded PKCS8.
+//
+// In Go versions prior to 1.10, we must shell out to openssl to
+// convert the private key to PKCS8 format.
+func encodePrivateKeyToPEM(priv *ecdsa.PrivateKey) ([]byte, error) {
+	der, err := x509.MarshalECPrivateKey(priv)
+	if err != nil {
+		return nil, err
+	}
+	pkcs1 := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der})
+
+	cmd := exec.Command("/usr/bin/openssl", "pkey")
+	cmd.Stdin = bytes.NewReader(pkcs1)
+	return cmd.Output()
+}