Skip to content

Commit

Permalink
Add support for elliptic curve key algorithm (#4)
Browse files Browse the repository at this point in the history
closes #3
  • Loading branch information
tsaarni authored Jul 19, 2020
1 parent b2a9b54 commit e9dc12d
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 40 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ Writing state: certs.state
| --- | ----------- | -------- |
| subject | Distinguished name for the certificate. `subject` is the only mandatory field. | `CN=Joe` |
| sans | List of values for x509 Subject Alternative Name extension. | `DNS:www.example.com`, `IP:1.2.3.4`, `URI:https://www.example.com` |
| key_size | RSA key size. Default value is 2048 if `key_size` is not defined. | 4096 |
| key_type | Certificate key algorithm. Default value is `EC` (elliptic curve). | `EC` or `RSA` |
| key_size | The key length in bits. Default value is 256 if `key_size` is not defined. | For key_type EC: `256`, `384`, `521`. For key_type RSA: `1024`, `2048`, `4096` |
| expires | Certificate NotAfter field is calculated by adding duration defined in `expires` to current time. Default value is 8760h (one year) if `expires` is not defined. `not_after` takes precedence over `expires`. | `1s`, `10m`, `1h` |
| key_usages | List of values for x509 key usage extension. If `key_usages` is not defined, `CertSign` and `CRLSign` are set for CA certificates, `KeyEncipherment` and `DigitalSignature` are set for end-entity certificates. | `DigitalSignature`, `ContentCommitment`, `KeyEncipherment`, `DataEncipherment`, `KeyAgreement`, `CertSign`, `CRLSign`, `EncipherOnly`, `DecipherOnly` |
| issuer | Distinguished name of the issuer. Issuer must be declared as a certificate in the manifest file before referring to it as issuer. Self-signed certificate is generated if `issuer` is not defined. | `CN=myca` |
Expand Down
84 changes: 69 additions & 15 deletions pkg/certificate/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
package certificate

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
Expand All @@ -38,6 +41,7 @@ import (
type Certificate struct {
Subject string `yaml:"subject"`
SubjectAltName []string `yaml:"sans"`
KeyType string `yaml:"key_type"`
KeySize int `yaml:"key_size"`
Expires string
KeyUsage []string `yaml:"key_usages"`
Expand All @@ -48,8 +52,8 @@ type Certificate struct {
NotAfter *time.Time `yaml:"not_after"`

// generated at runtime, not read from yaml
rsaKey *rsa.PrivateKey `yaml:"-"`
cert []byte `yaml:"-"`
Key crypto.Signer `yaml:"-"`
Cert []byte `yaml:"-"`
}

// getKeyUsage converts key usage string representation to x509.KeyUsage
Expand Down Expand Up @@ -78,6 +82,23 @@ func getKeyUsage(keyUsage []string) (x509.KeyUsage, error) {
return result, nil
}

const (
ecKey string = "EC"
rsaKey = "RSA"
)

func normalizeKeyType(keyType string) (string, error) {
if keyType == "" {
return ecKey, nil
} else if strings.EqualFold(keyType, "EC") {
return ecKey, nil
} else if strings.EqualFold(keyType, "RSA") {
return rsaKey, nil
} else {
return "", fmt.Errorf("Invalid key type %s", keyType)
}
}

// Defaults sets the default values to Certificate fields that may be overwritten by the fields in the certificate manifest file
func (c *Certificate) defaults() error {
if c.Subject == "" {
Expand All @@ -93,8 +114,17 @@ func (c *Certificate) defaults() error {
return errors.New("Subject must contain CN")
}

c.KeyType, err = normalizeKeyType(c.KeyType)
if err != nil {
return err
}

if c.KeySize == 0 {
c.KeySize = 2048
if c.KeyType == ecKey {
c.KeySize = 256
} else if c.KeyType == rsaKey {
c.KeySize = 2048
}
}

if c.Expires == "" && c.NotAfter == nil {
Expand Down Expand Up @@ -133,7 +163,23 @@ func (c *Certificate) Generate(ca *Certificate) error {
return err
}

c.rsaKey, err = rsa.GenerateKey(rand.Reader, c.KeySize)
if c.KeyType == ecKey {
var curve elliptic.Curve
switch c.KeySize {
case 256:
curve = elliptic.P256()
case 384:
curve = elliptic.P384()
case 521:
curve = elliptic.P521()
default:
return fmt.Errorf("Invalid EC key size: %d (valid: 256, 384, 521)", c.KeySize)
}
c.Key, err = ecdsa.GenerateKey(curve, rand.Reader)
} else if c.KeyType == rsaKey {
c.Key, err = rsa.GenerateKey(rand.Reader, c.KeySize)
}

if err != nil {
return err
}
Expand Down Expand Up @@ -189,16 +235,17 @@ func (c *Certificate) Generate(ca *Certificate) error {
}

var issuerCert *x509.Certificate
var issuerKey interface{}
var issuerKey crypto.Signer
if ca != nil {
issuerCert, err = x509.ParseCertificate(ca.cert)
issuerKey = ca.rsaKey
issuerCert, err = x509.ParseCertificate(ca.Cert)
issuerKey = ca.Key
} else {
// create self-signed certificate
issuerCert = template
issuerKey = c.rsaKey
issuerKey = c.Key
}

c.cert, err = x509.CreateCertificate(rand.Reader, template, issuerCert, &c.rsaKey.PublicKey, issuerKey)
c.Cert, err = x509.CreateCertificate(rand.Reader, template, issuerCert, c.Key.Public(), issuerKey)

return err
}
Expand All @@ -217,7 +264,7 @@ func (c *Certificate) Save(dstdir string) error {

pem.Encode(cf, &pem.Block{
Type: "CERTIFICATE",
Bytes: c.cert,
Bytes: c.Cert,
})

kf, err := os.Create(keyFilename)
Expand All @@ -226,9 +273,14 @@ func (c *Certificate) Save(dstdir string) error {
}
defer kf.Close()

bytes, err := x509.MarshalPKCS8PrivateKey(c.Key)
if err != nil {
return err
}

pem.Encode(kf, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(c.rsaKey),
Type: "PRIVATE KEY",
Bytes: bytes,
})

return nil
Expand All @@ -252,20 +304,22 @@ func (c *Certificate) Load(srcdir string) error {
if decoded == nil || decoded.Type != "CERTIFICATE" {
return fmt.Errorf("Error while decoding %s", certFilename)
}
c.cert = decoded.Bytes
c.Cert = decoded.Bytes

buf, err = ioutil.ReadFile(keyFilename)
if err != nil {
return err
}
decoded, _ = pem.Decode(buf)
if decoded == nil || decoded.Type != "RSA PRIVATE KEY" {
if decoded == nil || decoded.Type != "PRIVATE KEY" {
return fmt.Errorf("Error while decoding %s", keyFilename)
}
c.rsaKey, err = x509.ParsePKCS1PrivateKey(decoded.Bytes)

key, err := x509.ParsePKCS8PrivateKey(decoded.Bytes)
if err != nil {
return err
}
c.Key = key.(crypto.Signer)

return nil
}
Expand Down
78 changes: 54 additions & 24 deletions pkg/certificate/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
package certificate

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"io/ioutil"
"net"
Expand All @@ -30,7 +33,7 @@ func TestSubjectName(t *testing.T) {
input := Certificate{Subject: "CN=Joe"}
err := input.Generate(nil)
assert.Nil(t, err)
got, err := x509.ParseCertificate(input.cert)
got, err := x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, "Joe", got.Subject.CommonName)
}
Expand All @@ -39,36 +42,53 @@ func TestSubjectAltName(t *testing.T) {
input := Certificate{Subject: "CN=Joe", SubjectAltName: []string{"DNS:host.example.com", "URI:http://www.example.com", "IP:1.2.3.4"}}
err := input.Generate(nil)
assert.Nil(t, err)
got, err := x509.ParseCertificate(input.cert)
got, err := x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, "Joe", got.Subject.CommonName)
assert.Equal(t, "host.example.com", got.DNSNames[0])
assert.Equal(t, url.URL{Scheme: "http", Host: "www.example.com"}, *got.URIs[0])
assert.Equal(t, net.IP{1, 2, 3, 4}, got.IPAddresses[0])
}

func TestKeySize(t *testing.T) {
got := Certificate{Subject: "CN=Joe", KeySize: 1024}
func TestEcKeySize(t *testing.T) {
got := Certificate{Subject: "CN=Joe", KeyType: "EC", KeySize: 256}
err := got.Generate(nil)
assert.Nil(t, err)
assert.Equal(t, 1024, got.rsaKey.Size()*8)
assert.Equal(t, elliptic.P256(), got.Key.Public().(*ecdsa.PublicKey).Curve)

got = Certificate{Subject: "CN=Joe", KeySize: 2048}
got = Certificate{Subject: "CN=Joe", KeyType: "EC", KeySize: 384}
err = got.Generate(nil)
assert.Nil(t, err)
assert.Equal(t, 2048, got.rsaKey.Size()*8)
assert.Equal(t, elliptic.P384(), got.Key.Public().(*ecdsa.PublicKey).Curve)

got = Certificate{Subject: "CN=Joe", KeySize: 4096}
got = Certificate{Subject: "CN=Joe", KeyType: "EC", KeySize: 521}
err = got.Generate(nil)
assert.Nil(t, err)
assert.Equal(t, 4096, got.rsaKey.Size()*8)
assert.Equal(t, elliptic.P521(), got.Key.Public().(*ecdsa.PublicKey).Curve)
}

func TestRsaKeySize(t *testing.T) {
got := Certificate{Subject: "CN=Joe", KeyType: "RSA", KeySize: 1024}
err := got.Generate(nil)
assert.Nil(t, err)
assert.Equal(t, 1024, got.Key.Public().(*rsa.PublicKey).Size()*8)

got = Certificate{Subject: "CN=Joe", KeyType: "RSA", KeySize: 2048}
err = got.Generate(nil)
assert.Nil(t, err)
assert.Equal(t, 2048, got.Key.Public().(*rsa.PublicKey).Size()*8)

got = Certificate{Subject: "CN=Joe", KeyType: "RSA", KeySize: 4096}
err = got.Generate(nil)
assert.Nil(t, err)
assert.Equal(t, 4096, got.Key.Public().(*rsa.PublicKey).Size()*8)
}

func TestExpires(t *testing.T) {
input := Certificate{Subject: "CN=Joe", Expires: "1h"}
err := input.Generate(nil)
assert.Nil(t, err)
cert, err := x509.ParseCertificate(input.cert)
cert, err := x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
want, _ := time.ParseDuration("1h")
got := cert.NotAfter.Sub(cert.NotBefore)
Expand All @@ -79,21 +99,21 @@ func TestKeyUsage(t *testing.T) {
input := Certificate{Subject: "CN=Joe", KeyUsage: []string{"DigitalSignature"}}
err := input.Generate(nil)
assert.Nil(t, err)
got, err := x509.ParseCertificate(input.cert)
got, err := x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, x509.KeyUsageDigitalSignature, got.KeyUsage)

input = Certificate{Subject: "CN=Joe", KeyUsage: []string{"DigitalSignature", "KeyEncipherment"}}
err = input.Generate(nil)
assert.Nil(t, err)
got, err = x509.ParseCertificate(input.cert)
got, err = x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment, got.KeyUsage)

input = Certificate{Subject: "CN=Joe", KeyUsage: []string{"DigitalSignature", "ContentCommitment", "KeyEncipherment", "DataEncipherment", "KeyAgreement", "CertSign", "CRLSign", "EncipherOnly", "DecipherOnly"}}
err = input.Generate(nil)
assert.Nil(t, err)
got, err = x509.ParseCertificate(input.cert)
got, err = x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, x509.KeyUsageDigitalSignature|x509.KeyUsageContentCommitment|x509.KeyUsageKeyEncipherment|x509.KeyUsageDataEncipherment|x509.KeyUsageKeyAgreement|x509.KeyUsageCertSign|x509.KeyUsageCRLSign|x509.KeyUsageEncipherOnly|x509.KeyUsageDecipherOnly, got.KeyUsage)
}
Expand All @@ -102,7 +122,7 @@ func TestIssuer(t *testing.T) {
input1 := Certificate{Subject: "CN=Joe"}
err := input1.Generate(nil)
assert.Nil(t, err)
got, err := x509.ParseCertificate(input1.cert)
got, err := x509.ParseCertificate(input1.Cert)
assert.Nil(t, err)
assert.Equal(t, "Joe", got.Subject.CommonName)
assert.Equal(t, "Joe", got.Issuer.CommonName)
Expand All @@ -111,7 +131,7 @@ func TestIssuer(t *testing.T) {
input2 := Certificate{Subject: "CN=EndEntity", Issuer: "CN:Joe"}
err = input2.Generate(&input1)
assert.Nil(t, err)
got, err = x509.ParseCertificate(input2.cert)
got, err = x509.ParseCertificate(input2.Cert)
assert.Nil(t, err)
assert.Equal(t, "EndEntity", got.Subject.CommonName)
assert.Equal(t, "Joe", got.Issuer.CommonName)
Expand All @@ -132,7 +152,7 @@ func TestFilename(t *testing.T) {
input := Certificate{Subject: "CN=dummy", Filename: "Joe"}
err = input.Load(dir)
assert.Nil(t, err)
cert, err := x509.ParseCertificate(input.cert)
cert, err := x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, "Joe", cert.Subject.CommonName)

Expand All @@ -145,7 +165,7 @@ func TestFilename(t *testing.T) {
input = Certificate{Subject: "CN=dummy", Filename: "mycert"}
err = input.Load(dir)
assert.Nil(t, err)
cert, err = x509.ParseCertificate(input.cert)
cert, err = x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, "Jane", cert.Subject.CommonName)
}
Expand All @@ -154,7 +174,7 @@ func TestIsCa(t *testing.T) {
input1 := Certificate{Subject: "CN=Joe"}
err := input1.Generate(nil)
assert.Nil(t, err)
got, err := x509.ParseCertificate(input1.cert)
got, err := x509.ParseCertificate(input1.Cert)
assert.Nil(t, err)
assert.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign, got.KeyUsage)
assert.Equal(t, true, got.IsCA)
Expand All @@ -163,15 +183,15 @@ func TestIsCa(t *testing.T) {
input1 = Certificate{Subject: "CN=Joe", IsCA: &isCA}
err = input1.Generate(nil)
assert.Nil(t, err)
got, err = x509.ParseCertificate(input1.cert)
got, err = x509.ParseCertificate(input1.Cert)
assert.Nil(t, err)
assert.Equal(t, x509.KeyUsageCertSign|x509.KeyUsageCRLSign, got.KeyUsage)
assert.Equal(t, true, got.IsCA)

input2 := Certificate{Subject: "CN=EndEntity", Issuer: "CN=Joe"}
err = input2.Generate(&input1)
assert.Nil(t, err)
got, err = x509.ParseCertificate(input2.cert)
got, err = x509.ParseCertificate(input2.Cert)
assert.Nil(t, err)
assert.Equal(t, x509.KeyUsageDigitalSignature|x509.KeyUsageKeyEncipherment, got.KeyUsage)
assert.Equal(t, false, got.IsCA)
Expand All @@ -185,15 +205,15 @@ func TestNotBeforeAndNotAfter(t *testing.T) {
input := Certificate{Subject: "CN=Joe", NotBefore: &wantNotBefore}
err := input.Generate(nil)
assert.Nil(t, err)
got, err := x509.ParseCertificate(input.cert)
got, err := x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, wantNotBefore, got.NotBefore)
assert.Equal(t, got.NotBefore.Add(defaultDuration), got.NotAfter)

input = Certificate{Subject: "CN=Joe", NotBefore: &wantNotBefore, NotAfter: &wantNotAfter}
err = input.Generate(nil)
assert.Nil(t, err)
got, err = x509.ParseCertificate(input.cert)
got, err = x509.ParseCertificate(input.Cert)
assert.Nil(t, err)
assert.Equal(t, wantNotBefore, got.NotBefore)
assert.Equal(t, wantNotAfter, got.NotAfter)
Expand Down Expand Up @@ -223,10 +243,20 @@ func TestInvalidSubjectAltName(t *testing.T) {
assert.NotNil(t, err)
}

func TestInvalidKeysize(t *testing.T) {
input := Certificate{Subject: "CN=Joe", KeySize: 1}
func TestInvalidKeyType(t *testing.T) {
input := Certificate{Subject: "CN=Joe", KeyType: "not-a-key-type"}
err := input.Generate(nil)
assert.NotNil(t, err)
}

func TestInvalidKeySize(t *testing.T) {
input := Certificate{Subject: "CN=Joe", KeyType: "EC", KeySize: 1}
err := input.Generate(nil)
assert.NotNil(t, err)

input = Certificate{Subject: "CN=Joe", KeyType: "RSA", KeySize: 1}
err = input.Generate(nil)
assert.NotNil(t, err)
}

func TestInvalidExpires(t *testing.T) {
Expand Down

0 comments on commit e9dc12d

Please sign in to comment.