From 13ecf469f4f87a80b69e6b20b24d6f921a3513e4 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 5 Dec 2021 01:01:05 +0100 Subject: [PATCH] Add certificate service --- certs/certservice.go | 133 ++++++++++++++++++++++++++++++++++++++ certs/certservice_test.go | 84 ++++++++++++++++++++++++ certs/generate.go | 2 +- 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 certs/certservice.go create mode 100644 certs/certservice_test.go diff --git a/certs/certservice.go b/certs/certservice.go new file mode 100644 index 0000000..5a2e1fa --- /dev/null +++ b/certs/certservice.go @@ -0,0 +1,133 @@ +package certs + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "gitea.benny.dog/torjus/ezshare/store" + "math/big" + "time" +) + +type CertService struct { + caCert *x509.Certificate + caKey crypto.Signer + store store.CertificateStore +} + +func NewCertService(s store.CertificateStore, certBytes, keyBytes []byte) (*CertService, error) { + // Try to decode key as PEM + keyBlock, _ := pem.Decode(keyBytes) + if keyBlock != nil { + if keyBlock.Type != "EC PRIVATE KEY" { + return nil, fmt.Errorf("private key is not of type EC PRIVATE KEY: %s", keyBlock.Type) + } + keyBytes = keyBlock.Bytes + } + // Try to decode cert as PEM + certBlock, _ := pem.Decode(certBytes) + if certBlock != nil { + if certBlock.Type != "CERTIFICATE" { + return nil, fmt.Errorf("certificate is not of type CERTIFICATE: %s", certBlock.Type) + } + certBytes = certBlock.Bytes + } + + caCert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, fmt.Errorf("unable to parse certificate: %w", err) + } + + if !caCert.IsCA { + return nil, fmt.Errorf("certificate is not CA") + } + + caKey, err := x509.ParseECPrivateKey(keyBytes) + if err != nil { + return nil, fmt.Errorf("unable to parse private key: %w", err) + } + + return &CertService{caCert: caCert, caKey: caKey, store: s}, nil +} + +func (cs *CertService) NewClient(id string) ([]byte, []byte, error) { + cert := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().Unix()), + Subject: pkix.Name{ + CommonName: id, + Organization: []string{"ezshare"}, + Country: []string{"No"}, + Locality: []string{"Oslo"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + certPrivKeyBytes, err := x509.MarshalECPrivateKey(certPrivKey) + if err != nil { + return nil, nil, err + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, cs.caCert, &certPrivKey.PublicKey, cs.caKey) + if err != nil { + return nil, nil, err + } + + keyPEM := new(bytes.Buffer) + if err := pem.Encode(keyPEM, &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: certPrivKeyBytes, + }); err != nil { + return nil, nil, fmt.Errorf("unable to encode client private key: %w", err) + } + certPEM := new(bytes.Buffer) + if err := pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }); err != nil { + return nil, nil, fmt.Errorf("unable to encode client private key: %w", err) + } + + return certPEM.Bytes(), keyPEM.Bytes(), nil +} + +func (cs *CertService) VerifyClient(certBytes []byte) (string, error) { + + // Try to decode cert as PEM + certBlock, _ := pem.Decode(certBytes) + if certBlock != nil { + if certBlock.Type != "CERTIFICATE" { + return "", fmt.Errorf("certificate is not of type CERTIFICATE: %s", certBlock.Type) + } + certBytes = certBlock.Bytes + } + + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return "", fmt.Errorf("unable to parse certificate: %w", err) + } + + rootPool := x509.NewCertPool() + rootPool.AddCert(cs.caCert) + + if _, err := cert.Verify(x509.VerifyOptions{ + Roots: rootPool, + }); err != nil { + return "", fmt.Errorf("unable to verify: %w", err) + } + + return cert.Subject.CommonName, nil +} diff --git a/certs/certservice_test.go b/certs/certservice_test.go new file mode 100644 index 0000000..51f8fd2 --- /dev/null +++ b/certs/certservice_test.go @@ -0,0 +1,84 @@ +package certs_test + +import ( + "crypto/x509" + "encoding/pem" + "gitea.benny.dog/torjus/ezshare/certs" + "gitea.benny.dog/torjus/ezshare/store" + "github.com/google/uuid" + "testing" +) + +func TestCertService(t *testing.T) { + t.Run("TestManualVerifyClientCertificate", func(t *testing.T) { + + s := store.NewMemoryStore() + + caKeyBytes, caCertBytes, err := certs.GenCACert() + if err != nil { + t.Fatalf("Error generating ca cert: %s", err) + } + + svc, err := certs.NewCertService(s, caCertBytes, caKeyBytes) + if err != nil { + t.Fatalf("Unable to create service: %s", err) + } + + clientCertPEM, _, err := svc.NewClient("test") + if err != nil { + t.Fatalf("Unable to create client certificate: %s", err) + } + + caCert, err := x509.ParseCertificate(caCertBytes) + if err != nil { + t.Fatalf("Unable to parse CA certificate: %s", err) + } + certPool := x509.NewCertPool() + certPool.AddCert(caCert) + + clientCertPEMBlock, _ := pem.Decode(clientCertPEM) + if clientCertPEMBlock == nil { + t.Fatalf("Client does not contain PEM-encoded data") + } + if clientCertPEMBlock.Type != "CERTIFICATE" { + t.Fatal("Client cert is not certificate") + } + + clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("Could not parse client certificate: %s", err) + } + + if _, err := clientCert.Verify(x509.VerifyOptions{Roots: certPool}); err != nil { + t.Fatalf("Could not verify client certificate: %s", err) + } + }) + t.Run("TestVerifyClientCertificate", func(t *testing.T) { + + s := store.NewMemoryStore() + + caKeyBytes, caCertBytes, err := certs.GenCACert() + if err != nil { + t.Fatalf("Error generating ca cert: %s", err) + } + + svc, err := certs.NewCertService(s, caCertBytes, caKeyBytes) + if err != nil { + t.Fatalf("Unable to create service: %s", err) + } + + clientID := uuid.Must(uuid.NewRandom()).String() + clientCertPEM, _, err := svc.NewClient(clientID) + if err != nil { + t.Fatalf("Unable to create client certificate: %s", err) + } + + id, err := svc.VerifyClient(clientCertPEM) + if err != nil { + t.Fatalf("Failed to verify certificate: %s", err) + } + if id != clientID { + t.Fatalf("Verify returned wrong id. Got %s want %s", id, clientID) + } + }) +} diff --git a/certs/generate.go b/certs/generate.go index 8d698ac..05ae11b 100644 --- a/certs/generate.go +++ b/certs/generate.go @@ -142,7 +142,7 @@ func GenCert(caPub, caPrivKey []byte, dnsNames []string) (priv, pub []byte, err } cert := &x509.Certificate{ - SerialNumber: big.NewInt(1658), + SerialNumber: big.NewInt(time.Now().Unix()), Subject: pkix.Name{ Organization: []string{"ezshare"}, Country: []string{"No"},