Skip to content

Commit

Permalink
Add helpers for handling Ed25519 key (un)marshalling
Browse files Browse the repository at this point in the history
This implements the encoding of Ed25519 keys as defined in
https://datatracker.ietf.org/doc/draft-ietf-curdle-pkix-04.
  • Loading branch information
cjpatton authored and kisom committed Jul 24, 2018
1 parent e04a6dd commit 6aeb6e3
Show file tree
Hide file tree
Showing 10 changed files with 3,929 additions and 14 deletions.
22 changes: 14 additions & 8 deletions helpers/derhelpers/derhelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,27 @@ import (
"crypto/x509"

cferr "github.com/cloudflare/cfssl/errors"
"golang.org/x/crypto/ed25519"
)

// ParsePrivateKeyDER parses a PKCS #1, PKCS #8, or elliptic curve
// DER-encoded private key. The key must not be in PEM format.
// ParsePrivateKeyDER parses a PKCS #1, PKCS #8, ECDSA, or Ed25519 DER-encoded
// private key. The key must not be in PEM format.
func ParsePrivateKeyDER(keyDER []byte) (key crypto.Signer, err error) {
generalKey, err := x509.ParsePKCS8PrivateKey(keyDER)
if err != nil {
generalKey, err = x509.ParsePKCS1PrivateKey(keyDER)
if err != nil {
generalKey, err = x509.ParseECPrivateKey(keyDER)
if err != nil {
// We don't include the actual error into
// the final error. The reason might be
// we don't want to leak any info about
// the private key.
return nil, cferr.New(cferr.PrivateKeyError,
cferr.ParseFailed)
generalKey, err = ParseEd25519PrivateKey(keyDER)
if err != nil {
// We don't include the actual error into
// the final error. The reason might be
// we don't want to leak any info about
// the private key.
return nil, cferr.New(cferr.PrivateKeyError,
cferr.ParseFailed)
}
}
}
}
Expand All @@ -35,6 +39,8 @@ func ParsePrivateKeyDER(keyDER []byte) (key crypto.Signer, err error) {
return generalKey.(*rsa.PrivateKey), nil
case *ecdsa.PrivateKey:
return generalKey.(*ecdsa.PrivateKey), nil
case ed25519.PrivateKey:
return generalKey.(ed25519.PrivateKey), nil
}

// should never reach here
Expand Down
133 changes: 133 additions & 0 deletions helpers/derhelpers/ed25519.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package derhelpers

import (
"crypto"
"crypto/x509/pkix"
"encoding/asn1"
"errors"

"golang.org/x/crypto/ed25519"
)

var errEd25519WrongID = errors.New("incorrect object identifier")
var errEd25519WrongKeyType = errors.New("incorrect key type")

// ed25519OID is the OID for the Ed25519 signature scheme: see
// https://datatracker.ietf.org/doc/draft-ietf-curdle-pkix-04.
var ed25519OID = asn1.ObjectIdentifier{1, 3, 101, 112}

// subjectPublicKeyInfo reflects the ASN.1 object defined in the X.509 standard.
//
// This is defined in crypto/x509 as "publicKeyInfo".
type subjectPublicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}

// MarshalEd25519PublicKey creates a DER-encoded SubjectPublicKeyInfo for an
// ed25519 public key, as defined in
// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04. This is analagous to
// MarshalPKIXPublicKey in crypto/x509, which doesn't currently support Ed25519.
func MarshalEd25519PublicKey(pk crypto.PublicKey) ([]byte, error) {
pub, ok := pk.(ed25519.PublicKey)
if !ok {
return nil, errEd25519WrongKeyType
}

spki := subjectPublicKeyInfo{
Algorithm: pkix.AlgorithmIdentifier{
Algorithm: ed25519OID,
},
PublicKey: asn1.BitString{
BitLength: len(pub) * 8,
Bytes: pub,
},
}

return asn1.Marshal(spki)
}

// ParseEd25519PublicKey returns the Ed25519 public key encoded by the input.
func ParseEd25519PublicKey(der []byte) (crypto.PublicKey, error) {
var spki subjectPublicKeyInfo
if rest, err := asn1.Unmarshal(der, &spki); err != nil {
return nil, err
} else if len(rest) > 0 {
return nil, errors.New("SubjectPublicKeyInfo too long")
}

if !spki.Algorithm.Algorithm.Equal(ed25519OID) {
return nil, errEd25519WrongID
}

if spki.PublicKey.BitLength != ed25519.PublicKeySize*8 {
return nil, errors.New("SubjectPublicKeyInfo PublicKey length mismatch")
}

return ed25519.PublicKey(spki.PublicKey.Bytes), nil
}

// oneAsymmetricKey reflects the ASN.1 structure for storing private keys in
// https://tools.ietf.org/html/draft-ietf-curdle-pkix-04, excluding the optional
// fields, which we don't use here.
//
// This is identical to pkcs8 in crypto/x509.
type oneAsymmetricKey struct {
Version int
Algorithm pkix.AlgorithmIdentifier
PrivateKey []byte
}

// curvePrivateKey is the innter type of the PrivateKey field of
// oneAsymmetricKey.
type curvePrivateKey []byte

// MarshalEd25519PrivateKey returns a DER encdoing of the input private key as
// specified in https://tools.ietf.org/html/draft-ietf-curdle-pkix-04.
func MarshalEd25519PrivateKey(sk crypto.PrivateKey) ([]byte, error) {
priv, ok := sk.(ed25519.PrivateKey)
if !ok {
return nil, errEd25519WrongKeyType
}

// Marshal the innter CurvePrivateKey.
curvePrivateKey, err := asn1.Marshal(priv.Seed())
if err != nil {
return nil, err
}

// Marshal the OneAsymmetricKey.
asym := oneAsymmetricKey{
Version: 0,
Algorithm: pkix.AlgorithmIdentifier{
Algorithm: ed25519OID,
},
PrivateKey: curvePrivateKey,
}
return asn1.Marshal(asym)
}

// ParseEd25519PrivateKey returns the Ed25519 private key encoded by the input.
func ParseEd25519PrivateKey(der []byte) (crypto.PrivateKey, error) {
asym := new(oneAsymmetricKey)
if rest, err := asn1.Unmarshal(der, asym); err != nil {
return nil, err
} else if len(rest) > 0 {
return nil, errors.New("OneAsymmetricKey too long")
}

// Check that the key type is correct.
if !asym.Algorithm.Algorithm.Equal(ed25519OID) {
return nil, errEd25519WrongID
}

// Unmarshal the inner CurvePrivateKey.
seed := new(curvePrivateKey)
if rest, err := asn1.Unmarshal(asym.PrivateKey, seed); err != nil {
return nil, err
} else if len(rest) > 0 {
return nil, errors.New("CurvePrivateKey too long")
}

return ed25519.NewKeyFromSeed(*seed), nil
}
99 changes: 99 additions & 0 deletions helpers/derhelpers/ed25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package derhelpers

import (
"bytes"
"encoding/pem"
"testing"

"golang.org/x/crypto/ed25519"
)

var testPubKey = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=
-----END PUBLIC KEY-----
`

var testPrivKey = `-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC
-----END PRIVATE KEY-----`

func TestParseMarshalEd25519PublicKey(t *testing.T) {
block, rest := pem.Decode([]byte(testPubKey))
if len(rest) > 0 {
t.Fatal("pem.Decode(); len(rest) > 0, want 0")
}

pk, err := ParseEd25519PublicKey(block.Bytes)
if err != nil {
t.Fatal(err)
}

if pkLen := len(pk.(ed25519.PublicKey)); pkLen != 32 {
t.Fatalf("len(pk): got %d: want %d", pkLen, 32)
}

der, err := MarshalEd25519PublicKey(pk)
if err != nil {
t.Fatal(err)
}

if !bytes.Equal(der, block.Bytes) {
t.Errorf("got %d bytes:\n%v \nwant %d bytes:\n%v",
len(der), der, len(block.Bytes), block.Bytes)
}
}

func TestParseMarshalEd25519PrivateKey(t *testing.T) {
block, rest := pem.Decode([]byte(testPrivKey))
if len(rest) > 0 {
t.Fatal("pem.Decode(); len(rest) > 0, want 0")
}

sk, err := ParseEd25519PrivateKey(block.Bytes)
if err != nil {
t.Fatal(err)
}

if skLen := len(sk.(ed25519.PrivateKey)); skLen != 64 {
t.Fatalf("len(sk): got %d: want %d", skLen, 64)
}

der, err := MarshalEd25519PrivateKey(sk)
if err != nil {
t.Fatal(err)
}

if !bytes.Equal(der, block.Bytes) {
t.Errorf("got %d bytes:\n%v \nwant %d bytes:\n%v",
len(der), der, len(block.Bytes), block.Bytes)
}
}

func TestKeyPair(t *testing.T) {
block, rest := pem.Decode([]byte(testPrivKey))
if len(rest) > 0 {
t.Fatal("pem.Decode(); len(rest) > 0, want 0")
}

sk, err := ParseEd25519PrivateKey(block.Bytes)
if err != nil {
t.Fatal(err)
}

block, rest = pem.Decode([]byte(testPubKey))
if len(rest) > 0 {
t.Fatal("pem.Decode(); len(rest) > 0, want 0")
}

pub, err := ParseEd25519PublicKey(block.Bytes)
if err != nil {
t.Fatal(err)
}

pk := pub.(ed25519.PublicKey)
pk2 := sk.(ed25519.PrivateKey).Public().(ed25519.PublicKey)
if !bytes.Equal(pk, pk2) {
t.Errorf("pk %d bytes:\n%v \nsk.Public() %d bytes:\n%v",
len(pk), pk, len(pk2), pk2)
}
}
34 changes: 28 additions & 6 deletions helpers/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
testEmptyCertFile = "testdata/emptycert.pem"
testPrivateRSAKey = "testdata/priv_rsa_key.pem"
testPrivateECDSAKey = "testdata/private_ecdsa_key.pem"
testPrivateEd25519Key = "testdata/private_ed25519_key.pem"
testUnsupportedECDSAKey = "testdata/secp256k1-key.pem"
testMessedUpPrivateKey = "testdata/messed_up_priv_key.pem"
testEncryptedPrivateKey = "testdata/enc_priv_key.pem"
Expand Down Expand Up @@ -318,14 +319,20 @@ func TestParseCertificatesPEM(t *testing.T) {
}

func TestSelfSignedCertificatePEM(t *testing.T) {
testPEM, _ := ioutil.ReadFile(testCertFile)
_, err := ParseSelfSignedCertificatePEM(testPEM)
testPEM, err := ioutil.ReadFile(testCertFile)
if err != nil {
t.Fatal(err)
}
_, err = ParseSelfSignedCertificatePEM(testPEM)
if err != nil {
t.Fatalf("%v", err)
}

// a few lines deleted from the pem file
wrongPEM, _ := ioutil.ReadFile(testMessedUpCertFile)
wrongPEM, err := ioutil.ReadFile(testMessedUpCertFile)
if err != nil {
t.Fatal(err)
}
_, err2 := ParseSelfSignedCertificatePEM(wrongPEM)
if err2 == nil {
t.Fatal("Improper pem file failed to raise an error")
Expand All @@ -345,18 +352,33 @@ func TestSelfSignedCertificatePEM(t *testing.T) {
func TestParsePrivateKeyPEM(t *testing.T) {

// expected cases
testRSAPEM, _ := ioutil.ReadFile(testPrivateRSAKey)
_, err := ParsePrivateKeyPEM(testRSAPEM)
testRSAPEM, err := ioutil.ReadFile(testPrivateRSAKey)
if err != nil {
t.Fatal(err)
}
_, err = ParsePrivateKeyPEM(testRSAPEM)
if err != nil {
t.Fatal(err)
}

testECDSAPEM, _ := ioutil.ReadFile(testPrivateECDSAKey)
testECDSAPEM, err := ioutil.ReadFile(testPrivateECDSAKey)
if err != nil {
t.Fatal(err)
}
_, err = ParsePrivateKeyPEM(testECDSAPEM)
if err != nil {
t.Fatal(err)
}

testEd25519PEM, err := ioutil.ReadFile(testPrivateEd25519Key)
if err != nil {
t.Fatal(err)
}
_, err = ParsePrivateKeyPEM(testEd25519PEM)
if err != nil {
t.Fatal(err)
}

// error cases
errCases := []string{
testMessedUpPrivateKey, // a few lines deleted
Expand Down
3 changes: 3 additions & 0 deletions helpers/testdata/private_ed25519_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIEHfDC6M85b3uVU5GO82Y5D6Qkx5YehoCe2T1auTbTFN
-----END PRIVATE KEY-----
Loading

0 comments on commit 6aeb6e3

Please sign in to comment.