Skip to content

Commit

Permalink
feat: Add pkix module
Browse files Browse the repository at this point in the history
pkix module is responsible for certificate and key in PKIX.

For now, it could create a brand-new CA.
  • Loading branch information
yichengq committed Mar 10, 2014
1 parent 59aa5a7 commit 821327d
Show file tree
Hide file tree
Showing 7 changed files with 626 additions and 1 deletion.
137 changes: 137 additions & 0 deletions pkix/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package pkix

import (
"bytes"
"crypto/x509"
"encoding/pem"
"errors"
"time"
)

const (
certificatePEMBlockType = "CERTIFICATE"
)

type Certificate struct {
// derBytes is always set for valid Certificate
derBytes []byte

pemBlock *pem.Block

crt *x509.Certificate
}

// NewCertificateFromDER inits Certificate from DER-format bytes
func NewCertificateFromDER(derBytes []byte) *Certificate {
return &Certificate{derBytes: derBytes}
}

// NewCertificateFromDER inits Certificate from PEM-format bytes
// data should contain at most one certificate
func NewCertificateFromPEM(data []byte) (c *Certificate, err error) {
pemBlock, _ := pem.Decode(data)
if pemBlock == nil {
err = errors.New("cannot find the next PEM formatted block")
return
}
if pemBlock.Type != certificatePEMBlockType || len(pemBlock.Headers) != 0 {
err = errors.New("unmatched type or headers")
return
}
c = &Certificate{derBytes: pemBlock.Bytes, pemBlock: pemBlock}
return
}

// build crt field if needed
func (c *Certificate) buildX509Certificate() error {
if c.crt != nil {
return nil
}

crts, err := x509.ParseCertificates(c.derBytes)
if err != nil {
return err
}
if len(crts) != 1 {
return errors.New("unsupported multiple certificates in a block")
}
c.crt = crts[0]
return nil
}

// GetRawCrt gets crypto/x509-format Certificate
func (c *Certificate) GetRawCrt() (*x509.Certificate, error) {
if err := c.buildX509Certificate(); err != nil {
return nil, err
}
return c.crt, nil
}

// CheckAuthority checks the authority of certificate against itself.
// It only ensures that certificate is self-explanatory, and
// cannot promise the validity and security.
func (c *Certificate) CheckAuthority() error {
if err := c.buildX509Certificate(); err != nil {
return err
}
return c.crt.CheckSignatureFrom(c.crt)
}

// VerifyHost verifies the host certificate using host name.
// Only certificate of authority could call this function successfully.
// Current implementation allows one CA and direct hosts only,
// so the organization is always this:
// CA
// host1 host2 host3
func (c *Certificate) VerifyHost(hostCert *Certificate, name string) error {
if err := c.CheckAuthority(); err != nil {
return err
}

roots := x509.NewCertPool()
roots.AddCert(c.crt)

verifyOpts := x509.VerifyOptions{
DNSName: name,
// no intermediates are allowed for now
Intermediates: nil,
Roots: roots,
// if zero, the current time is used
CurrentTime: time.Now(),
// An empty list means ExtKeyUsageServerAuth.
KeyUsages: nil,
}

chains, err := hostCert.crt.Verify(verifyOpts)
if err != nil {
return err
}
if len(chains) != 1 {
err = errors.New("internal error: verify chain number != 1")
return err
}
return nil
}

func (c *Certificate) buildPEM() {
if c.pemBlock != nil {
return
}

c.pemBlock = &pem.Block{
Type: certificatePEMBlockType,
Headers: nil,
Bytes: c.derBytes,
}
}

// Export returns PEM-format bytes
func (c *Certificate) Export() ([]byte, error) {
c.buildPEM()

buf := new(bytes.Buffer)
if err := pem.Encode(buf, c.pemBlock); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
74 changes: 74 additions & 0 deletions pkix/cert_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package pkix

import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"time"
)

const (
// hostname used by CA certificate
authHostname = "CA"
)

var (
authPkixName = pkix.Name{
Country: []string{"USA"},
Organization: []string{"etcd-ca"},
OrganizationalUnit: nil,
Locality: nil,
Province: nil,
StreetAddress: nil,
PostalCode: nil,
SerialNumber: "",
CommonName: authHostname,
}
// Build CA based on RFC5280
authTemplate = x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: authPkixName,
// NotBefore is set to be 10min earlier to fix gap on time difference in cluster
NotBefore: time.Now().Add(-600).UTC(),
// 10-year lease
NotAfter: time.Now().AddDate(10, 0, 0).UTC(),
// Used for certificate signing only
KeyUsage: x509.KeyUsageCertSign,

ExtKeyUsage: nil,
UnknownExtKeyUsage: nil,

// activate CA
BasicConstraintsValid: true,
IsCA: true,
// Not allow any non-self-issued intermediate CA
MaxPathLen: 0,

// 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey
// (excluding the tag, length, and number of unused bits)
// **SHOULD** be filled in later
SubjectKeyId: nil,

// Subject Alternative Name
DNSNames: nil,

PermittedDNSDomainsCritical: false,
PermittedDNSDomains: nil,
}
)

func CreateCertificateAuthority(key *Key) (*Certificate, error) {
subjectKeyId, err := key.GenerateSubjectKeyId()
if err != nil {
return nil, err
}
authTemplate.SubjectKeyId = subjectKeyId

crtBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, &authTemplate, key.pub, key.priv)
if err != nil {
return nil, err
}

return NewCertificateFromDER(crtBytes), nil
}
39 changes: 39 additions & 0 deletions pkix/cert_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package pkix

import (
"testing"
"time"
)

func TestCreateCertificateAuthority(t *testing.T) {
key, err := CreateRSAKey()
if err != nil {
t.Fatal("Failed creating rsa key:", err)
}

crt, err := CreateCertificateAuthority(key)
if err != nil {
t.Fatal("Failed creating certificate authority:", err)
}

rawCrt, err := crt.GetRawCrt()
if err != nil {
t.Fatal("Failed to get x509.Certificate:", err)
}

if err = rawCrt.CheckSignatureFrom(rawCrt); err != nil {
t.Fatal("Failed to check signature:", err)
}

if err = rawCrt.VerifyHostname(authHostname); err != nil {
t.Fatal("Failed to verify hostname:", err)
}

if !time.Now().After(rawCrt.NotBefore) {
t.Fatal("Failed to be after NotBefore")
}

if !time.Now().Before(rawCrt.NotAfter) {
t.Fatal("Failed to be before NotAfter")
}
}
123 changes: 123 additions & 0 deletions pkix/cert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package pkix

import (
"bytes"
"testing"
)

const (
certAuthPEM = `-----BEGIN CERTIFICATE-----
MIIB9zCCAWKgAwIBAgIBATALBgkqhkiG9w0BAQUwMTEMMAoGA1UEBhMDVVNBMRQw
EgYDVQQKEwtDb3JlT1MgSW5jLjELMAkGA1UEAxMCQ0EwHhcNMTQwMzA5MTgzMzQx
WhcNMjQwMzA5MTkzMzQxWjAxMQwwCgYDVQQGEwNVU0ExFDASBgNVBAoTC0NvcmVP
UyBJbmMuMQswCQYDVQQDEwJDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
ptSfk77PDDWYiNholqgPyQwtnf7hmoFGEqiA4Cu0u+LW7vLqkysaXHUVjQH/ditJ
FPlvwsllgPbgCF9bUzrCbXbrV2xjIhairyOGFSrLGBZMIB91xHXPlFhy2U+4Piio
bisrv2InHvPTyyZqVbqLDhF8DmVMIZI/UCOKtCMSrN8CAwEAAaMjMCEwDgYDVR0P
AQH/BAQDAgAEMA8GA1UdEwEB/wQFMAMBAf8wCwYJKoZIhvcNAQEFA4GBAHKzf9iH
fKUdWUz5Ue8a1yRRTu5EuGK3pz22x6udcIYH6KFBPVfj5lSbbE3NirE7TKWvF2on
SCP/620bWJMxqNAYdwpiyGibsiUlueWB/3aavoq10MIHA6MBxw/wrsoLPns9f7dP
+ddM40NjuI1tvX6SnUwuahONdvUJDxqVR+AM
-----END CERTIFICATE-----
`
badCertAuthPEM = `-----BEGIN CERTIFICATE-----
MIIB9zCCAWKgAwIBAgIBATALBgkqhkiG9w0BAQUwMTEMMAoGA1UEBhMDVVNBMRQw
EgYAVQQKEwtDb3JlT1MgSW5jLjELMAkGA1UEAxMCQ0EwHhcNMTQwMzA5MjE1NDI5
WhcNMjQwMzA5MjI1NDI5WjAxMQwwCgYDVQQGEwNVU0ExFDASBgNVBAoTC0NvcmVP
UyBJbmMuMQswCQYDVQQDEwJDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
xLZYiSaYRWC90r/W+3cVFI6NnWfEo9Wrbn/PsJRz+Nn1NURuLpYWrMSZa1ihipVr
bPY9Xi8Xo5YCll2z9RcWoVp0ASU1VxctXKWbsk/lqnAKDX+/lTW4iKERUF67NOlR
GFtBzq7iVPQT7qNYCMu3CRG/4cTuOcCglH/xE9HdgdcCAwEAAaMjMCEwDgYDVR0P
AQH/BAQDAgAEMA8GA1UdEwEB/wQFMAMBAf8wCwYJKoZIhvcNAQEFA4GBAL129Vc3
lcfYfSfI2fMgkG3hc2Yhtu/SJ7wRFqlrNBM9lnNJnYMF+fAWv6u8xix8OWfYs38U
BB6sTriDpe5oo2H0o7Pf5ACE3IIy2Cf2+HAmNClYrdlwNYfP7aUazbEhuzPcvJYA
zPNy61oRnsETV77BH+JQ7j4E+pAJ5MHpKUcq
-----END CERTIFICATE-----
`
wrongCertAuthPEM = `-----BEGIN WRONG CERTIFICATE-----
MIIB9zCCAWKgAwIBAgIBATALBgkqhkiG9w0BAQUwMTEMMAoGA1UEBhMDVVNBMRQw
EgYDVQQKEwtDb3JlT1MgSW5jLjELMAkGA1UEAxMCQ0EwHhcNMTQwMzA5MTgzMzQx
WhcNMjQwMzA5MTkzMzQxWjAxMQwwCgYDVQQGEwNVU0ExFDASBgNVBAoTC0NvcmVP
UyBJbmMuMQswCQYDVQQDEwJDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
ptSfk77PDDWYiNholqgPyQwtnf7hmoFGEqiA4Cu0u+LW7vLqkysaXHUVjQH/ditJ
FPlvwsllgPbgCF9bUzrCbXbrV2xjIhairyOGFSrLGBZMIB91xHXPlFhy2U+4Piio
bisrv2InHvPTyyZqVbqLDhF8DmVMIZI/UCOKtCMSrN8CAwEAAaMjMCEwDgYDVR0P
AQH/BAQDAgAEMA8GA1UdEwEB/wQFMAMBAf8wCwYJKoZIhvcNAQEFA4GBAHKzf9iH
fKUdWUz5Ue8a1yRRTu5EuGK3pz22x6udcIYH6KFBPVfj5lSbbE3NirE7TKWvF2on
SCP/620bWJMxqNAYdwpiyGibsiUlueWB/3aavoq10MIHA6MBxw/wrsoLPns9f7dP
+ddM40NjuI1tvX6SnUwuahONdvUJDxqVR+AM
-----END WRONG CERTIFICATE-----
`
)

func TestCertificateAuthority(t *testing.T) {
crt, err := NewCertificateFromPEM([]byte(certAuthPEM))
if err != nil {
t.Fatal("Failed to parse certificate from PEM:", err)
}

if err = crt.CheckAuthority(); err != nil {
t.Fatal("Failed to check self-sign:", err)
}

if err = crt.VerifyHost(crt, authHostname); err != nil {
t.Fatal("Failed to verify CA:", err)
}
}

func TestWrongCertificate(t *testing.T) {
if _, err := NewCertificateFromPEM([]byte("-")); err == nil {
t.Fatal("Expect not to parse certificate from PEM:", err)
}

if _, err := NewCertificateFromPEM([]byte(wrongCertAuthPEM)); err == nil {
t.Fatal("Expect not to parse certificate from PEM:", err)
}
}

func TestBadCertificate(t *testing.T) {
crt, err := NewCertificateFromPEM([]byte(badCertAuthPEM))
if err != nil {
t.Fatal("Failed to parse certificate from PEM:", err)
}

if _, err = crt.GetRawCrt(); err == nil {
t.Fatal("Expect not to get x509.Certificate")
}

if err = crt.CheckAuthority(); err == nil {
t.Fatal("Expect not to get x509.Certificate")
}

if err = crt.VerifyHost(crt, authHostname); err == nil {
t.Fatal("Expect not to get x509.Certificate")
}

pemBytes, err := crt.Export()
if err != nil {
t.Fatal("Failed exporting PEM-format bytes:", err)
}
if bytes.Compare(pemBytes, []byte(badCertAuthPEM)) != 0 {
t.Fatal(len(pemBytes), len(badCertAuthPEM))
t.Fatal("Failed exporting the same PEM-format bytes")
}
}

// TestCertificateExport tests the ability to convert DER bytes into PEM bytes
func TestCertificateExport(t *testing.T) {
crt, err := NewCertificateFromPEM([]byte(certAuthPEM))
if err != nil {
t.Fatal("Failed to parse certificate from PEM:", err)
}

// remove the copy of PEM in crt
crt.pemBlock = nil

pemBytes, err := crt.Export()
if err != nil {
t.Fatal("Failed exporting PEM-format bytes:", err)
}
if bytes.Compare(pemBytes, []byte(certAuthPEM)) != 0 {
t.Fatal("Failed exporting the same PEM-format bytes")
}
}
Loading

0 comments on commit 821327d

Please sign in to comment.