forked from coreos/etcd-ca
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
pkix module is responsible for certificate and key in PKIX. For now, it could create a brand-new CA.
- Loading branch information
Showing
7 changed files
with
626 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
Oops, something went wrong.