Skip to content

Commit

Permalink
Merge pull request #30 from deploymenttheory/feature-scaffolding
Browse files Browse the repository at this point in the history
Feature scaffolding - added client assertion (oidc) auth method to provider for testing
  • Loading branch information
ShocOne authored Aug 5, 2024
2 parents 1d5ceb1 + 8cf51a6 commit b8effdb
Show file tree
Hide file tree
Showing 7 changed files with 548 additions and 20 deletions.
61 changes: 54 additions & 7 deletions internal/helpers/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,44 @@ import (
pkcs12 "software.sslmate.com/src/go-pkcs12"
)

// ParseCertificateData decodes and parses PKCS#12 data, extracting certificates and a private key.
//
// This function attempts to decode PKCS#12 data using the provided password. It extracts
// the certificate chain (including the end-entity certificate and any CA certificates),
// as well as the private key associated with the end-entity certificate.
//
// The function performs several validations:
// - It checks if any certificates are present in the decoded data.
// - It verifies the presence of a private key.
// - It ensures the private key is of RSA type.
//
// The function logs debug, error, and info messages at various stages of the process.
//
// Parameters:
// - ctx: A context.Context for logging and potential cancellation.
// - certData: A byte slice containing the PKCS#12 data to be parsed.
// - password: A byte slice containing the password to decrypt the PKCS#12 data.
//
// Returns:
// - []*x509.Certificate: A slice of parsed X.509 certificates, with the end-entity
// certificate as the first element, followed by any CA certificates.
// - crypto.PrivateKey: The private key associated with the end-entity certificate.
// - error: An error if any step of the parsing or validation process fails. This will
// be nil if the function executes successfully.
//
// Possible errors:
// - Failure to parse PKCS#12 data
// - No certificates found in the PKCS#12 data
// - No private key found in the PKCS#12 data
// - Private key is not of RSA type
//
// Usage example:
//
// certs, privKey, err := ParseCertificateData(ctx, pkcs12Data, []byte("password"))
// if err != nil {
// // Handle error
// }
// // Use certs and privKey as needed
func ParseCertificateData(ctx context.Context, certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) {
tflog.Debug(ctx, "Attempting to parse PKCS#12 data")

Expand All @@ -25,9 +63,16 @@ func ParseCertificateData(ctx context.Context, certData []byte, password []byte)
}
certs := append([]*x509.Certificate{certificate}, caCerts...)

if len(certs) == 0 {
tflog.Error(ctx, "No certificates found in PKCS#12 data")
return nil, nil, errors.New("no certificates found in PKCS#12 data")
validCerts := []*x509.Certificate{}
for _, cert := range certs {
if cert != nil {
validCerts = append(validCerts, cert)
}
}

if len(validCerts) == 0 {
tflog.Error(ctx, "No valid certificates found in PKCS#12 data")
return nil, nil, errors.New("no valid certificates found in PKCS#12 data")
}

if privateKey == nil {
Expand All @@ -37,15 +82,17 @@ func ParseCertificateData(ctx context.Context, certData []byte, password []byte)

rsaKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
tflog.Error(ctx, "Private key is not of RSA type")
return nil, nil, errors.New("private key is not of RSA type")
tflog.Error(ctx, "Private key is not of RSA type", map[string]interface{}{
"actualType": fmt.Sprintf("%T", privateKey),
})
return nil, nil, fmt.Errorf("private key is not of RSA type, got %T", privateKey)
}

tflog.Info(ctx, "PKCS#12 data parsed successfully", map[string]interface{}{
"certificateCount": len(certs),
"certificateCount": len(validCerts),
"privateKeyType": "RSA",
"privateKeyBits": rsaKey.N.BitLen(),
})

return certs, privateKey, nil
return validCerts, privateKey, nil
}
195 changes: 195 additions & 0 deletions internal/helpers/cert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package helpers

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
pkcs12 "software.sslmate.com/src/go-pkcs12"
)

func TestParseCertificateData(t *testing.T) {
ctx := context.Background()

t.Run("Valid PFX with password", func(t *testing.T) {
pfxData, password, err := generatePFXWithPassword()
require.NoError(t, err)

certs, privKey, err := ParseCertificateData(ctx, pfxData, []byte(password))
assert.NoError(t, err)
assert.Len(t, certs, 1)
assert.NotNil(t, privKey)
_, ok := privKey.(*rsa.PrivateKey)
assert.True(t, ok)
})

t.Run("Valid PFX without password", func(t *testing.T) {
pfxData, err := generatePFXWithoutPassword()
require.NoError(t, err)

certs, privKey, err := ParseCertificateData(ctx, pfxData, []byte(""))
assert.NoError(t, err)
assert.Len(t, certs, 1)
assert.NotNil(t, privKey)
_, ok := privKey.(*rsa.PrivateKey)
assert.True(t, ok)
})

t.Run("PFX with non-RSA key", func(t *testing.T) {
pfxData, password, err := generatePFXWithNonRSAKey()
require.NoError(t, err)

_, _, err = ParseCertificateData(ctx, pfxData, []byte(password))
assert.Error(t, err)
assert.Contains(t, err.Error(), "private key is not of RSA type")
})

t.Run("Invalid PFX data", func(t *testing.T) {
_, _, err := ParseCertificateData(ctx, []byte("invalid data"), []byte("password"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse PKCS#12 data")
})

t.Run("Incorrect password", func(t *testing.T) {
pfxData, _, err := generatePFXWithPassword()
require.NoError(t, err)

_, _, err = ParseCertificateData(ctx, pfxData, []byte("wrongpassword"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse PKCS#12 data")
})
}

// generatePFXWithPassword creates a PKCS#12 (PFX) certificate with an RSA private key and password
func generatePFXWithPassword() (pfxData []byte, password string, err error) {
// Generate RSA private key
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, "", err
}

// Create certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Test Cert",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

// Create certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, "", err
}

cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, "", err
}

// Encode to PKCS#12
password = "testpassword"
pfxData, err = pkcs12.Modern.Encode(privateKey, cert, nil, password)
if err != nil {
return nil, "", err
}

return pfxData, password, nil
}

// generatePFXWithoutPassword creates a PKCS#12 (PFX) certificate with an RSA private key and no password
func generatePFXWithoutPassword() (pfxData []byte, err error) {
// Generate RSA private key
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}

// Create certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Test Cert No Password",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

// Create certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, err
}

cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, err
}

// Encode to PKCS#12 without password
pfxData, err = pkcs12.Modern.Encode(privateKey, cert, nil, "")
if err != nil {
return nil, err
}

return pfxData, nil
}

// generatePFXWithNonRSAKey creates a PKCS#12 (PFX) certificate with an ECDSA private key (non-RSA)
func generatePFXWithNonRSAKey() (pfxData []byte, password string, err error) {
// Generate ECDSA private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, "", err
}

// Create certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Test Cert ECDSA",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

// Create certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, "", err
}

cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, "", err
}

// Encode to PKCS#12
password = "testpassword"
pfxData, err = pkcs12.Modern.Encode(privateKey, cert, nil, password)
if err != nil {
return nil, "", err
}

return pfxData, password, nil
}
33 changes: 33 additions & 0 deletions internal/helpers/conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package helpers

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestStringPtrToString(t *testing.T) {
t.Run("Non-nil string pointer", func(t *testing.T) {
input := "test string"
result := StringPtrToString(&input)
assert.Equal(t, input, result, "Should return the dereferenced string value")
})

t.Run("Nil string pointer", func(t *testing.T) {
var input *string
result := StringPtrToString(input)
assert.Equal(t, "", result, "Should return an empty string for nil input")
})

t.Run("Empty string pointer", func(t *testing.T) {
input := ""
result := StringPtrToString(&input)
assert.Equal(t, "", result, "Should return an empty string for empty string input")
})

t.Run("String pointer with whitespace", func(t *testing.T) {
input := " "
result := StringPtrToString(&input)
assert.Equal(t, " ", result, "Should preserve whitespace")
})
}
76 changes: 76 additions & 0 deletions internal/helpers/md5_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package helpers

import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
"io"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCalculateMd5(t *testing.T) {
tempDir := t.TempDir()

t.Run("Calculate MD5 for non-empty file", func(t *testing.T) {
content := []byte("Hello, World!")
filePath := filepath.Join(tempDir, "test1.txt")
err := os.WriteFile(filePath, content, 0644)
require.NoError(t, err)

expectedMD5 := md5.Sum(content)
expectedMD5String := hex.EncodeToString(expectedMD5[:])

result, err := CalculateMd5(filePath)
assert.NoError(t, err)
assert.Equal(t, expectedMD5String, result)
})

t.Run("Calculate MD5 for empty file", func(t *testing.T) {
filePath := filepath.Join(tempDir, "empty.txt")
err := os.WriteFile(filePath, []byte{}, 0644)
require.NoError(t, err)

expectedMD5 := md5.Sum([]byte{})
expectedMD5String := hex.EncodeToString(expectedMD5[:])

result, err := CalculateMd5(filePath)
assert.NoError(t, err)
assert.Equal(t, expectedMD5String, result)
})

t.Run("Calculate MD5 for non-existent file", func(t *testing.T) {
filePath := filepath.Join(tempDir, "non_existent.txt")

result, err := CalculateMd5(filePath)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to open file")
assert.Empty(t, result)
})

t.Run("Calculate MD5 for large file", func(t *testing.T) {
filePath := filepath.Join(tempDir, "large.bin")
f, err := os.Create(filePath)
require.NoError(t, err)
defer f.Close()

// Write 10MB of random data
data := make([]byte, 1024*1024*10)
_, err = io.ReadFull(rand.Reader, data)
require.NoError(t, err)

_, err = f.Write(data)
require.NoError(t, err)

expectedMD5 := md5.Sum(data)
expectedMD5String := hex.EncodeToString(expectedMD5[:])

result, err := CalculateMd5(filePath)
assert.NoError(t, err)
assert.Equal(t, expectedMD5String, result)
})
}
Loading

0 comments on commit b8effdb

Please sign in to comment.