Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
hdecarne committed Dec 10, 2023
1 parent d604a3d commit 581e2b4
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 168 deletions.
12 changes: 6 additions & 6 deletions certs/acme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ func TestACMECertificateFactory(t *testing.T) {
require.NoError(t, err)
defer os.RemoveAll(tempDir)
config := loadAndPrepareACMEConfig(t, "./acme/testdata/acme-test.yaml", tempDir)
newCertificate(t, config, "Test1", keys.RSA2048)
newCertificate(t, config, "Test1", keys.RSA4096)
newCertificate(t, config, "Test1", keys.RSA8192)
newCertificate(t, config, "Test2", keys.ECDSA256)
newCertificate(t, config, "Test2", keys.ECDSA384)
newACMECertificate(t, config, "Test1", keys.RSA2048)
newACMECertificate(t, config, "Test1", keys.RSA4096)
newACMECertificate(t, config, "Test1", keys.RSA8192)
newACMECertificate(t, config, "Test2", keys.ECDSA256)
newACMECertificate(t, config, "Test2", keys.ECDSA384)
}

func newCertificate(t *testing.T, config *acme.Config, provider string, alg keys.Algorithm) {
func newACMECertificate(t *testing.T, config *acme.Config, provider string, alg keys.Algorithm) {
host, err := os.Hostname()
require.NoError(t, err)
request, err := config.ResolveCertificateRequest([]string{host}, provider)
Expand Down
85 changes: 85 additions & 0 deletions certs/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ package certs
import (
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"math/big"
"sync"
"time"

"github.com/go-ldap/ldap/v3"
)

// CertificateFactory interface provides a unified way to create X.509 certificates.
Expand All @@ -34,3 +42,80 @@ type RevocationListFactory interface {
// New creates a new X.509 revocation list.
New() (*x509.RevocationList, error)
}

// IsRoot checks whether the given certificate is a root certificate.
func IsRoot(cert *x509.Certificate) bool {
return IsIssuedBy(cert, cert)
}

// IsIssuedBy checks whether the given certificate has been issued/signed by the given issuer certificate.
func IsIssuedBy(cert *x509.Certificate, issuer *x509.Certificate) bool {
return cert.CheckSignatureFrom(issuer) == nil
}

// ParseDN parses a X.509 certificate's Distinguished Name (DN) attribute.
func ParseDN(dn string) (*pkix.Name, error) {
ldapDN, err := ldap.ParseDN(dn)
if err != nil {
return nil, fmt.Errorf("invalid DN '%s' (cause: %w)", dn, err)
}
rdns := make([]pkix.RelativeDistinguishedNameSET, 0)
for _, ldapRDN := range ldapDN.RDNs {
rdn := make([]pkix.AttributeTypeAndValue, 0)
for _, ldapRDNAttribute := range ldapRDN.Attributes {
rdnType, err := parseLdapRDNType(ldapRDNAttribute.Type)
if err != nil {
return nil, err
}
rdn = append(rdn, pkix.AttributeTypeAndValue{Type: rdnType, Value: ldapRDNAttribute.Value})
}
rdns = append(rdns, rdn)
}
parsedDN := &pkix.Name{}
parsedDN.FillFromRDNSequence((*pkix.RDNSequence)(&rdns))
return parsedDN, nil
}

func parseLdapRDNType(ldapRDNType string) (asn1.ObjectIdentifier, error) {
switch ldapRDNType {
case "CN":
return []int{2, 5, 4, 3}, nil
case "SERIALNUMBER":
return []int{2, 5, 4, 5}, nil
case "C":
return []int{2, 5, 4, 6}, nil
case "L":
return []int{2, 5, 4, 7}, nil
case "ST":
return []int{2, 5, 4, 8}, nil
case "STREET":
return []int{2, 5, 4, 9}, nil
case "O":
return []int{2, 5, 4, 10}, nil
case "OU":
return []int{2, 5, 4, 11}, nil
case "POSTALCODE":
return []int{2, 5, 4, 17}, nil
case "UID":
return []int{0, 9, 2342, 19200300, 100, 1, 1}, nil
case "DC":
return []int{0, 9, 2342, 19200300, 100, 1, 25}, nil
}
return nil, fmt.Errorf("unrecognized RDN type '%s'", ldapRDNType)
}

var serialNumberLock sync.Mutex = sync.Mutex{}

func nextSerialNumber() *big.Int {
// lock to avoid double numbers via multiple goroutines
serialNumberLock.Lock()
defer serialNumberLock.Unlock()
// wait at least one update, to ensure this functions never returns the same result twice
current := time.Now().UnixMilli()
for {
next := time.Now().UnixMilli()
if next != current {
return big.NewInt(next)
}
}
}
114 changes: 114 additions & 0 deletions certs/certs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (C) 2023 Holger de Carne and contributors
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.

package certs_test

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

"github.com/hdecarne-github/go-certstore/certs"
"github.com/hdecarne-github/go-certstore/keys"
"github.com/stretchr/testify/require"
)

const testKeyAlg = keys.ECDSA256

func TestIsRootAndIsIssuedBy(t *testing.T) {
rootKey, root := newTestRootCert(t, "root")
intermediateKey, intermediate := newTestIntermediateCert(t, "intermediate", root, rootKey)
_, leaf := newTestLeafCert(t, "leaf", intermediate, intermediateKey)
require.True(t, certs.IsRoot(root))
require.False(t, certs.IsRoot(intermediate))
require.False(t, certs.IsRoot(leaf))
require.True(t, certs.IsIssuedBy(intermediate, root))
require.True(t, certs.IsIssuedBy(leaf, intermediate))
require.False(t, certs.IsIssuedBy(leaf, root))
}

func newTestKeyPair(t *testing.T) keys.KeyPair {
keyPair, err := testKeyAlg.NewKeyPairFactory().New()
require.NoError(t, err)
return keyPair
}

func newTestRootCert(t *testing.T, cn string) (crypto.PrivateKey, *x509.Certificate) {
keyPair := newTestKeyPair(t)
now := time.Now()
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: cn},
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 2,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
NotBefore: now,
NotAfter: now.AddDate(0, 0, 1),
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, keyPair.Public(), keyPair.Private())
require.NoError(t, err)
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err)
return keyPair.Private(), cert
}

func newTestIntermediateCert(t *testing.T, cn string, root *x509.Certificate, signer crypto.PrivateKey) (crypto.PrivateKey, *x509.Certificate) {
keyPair := newTestKeyPair(t)
now := time.Now()
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixMilli()),
Subject: pkix.Name{CommonName: cn},
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
KeyUsage: x509.KeyUsageCertSign,
NotBefore: now,
NotAfter: now.AddDate(0, 0, 1),
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, keyPair.Public(), signer)
require.NoError(t, err)
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err)
return keyPair.Private(), cert
}

func newTestLeafCert(t *testing.T, cn string, root *x509.Certificate, signer crypto.PrivateKey) (crypto.PrivateKey, *x509.Certificate) {
keyPair := newTestKeyPair(t)
now := time.Now()
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixMilli()),
Subject: pkix.Name{CommonName: cn},
NotBefore: now,
NotAfter: now.AddDate(0, 0, 1),
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, keyPair.Public(), signer)
require.NoError(t, err)
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err)
return keyPair.Private(), cert
}

func TestParseDN(t *testing.T) {
dn := &pkix.Name{
CommonName: "CommonName",
Locality: []string{"Locality"},
Country: []string{"Country"},
Organization: []string{"Organization"},
OrganizationalUnit: []string{"OrganizationUnit"},
PostalCode: []string{"PostalCode"},
Province: []string{"Province"},
SerialNumber: "SerialNumber",
StreetAddress: []string{"StreetAddress"},
}
parsed, err := certs.ParseDN(dn.String())
require.NoError(t, err)
require.NotNil(t, parsed)
require.Equal(t, dn.String(), parsed.String())
}
55 changes: 0 additions & 55 deletions certs/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ package certs
import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"

"github.com/go-ldap/ldap/v3"
)

// ReadCertificates reads X.509 certificates from the given file.
Expand Down Expand Up @@ -134,54 +130,3 @@ func verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certifica
err.Err = fmt.Errorf("%d peer certifcates received", len(err.UnverifiedCertificates))
return &err
}

// ParseDN parses a X.509 certificate's Distinguished Name (DN) attribute.
func ParseDN(dn string) (*pkix.Name, error) {
ldapDN, err := ldap.ParseDN(dn)
if err != nil {
return nil, fmt.Errorf("invalid DN '%s' (cause: %w)", dn, err)
}
rdns := make([]pkix.RelativeDistinguishedNameSET, 0)
for _, ldapRDN := range ldapDN.RDNs {
rdn := make([]pkix.AttributeTypeAndValue, 0)
for _, ldapRDNAttribute := range ldapRDN.Attributes {
rdnType, err := parseLdapRDNType(ldapRDNAttribute.Type)
if err != nil {
return nil, err
}
rdn = append(rdn, pkix.AttributeTypeAndValue{Type: rdnType, Value: ldapRDNAttribute.Value})
}
rdns = append(rdns, rdn)
}
parsedDN := &pkix.Name{}
parsedDN.FillFromRDNSequence((*pkix.RDNSequence)(&rdns))
return parsedDN, nil
}

func parseLdapRDNType(ldapRDNType string) (asn1.ObjectIdentifier, error) {
switch ldapRDNType {
case "CN":
return []int{2, 5, 4, 3}, nil
case "SERIALNUMBER":
return []int{2, 5, 4, 5}, nil
case "C":
return []int{2, 5, 4, 6}, nil
case "L":
return []int{2, 5, 4, 7}, nil
case "ST":
return []int{2, 5, 4, 8}, nil
case "STREET":
return []int{2, 5, 4, 9}, nil
case "O":
return []int{2, 5, 4, 10}, nil
case "OU":
return []int{2, 5, 4, 11}, nil
case "POSTALCODE":
return []int{2, 5, 4, 17}, nil
case "UID":
return []int{0, 9, 2342, 19200300, 100, 1, 1}, nil
case "DC":
return []int{0, 9, 2342, 19200300, 100, 1, 25}, nil
}
return nil, fmt.Errorf("unrecognized RDN type '%s'", ldapRDNType)
}
19 changes: 0 additions & 19 deletions certs/io_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package certs_test

import (
"crypto/x509/pkix"
"os"
"testing"

Expand Down Expand Up @@ -80,21 +79,3 @@ func TestServerCertificates(t *testing.T) {
require.NotNil(t, certs)
require.Equal(t, 2, len(certs))
}

func TestParseDN(t *testing.T) {
dn := &pkix.Name{
CommonName: "CommonName",
Locality: []string{"Locality"},
Country: []string{"Country"},
Organization: []string{"Organization"},
OrganizationalUnit: []string{"OrganizationUnit"},
PostalCode: []string{"PostalCode"},
Province: []string{"Province"},
SerialNumber: "SerialNumber",
StreetAddress: []string{"StreetAddress"},
}
parsed, err := certs.ParseDN(dn.String())
require.NoError(t, err)
require.NotNil(t, parsed)
require.Equal(t, dn.String(), parsed.String())
}
6 changes: 3 additions & 3 deletions certs/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ func (factory *localCertificateFactory) New() (crypto.PrivateKey, *x509.Certific
if factory.parent != nil {
// parent signed
factory.logger.Info().Msg("creating signed local X.509 certificate...")
createTemplate.SerialNumber = big.NewInt(zerolog.TimestampFunc().UnixMicro())
certificateBytes, err = x509.CreateCertificate(rand.Reader, factory.template, factory.parent, keyPair.Public(), factory.signer)
createTemplate.SerialNumber = nextSerialNumber()
certificateBytes, err = x509.CreateCertificate(rand.Reader, createTemplate, createTemplate, keyPair.Public(), factory.signer)
} else {
// self-signed
factory.logger.Info().Msg("creating self-signed local X.509 certificate...")
createTemplate.SerialNumber = big.NewInt(1)
certificateBytes, err = x509.CreateCertificate(rand.Reader, createTemplate, factory.template, keyPair.Public(), keyPair.Private())
certificateBytes, err = x509.CreateCertificate(rand.Reader, createTemplate, createTemplate, keyPair.Public(), keyPair.Private())
}
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate (cause: %w)", err)
Expand Down
Loading

0 comments on commit 581e2b4

Please sign in to comment.