Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ jobs:
os: [ubuntu-latest, macos-latest]
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ^1.20
id: go

- name: Check out code into the Go module directory
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Test
run: |
go vet -v ./...
go test -v -race -covermode=atomic -coverprofile=coverage.out ./...

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
7 changes: 3 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ jobs:
name: golangci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v8
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: latest
version: v2.1.6

# Optional: working directory, useful for monorepos
# working-directory: somedir
Expand Down
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: "2"
linters:
enable:
- misspell
- revive
35 changes: 19 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Stop saving test certificates in your code repos. Start generating them in your
```go
func TestFunc(t *testing.T) {
// Create and write self-signed Certificate and Key to temporary files
cert, key, err := testcerts.GenerateToTempFile("/tmp/")
cert, key, err := testcerts.GenerateCertsToTempFile("/tmp/")
if err != nil {
// do something
}
Expand All @@ -33,30 +33,35 @@ func TestFunc(t *testing.T) {
// Generate Certificate Authority
ca := testcerts.NewCA()

go func() {
// Create a signed Certificate and Key for "localhost"
certs, err := ca.NewKeyPair("localhost")
if err != nil {
// do something
}
// Create a signed Certificate and Key for "localhost"
certs, err := ca.NewKeyPair("localhost")
if err != nil {
// do something
}

// Write certificates to a file
err = certs.ToFile("/tmp/cert", "/tmp/key")
if err {
// do something
}
// Write certificates to a file
err = certs.ToFile("/tmp/cert", "/tmp/key")
if err != nil {
// do something
}

// Start HTTP Listener
// Start HTTP Listener
go func() {
err = http.ListenAndServeTLS("localhost:443", "/tmp/cert", "/tmp/key", someHandler)
if err != nil {
// do something
}
}()

// Create a client with the self-signed CA
tlsConfig, err := certs.ConfigureTLSConfig(ca.GenerateTLSConfig())
if err != nil {
// do something
}

client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: certs.ConfigureTLSConfig(ca.GenerateTLSConfig()),
TLSClientConfig: tlsConfig,
},
}

Expand All @@ -75,5 +80,3 @@ If you find a bug or have an idea for a feature, please open an issue or a pull

testcerts is released under the MIT License. See [LICENSE](./LICENSE) for details.



9 changes: 9 additions & 0 deletions gencerts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testcerts
import (
"os"
"path/filepath"
"runtime"
"testing"
)

Expand Down Expand Up @@ -109,6 +110,10 @@ func TestGeneratingCertsToFile(t *testing.T) {
})

t.Run("Testing the unhappy path for insufficient permissions", func(t *testing.T) {
if runtime.GOOS != "windows" && os.Geteuid() == 0 {
// CAP_DAC_OVERRIDE can also bypass this in some rootless Linux containers.
t.Skip("running as root bypasses directory permission checks")
}
dir, err := os.MkdirTemp("", "permission-test")
if err != nil {
t.Errorf("Error creating temp directory - %s", err)
Expand Down Expand Up @@ -167,6 +172,10 @@ func TestGenerateCertsToTempFile(t *testing.T) {
})

t.Run("Testing the unhappy path for insufficient permissions when creating temp file", func(t *testing.T) {
if runtime.GOOS != "windows" && os.Geteuid() == 0 {
// CAP_DAC_OVERRIDE can also bypass this in some rootless Linux containers.
t.Skip("running as root bypasses directory permission checks")
}
dir, err := os.MkdirTemp("", "permission-test")
if err != nil {
t.Errorf("Error creating temp directory - %s", err)
Expand Down
2 changes: 1 addition & 1 deletion kpconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (c *KeyPairConfig) Validate() error {
return nil
}

// IPAddresses returns a list of IP addresses in Net.IP format.
// IPNetAddresses returns a list of IP addresses in net.IP format.
func (c *KeyPairConfig) IPNetAddresses() ([]net.IP, error) {
var ips []net.IP
for _, ip := range c.IPAddresses {
Expand Down
94 changes: 68 additions & 26 deletions testcerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ import (
"time"
)

const fileMode = 0640

var (
// ErrEmptyCertificateData is returned when certificate PEM data is empty.
ErrEmptyCertificateData = errors.New("empty certificate data")

// ErrEmptyKeyData is returned when private key PEM data is empty.
ErrEmptyKeyData = errors.New("empty key data")

// ErrInvalidCertificateData is returned when certificate data is not valid PEM.
ErrInvalidCertificateData = errors.New("invalid certificate data")

// ErrInvalidKeyData is returned when private key data is not valid PEM.
ErrInvalidKeyData = errors.New("invalid key data")
)

// CertificateAuthority represents a self-signed x509 certificate authority.
type CertificateAuthority struct {
cert *x509.Certificate
Expand Down Expand Up @@ -210,32 +226,60 @@ func (ca *CertificateAuthority) CertPool() *x509.CertPool {

// PrivateKey returns the private key of the CertificateAuthority.
func (ca *CertificateAuthority) PrivateKey() []byte {
if ca == nil || ca.privateKey == nil {
return nil
}
return pem.EncodeToMemory(ca.privateKey)
}

// PublicKey returns the public key of the CertificateAuthority.
func (ca *CertificateAuthority) PublicKey() []byte {
if ca == nil || ca.publicKey == nil {
return nil
}
return pem.EncodeToMemory(ca.publicKey)
}

// ToFile saves the CertificateAuthority certificate and private key to the specified files.
// Returns an error if any file operation fails.
func (ca *CertificateAuthority) ToFile(certFile, keyFile string) error {
// Write Certificate
err := os.WriteFile(certFile, ca.PublicKey(), 0640)
if err != nil {
return fmt.Errorf("unable to create certificate file - %w", err)
func writePairToFiles(certData []byte, certFile string, keyData []byte, keyFile string) error {
if err := validatePEMData(certData, ErrEmptyCertificateData, ErrInvalidCertificateData); err != nil {
return err
}
if err := validatePEMData(keyData, ErrEmptyKeyData, ErrInvalidKeyData); err != nil {
return err
}

// Write Key
err = os.WriteFile(keyFile, ca.PrivateKey(), 0640)
if err != nil {
if err := os.WriteFile(certFile, certData, fileMode); err != nil {
return fmt.Errorf("unable to create certificate file - %w", err)
}
if err := os.WriteFile(keyFile, keyData, fileMode); err != nil {
if removeErr := os.Remove(certFile); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
return errors.Join(
fmt.Errorf("unable to create key file - %w", err),
fmt.Errorf("unable to remove certificate file after key write failure - %w", removeErr),
)
}
return fmt.Errorf("unable to create key file - %w", err)
}
return nil
}
Comment on lines +243 to 264

func validatePEMData(data []byte, emptyErr, invalidErr error) error {
if len(data) == 0 {
return emptyErr
}
block, _ := pem.Decode(data)
if block == nil || len(block.Bytes) == 0 {
return invalidErr
}
return nil
}

// ToFile saves the CertificateAuthority certificate and private key to the specified files.
// Returns an error if any file operation fails.
func (ca *CertificateAuthority) ToFile(certFile, keyFile string) error {
return writePairToFiles(ca.PublicKey(), certFile, ca.PrivateKey(), keyFile)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ToTempFile saves the CertificateAuthority certificate and private key to temporary files.
// The temporary files are created in the specified directory and have random names.
func (ca *CertificateAuthority) ToTempFile(dir string) (cfh *os.File, kfh *os.File, err error) {
Expand All @@ -257,7 +301,7 @@ func (ca *CertificateAuthority) ToTempFile(dir string) (cfh *os.File, kfh *os.Fi
// Write Key
kfh, err = os.CreateTemp(dir, "*.key")
if err != nil {
return cfh, &os.File{}, fmt.Errorf("unable to create certificate file - %w", err)
return cfh, &os.File{}, fmt.Errorf("unable to create key file - %w", err)
}
defer func() {
if closeErr := kfh.Close(); closeErr != nil {
Expand All @@ -266,7 +310,7 @@ func (ca *CertificateAuthority) ToTempFile(dir string) (cfh *os.File, kfh *os.Fi
}()
_, err = kfh.Write(ca.PrivateKey())
if err != nil {
return cfh, kfh, fmt.Errorf("unable to create certificate file - %w", err)
return cfh, kfh, fmt.Errorf("unable to create key file - %w", err)
}

return cfh, kfh, nil
Expand All @@ -287,30 +331,24 @@ func (kp *KeyPair) Cert() *x509.Certificate {

// PrivateKey returns the private key of the KeyPair.
func (kp *KeyPair) PrivateKey() []byte {
if kp == nil || kp.privateKey == nil {
return nil
}
return pem.EncodeToMemory(kp.privateKey)
}

// PublicKey returns the public key of the KeyPair.
func (kp *KeyPair) PublicKey() []byte {
if kp == nil || kp.publicKey == nil {
return nil
}
return pem.EncodeToMemory(kp.publicKey)
}

// ToFile saves the KeyPair certificate and private key to the specified files.
// Returns an error if any file operation fails.
func (kp *KeyPair) ToFile(certFile, keyFile string) error {
// Write Certificate
err := os.WriteFile(certFile, kp.PublicKey(), 0640)
if err != nil {
return fmt.Errorf("unable to create certificate file - %w", err)
}

// Write Key
err = os.WriteFile(keyFile, kp.PrivateKey(), 0640)
if err != nil {
return fmt.Errorf("unable to create key file - %w", err)
}

return nil
return writePairToFiles(kp.PublicKey(), certFile, kp.PrivateKey(), keyFile)
}

// ToTempFile saves the KeyPair certificate and private key to temporary files.
Expand Down Expand Up @@ -349,9 +387,13 @@ func (kp *KeyPair) ToTempFile(dir string) (cfh *os.File, kfh *os.File, err error
return cfh, kfh, nil
}

// ConfigureTLSConfig will configure the tls.Config with the KeyPair certificate and private key.
// ConfigureTLSConfig configures tlsConfig with the KeyPair certificate and private key.
// If tlsConfig is nil, it creates one. Otherwise, it mutates and returns the provided config.
// The returned tls.Config can be used for a server or client.
func (kp *KeyPair) ConfigureTLSConfig(tlsConfig *tls.Config) (*tls.Config, error) {
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
cert, err := tls.X509KeyPair(kp.PublicKey(), kp.PrivateKey())
Comment on lines 393 to 397
if err != nil {
return nil, fmt.Errorf("could not create x509 key pair - %w", err)
Expand Down
Loading
Loading