Skip to content

Commit

Permalink
added cert meta data to payload (#926)
Browse files Browse the repository at this point in the history
* added cert meta data to config payload if specified in the config
  • Loading branch information
oliveromahony authored Nov 18, 2024
1 parent b35fdab commit f010cf3
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 23 deletions.
49 changes: 49 additions & 0 deletions internal/datasource/cert/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) F5, Inc.
//
// This source code is licensed under the Apache License, Version 2.0 license found in the
// LICENSE file in the root directory of this source tree.
package cert

import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
)

func LoadCertificates(certPath, keyPath string) (*tls.Certificate, *x509.CertPool, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, nil, err
}

cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil, nil, err
}

pool := x509.NewCertPool()
pool.AddCert(cert.Leaf)

return &cert, pool, nil
}

func LoadCertificate(certPath string) (*x509.Certificate, error) {
fileContents, err := os.ReadFile(certPath)
if err != nil {
return nil, err
}

certPEMBlock, _ := pem.Decode(fileContents)
if certPEMBlock == nil {
return nil, fmt.Errorf("could not decode: cert was not PEM format")
}

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

return cert, nil
}
125 changes: 125 additions & 0 deletions internal/datasource/cert/cert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) F5, Inc.
//
// This source code is licensed under the Apache License, Version 2.0 license found in the
// LICENSE file in the root directory of this source tree.
package cert

import (
"testing"

"github.com/nginx/agent/v3/test/helpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
keyFileName = "key.pem"
certFileName = "cert.pem"
caFileName = "ca.pem"
nonPemCertFileName = "cert.nonpem"
certificateType = "CERTIFICATE"
privateKeyType = "RSA PRIVATE KEY"
)

func TestLoadCertificates(t *testing.T) {
tmpDir := t.TempDir()

key, cert := helpers.GenerateSelfSignedCert(t)

keyContents := helpers.Cert{Name: keyFileName, Type: privateKeyType, Contents: key}
certContents := helpers.Cert{Name: certFileName, Type: certificateType, Contents: cert}

keyFile := helpers.WriteCertFiles(t, tmpDir, keyContents)
certFile := helpers.WriteCertFiles(t, tmpDir, certContents)

testCases := []struct {
testName string
certFile string
keyFile string
isError bool
}{
{
testName: "valid files",
certFile: certFile,
keyFile: keyFile,
isError: false,
},
{
testName: "invalid cert file",
certFile: "/invalid/cert.pem",
keyFile: keyFile,
isError: true,
},
{
testName: "invalid key file",
certFile: certFile,
keyFile: "/invalid/key.pem",
isError: true,
},
}

for _, tc := range testCases {
t.Run(tc.testName, func(t *testing.T) {
certificate, pool, loadErr := LoadCertificates(tc.certFile, tc.keyFile)
if tc.isError {
assert.Nil(t, certificate)
assert.Nil(t, pool)
require.Error(t, loadErr)
} else {
assert.Equal(t, cert, certificate.Certificate[0])
assert.NotNil(t, pool)
require.NoError(t, loadErr)
}
})
}
}

func TestLoadCertificate(t *testing.T) {
tmpDir := t.TempDir()

_, cert := helpers.GenerateSelfSignedCert(t)

certContents := helpers.Cert{Name: certFileName, Type: certificateType, Contents: cert}
certNonPemContents := helpers.Cert{Name: nonPemCertFileName, Type: "", Contents: cert}

certFile := helpers.WriteCertFiles(t, tmpDir, certContents)
nonPEMFile := helpers.WriteCertFiles(t, tmpDir, certNonPemContents)
require.NotEmpty(t, nonPEMFile)

helpers.CreateFileWithErrorCheck(t, tmpDir, nonPemCertFileName)

testCases := []struct {
testName string
certFile string
isError bool
}{
{
testName: "valid cert file",
certFile: certFile,
isError: false,
},
{
testName: "invalid cert file",
certFile: "/invalid/cert.pem",
isError: true,
},
{
testName: "non-PEM cert file",
certFile: "",
isError: true,
},
}

for _, tc := range testCases {
t.Run(tc.testName, func(t *testing.T) {
certificate, loadErr := LoadCertificate(tc.certFile)
if tc.isError {
assert.Nil(t, certificate)
require.Error(t, loadErr)
} else {
assert.Equal(t, cert, certificate.Raw)
require.NoError(t, loadErr)
}
})
}
}
2 changes: 1 addition & 1 deletion internal/watcher/instance/nginx_config_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func (ncp *NginxConfigParser) sslCert(ctx context.Context, file, rootDir string)
if !ncp.agentConfig.IsDirectoryAllowed(file) {
slog.DebugContext(ctx, "File not in allowed directories", "file", file)
} else {
sslCertFileMeta, fileMetaErr := files.FileMeta(file)
sslCertFileMeta, fileMetaErr := files.FileMetaWithCertificate(file)
if fileMetaErr != nil {
slog.ErrorContext(ctx, "Unable to get file metadata", "file", file, "error", fileMetaErr)
} else {
Expand Down
14 changes: 9 additions & 5 deletions internal/watcher/instance/nginx_config_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,19 +346,23 @@ func TestNginxConfigParser_sslCert(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()

file1 := helpers.CreateFileWithErrorCheck(t, dir, "nginx-1.conf")
defer helpers.RemoveFileWithErrorCheck(t, file1.Name())
_, cert := helpers.GenerateSelfSignedCert(t)

certContents := helpers.Cert{Name: "nginx.cert", Type: "", Contents: cert}

certFile := helpers.WriteCertFiles(t, dir, certContents)
require.NotNil(t, certFile)

// Not in allowed directory
nginxConfig := NewNginxConfigParser(types.AgentConfig())
nginxConfig.agentConfig.AllowedDirectories = []string{}
sslCert := nginxConfig.sslCert(ctx, file1.Name(), dir)
sslCert := nginxConfig.sslCert(ctx, certFile, dir)
assert.Nil(t, sslCert)

// In allowed directory
nginxConfig.agentConfig.AllowedDirectories = []string{dir}
sslCert = nginxConfig.sslCert(ctx, file1.Name(), dir)
assert.Equal(t, file1.Name(), sslCert.GetFileMeta().GetName())
sslCert = nginxConfig.sslCert(ctx, certFile, dir)
assert.Equal(t, certFile, sslCert.GetFileMeta().GetName())
}

func TestNginxConfigParser_urlsForLocationDirective(t *testing.T) {
Expand Down
104 changes: 100 additions & 4 deletions pkg/files/file_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ package files

import (
"cmp"
"crypto/x509"
"fmt"
"net"
"os"
"slices"
"strconv"

"github.com/google/uuid"

mpi "github.com/nginx/agent/v3/api/grpc/mpi/v1"
"github.com/nginx/agent/v3/internal/datasource/cert"
"google.golang.org/protobuf/types/known/timestamppb"
)

const permissions = 0o644
const permissions = 0o600

// FileMeta returns a proto FileMeta struct from a given file path.
func FileMeta(filePath string) (*mpi.FileMeta, error) {
Expand All @@ -34,14 +37,70 @@ func FileMeta(filePath string) (*mpi.FileMeta, error) {
}

fileHash := GenerateHash(content)

return &mpi.FileMeta{
fileMeta := &mpi.FileMeta{
Name: filePath,
Hash: fileHash,
ModifiedTime: timestamppb.New(fileInfo.ModTime()),
Permissions: Permissions(fileInfo.Mode()),
Size: fileInfo.Size(),
}, nil
}

return fileMeta, nil
}

// FileMetaWithCertificate returns a FileMeta struct with certificate metadata if applicable.
func FileMetaWithCertificate(filePath string) (*mpi.FileMeta, error) {
fileMeta, err := FileMeta(filePath)
if err != nil {
return nil, err
}

loadedCert, certErr := cert.LoadCertificate(filePath)
if certErr != nil {
// If it's not a certificate, just return the base file metadata.
return fileMeta, certErr
}

// Populate certificate-specific metadata
fileMeta.FileType = &mpi.FileMeta_CertificateMeta{
CertificateMeta: &mpi.CertificateMeta{
SerialNumber: []byte{},
Issuer: &mpi.X509Name{
Country: loadedCert.Issuer.Country,
Organization: loadedCert.Issuer.Organization,
OrganizationalUnit: loadedCert.Issuer.OrganizationalUnit,
Locality: loadedCert.Issuer.Locality,
Province: loadedCert.Issuer.Province,
StreetAddress: loadedCert.Issuer.StreetAddress,
PostalCode: loadedCert.Issuer.PostalCode,
SerialNumber: loadedCert.Issuer.SerialNumber,
CommonName: loadedCert.Issuer.CommonName,
},
Subject: &mpi.X509Name{
Country: loadedCert.Subject.Country,
Organization: loadedCert.Subject.Organization,
OrganizationalUnit: loadedCert.Subject.OrganizationalUnit,
Locality: loadedCert.Subject.Locality,
Province: loadedCert.Subject.Province,
StreetAddress: loadedCert.Subject.StreetAddress,
PostalCode: loadedCert.Subject.PostalCode,
SerialNumber: loadedCert.Subject.SerialNumber,
CommonName: loadedCert.Subject.CommonName,
},
Sans: &mpi.SubjectAlternativeNames{
DnsNames: loadedCert.DNSNames,
IpAddresses: convertIPBytes(loadedCert.IPAddresses),
},
Dates: &mpi.CertificateDates{
NotBefore: loadedCert.NotBefore.Unix(),
NotAfter: loadedCert.NotAfter.Unix(),
},
SignatureAlgorithm: convertX509SignatureAlgorithm(loadedCert.SignatureAlgorithm),
PublicKeyAlgorithm: loadedCert.PublicKeyAlgorithm.String(),
},
}

return fileMeta, nil
}

// Permissions returns a file's permissions as a string.
Expand Down Expand Up @@ -88,3 +147,40 @@ func ConvertToMapOfFiles(files []*mpi.File) map[string]*mpi.File {

return filesMap
}

func convertIPBytes(ips []net.IP) []string {
stringArray := make([]string, len(ips))

for i, byteArray := range ips {
stringArray[i] = string(byteArray)
}

return stringArray
}

func convertX509SignatureAlgorithm(alg x509.SignatureAlgorithm) mpi.SignatureAlgorithm {
x509ToMpiSignatureMap := map[x509.SignatureAlgorithm]mpi.SignatureAlgorithm{
x509.MD2WithRSA: mpi.SignatureAlgorithm_MD2_WITH_RSA,
x509.MD5WithRSA: mpi.SignatureAlgorithm_MD5_WITH_RSA,
x509.SHA1WithRSA: mpi.SignatureAlgorithm_SHA1_WITH_RSA,
x509.SHA256WithRSA: mpi.SignatureAlgorithm_SHA256_WITH_RSA,
x509.SHA384WithRSA: mpi.SignatureAlgorithm_SHA384_WITH_RSA,
x509.SHA512WithRSA: mpi.SignatureAlgorithm_SHA512_WITH_RSA,
x509.DSAWithSHA1: mpi.SignatureAlgorithm_DSA_WITH_SHA1,
x509.DSAWithSHA256: mpi.SignatureAlgorithm_DSA_WITH_SHA256,
x509.ECDSAWithSHA1: mpi.SignatureAlgorithm_ECDSA_WITH_SHA1,
x509.ECDSAWithSHA256: mpi.SignatureAlgorithm_ECDSA_WITH_SHA256,
x509.ECDSAWithSHA384: mpi.SignatureAlgorithm_ECDSA_WITH_SHA384,
x509.ECDSAWithSHA512: mpi.SignatureAlgorithm_ECDSA_WITH_SHA512,
x509.SHA256WithRSAPSS: mpi.SignatureAlgorithm_SHA256_WITH_RSA_PSS,
x509.SHA384WithRSAPSS: mpi.SignatureAlgorithm_SHA384_WITH_RSA_PSS,
x509.SHA512WithRSAPSS: mpi.SignatureAlgorithm_SHA512_WITH_RSA_PSS,
x509.PureEd25519: mpi.SignatureAlgorithm_PURE_ED25519,
x509.UnknownSignatureAlgorithm: mpi.SignatureAlgorithm_SIGNATURE_ALGORITHM_UNKNOWN,
}
if mappedAlg, exists := x509ToMpiSignatureMap[alg]; exists {
return mappedAlg
}

return mpi.SignatureAlgorithm_SIGNATURE_ALGORITHM_UNKNOWN
}
Loading

0 comments on commit f010cf3

Please sign in to comment.