Skip to content

Commit

Permalink
Merge pull request #252 from Venafi/enhancement/certificate-search
Browse files Browse the repository at this point in the history
Search valid certificate
  • Loading branch information
luispresuelVenafi authored Aug 30, 2022
2 parents 8713a2f + bb2100b commit 0890bb6
Show file tree
Hide file tree
Showing 14 changed files with 743 additions and 48 deletions.
55 changes: 50 additions & 5 deletions pkg/certificate/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import (
"time"

"github.com/Venafi/vcert/v4/pkg/verror"
"reflect"
"sort"
)

// EllipticCurve represents the types of supported elliptic curves
Expand Down Expand Up @@ -342,12 +344,18 @@ type ImportResponse struct {
PrivateKeyVaultId int `json:",omitempty"`
}

type Sans struct {
DNS []string
Email []string `json:",omitempty"`
IP []string `json:",omitempty"`
URI []string `json:",omitempty"`
UPN []string `json:",omitempty"`
}

type CertificateInfo struct {
ID string
CN string
SANS struct {
DNS, Email, IP, URI, UPN []string
}
ID string `json:",omitempty"`
CN string
SANS Sans
Serial string
Thumbprint string
ValidFrom time.Time
Expand Down Expand Up @@ -738,3 +746,40 @@ func NewRequest(cert *x509.Certificate) *Request {
}
return req
}

// find a certificate from a list of certificates whose Sans.DNS matches and is
// the newest
func FindNewestCertificateWithSans(certificates []*CertificateInfo, sans_ *Sans) (*CertificateInfo, error) {
sans := Sans{}

if sans_ != nil {
sans.DNS = sans_.DNS
}

// order provided SANS-DNS
sort.Strings(sans.DNS)

// create local variable to hold the newest certificate
var newestCertificate *CertificateInfo
for _, certificate := range certificates {
// order certificate SANS before comparison
if certificate.SANS.DNS != nil {
sort.Strings(certificate.SANS.DNS)
}
// exact match SANs
if reflect.DeepEqual(sans.DNS, certificate.SANS.DNS) {
// update the certificate to the newest match
if newestCertificate == nil || certificate.ValidTo.Unix() > newestCertificate.ValidTo.Unix() {
newestCertificate = certificate
}
}
}

// a valid certificate has been found, return it
if newestCertificate != nil {
return newestCertificate, nil
}

// fail, since no valid certificate was found at this point
return nil, verror.NoCertificateFoundError
}
139 changes: 139 additions & 0 deletions pkg/certificate/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"math/big"
"net"
"os"
"reflect"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -580,3 +581,141 @@ func pemRSADecode(priv string) *rsa.PrivateKey {
}
return parsedKey.(*rsa.PrivateKey)
}

type FindNewestCertificateWithSansMock struct {
// testCase name
name string
// expected returned certificate
expected *CertificateInfo
// sans argument passed to FindNewestCertificateWithSans function
sans *Sans
}

func createTimeMock(t *testing.T, s string) time.Time {
time, err := time.Parse(time.RFC3339, s)

if err != nil {
t.Error(err)
t.Fatalf("error parsing time string %q", s)
}

return time
}

func TestFindNewestCertificateWithSans(t *testing.T) {
// create a list of certificates where we will try to find according to our
// testCases, we are not interested in any other field but the `SANS.DNS`,
// and `ValidTo` which are the ones used to match in the
// `FindNewestCertificateWithSans` function
certificates := []*CertificateInfo{
{
SANS: Sans{
DNS: []string{
"one.vfidev.com",
},
},
ValidTo: createTimeMock(t, "2022-08-22T00:00:00.000+00:00"),
},
{
SANS: Sans{
DNS: []string{
"one.vfidev.com",
"two.vfidev.com",
},
},
ValidTo: createTimeMock(t, "2022-08-22T00:00:00.000+00:00"),
},
{
SANS: Sans{
DNS: []string{
"one.vfidev.com",
"two.vfidev.com",
},
},
ValidTo: createTimeMock(t, "2030-01-01T00:00:00.000+00:00"),
},
{
SANS: Sans{
DNS: []string{
"1.vfidev.com",
"2.vfidev.com",
"3.vfidev.com",
},
},
ValidTo: createTimeMock(t, "2022-08-22T00:00:00.000+00:00"),
},
}

testCases := []FindNewestCertificateWithSansMock{
// should not return any certificate
{
name: "Empty SANS",
expected: nil,
sans: nil,
},
// should return the only existing certificate
{
name: "Simple",
expected: &CertificateInfo{
SANS: Sans{
DNS: []string{
"one.vfidev.com",
},
},
ValidTo: createTimeMock(t, "2022-08-22T00:00:00.000+00:00"),
},
sans: &Sans{DNS: []string{"one.vfidev.com"}},
},
//should return the newest certificate
{
name: "Newest",
expected: &CertificateInfo{
SANS: Sans{
DNS: []string{
"one.vfidev.com",
"two.vfidev.com",
},
},
ValidTo: createTimeMock(t, "2030-01-01T00:00:00.000+00:00"),
},
sans: &Sans{DNS: []string{"one.vfidev.com", "two.vfidev.com"}},
},
// should return the only existing certificate regardless of the order of
// the sans arguments
{
name: "Order of arguments",
expected: &CertificateInfo{
SANS: Sans{
DNS: []string{
"1.vfidev.com",
"2.vfidev.com",
"3.vfidev.com",
},
},
ValidTo: createTimeMock(t, "2022-08-22T00:00:00.000+00:00"),
},
sans: &Sans{DNS: []string{"3.vfidev.com", "2.vfidev.com", "1.vfidev.com"}},
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
// ignore error, just use the certificate value as a sign of success
certificate, _ := FindNewestCertificateWithSans(certificates, testCase.sans)

// certificate should have been found but function returned no certificate
if testCase.expected != nil && certificate == nil {
t.Fatalf("certificate should have been found but function returned no certificate\nsans provided: %v\nexpected certificate: %v", util.GetJsonAsString(testCase.sans), util.GetJsonAsString(testCase.expected))
}

// no certificate should have been found but function returned one
if testCase.expected == nil && certificate != nil {
t.Fatalf("no certificate should have been found but function returned one\nsans provided: %v\nreturned certificate: %v", util.GetJsonAsString(testCase.sans), util.GetJsonAsString(certificate))
}

if !reflect.DeepEqual(testCase.expected, certificate) {
t.Fatalf("certificates did not match.\nexpected:\n%v\ngot:\n%v", util.GetJsonAsString(testCase.expected), util.GetJsonAsString(certificate))
}
})
}
}
12 changes: 12 additions & 0 deletions pkg/endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"net"
"net/http"
"regexp"
"time"

"github.com/Venafi/vcert/v4/pkg/policy"

Expand Down Expand Up @@ -105,6 +106,17 @@ type Connector interface {
RetrieveSSHCertificate(req *certificate.SshCertRequest) (response *certificate.SshCertificateObject, err error)
RetrieveSshConfig(ca *certificate.SshCaTemplateRequest) (*certificate.SshConfig, error)
SearchCertificates(req *certificate.SearchRequest) (*certificate.CertSearchResponse, error)
// Returns a valid certificate
//
// If it returns no error, the certificate returned should be the latest [1]
// exact matching zone [2], CN and sans.DNS [3] provided, with a minimum
// validity of `certMinTimeLeft`
//
// [1] the one with longest validity; field named ValidTo for TPP and
// validityEnd for VaaS
// [2] application name for VaaS
// [3] an array of strings representing the DNS names
SearchCertificate(zone string, cn string, sans *certificate.Sans, certMinTimeLeft time.Duration) (*certificate.CertificateInfo, error)
RetrieveAvailableSSHTemplates() ([]certificate.SshAvaliableTemplate, error)
RetrieveCertificateMetaData(dn string) (*certificate.CertificateMetaData, error)
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,13 @@ func GetPrivateKeyType(pk, pass string) string {

return keyType
}

// TODO: test this function
func ArrayContainsString(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
89 changes: 82 additions & 7 deletions pkg/venafi/cloud/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"fmt"
"io/ioutil"
"log"
"math"
"net/http"
netUrl "net/url"
"regexp"
Expand Down Expand Up @@ -98,6 +99,76 @@ func (c *Connector) SearchCertificates(req *certificate.SearchRequest) (*certifi
panic("operation is not supported yet")
}

func (c *Connector) SearchCertificate(zone string, cn string, sans *certificate.Sans, certMinTimeLeft time.Duration) (certificateInfo *certificate.CertificateInfo, err error) {
appName := getAppNameFromZone(zone)
// get application id
app, _, err := c.getAppDetailsByName(appName)
if err != nil {
return nil, err
}

// convert a time.Duration to days
certMinTimeDays := math.Floor(certMinTimeLeft.Hours() / 24)

// format arguments for request
req := &SearchRequest{
Expression: &Expression{
Operator: AND,
Operands: []Operand{
{
Field: "subjectCN",
Operator: EQ,
Value: cn,
},
{
Field: "subjectAlternativeNameDns",
Operator: IN,
Values: sans.DNS,
},
{
Field: "validityPeriodDays",
Operator: GTE,
Value: certMinTimeDays,
},
},
},
}

// perform request
searchResult, err := c.searchCertificates(req)
if err != nil {
return nil, err
}

// fail if no certificate is returned from api
if searchResult.Count == 0 {
return nil, verror.NoCertificateFoundError
}

// map (convert) response to an array of CertificateInfo, TODO: only add
// those certificates whose Zone matches ours
certificates := make([]*certificate.CertificateInfo, 0)
n := 0
for _, cert := range searchResult.Certificates {
// log.Printf("looping %v\n", util.GetJsonAsString(cert))
// TODO: filter based on applicationId (VaaS equivalent to TPP Zone)
if util.ArrayContainsString(cert.ApplicationIds, app.ApplicationId) {
match := cert.ToCertificateInfo()
certificates = append(certificates, &match)
n = n + 1
}
}

// fail if no certificates found with matching zone
if n == 0 {
return nil, verror.NoCertificateWithMatchingZoneFoundError
}

// at this point all certificates belong to our zone, the next step is
// finding the newest valid certificate matching the provided sans
return certificate.FindNewestCertificateWithSans(certificates, sans)
}

func (c *Connector) IsCSRServiceGenerated(req *certificate.Request) (bool, error) {
if c.user == nil || c.user.Company == nil {
return false, fmt.Errorf("must be autheticated to retieve certificate")
Expand Down Expand Up @@ -1289,9 +1360,9 @@ func (c *Connector) searchCertificatesByFingerprint(fp string) (*CertificateSear
Expression: &Expression{
Operands: []Operand{
{
"fingerprint",
MATCH,
fp,
Field: "fingerprint",
Operator: MATCH,
Value: fp,
},
},
},
Expand Down Expand Up @@ -1470,17 +1541,21 @@ func (c *Connector) getCertsBatch(page, pageSize int, withExpired bool) ([]certi
req := &SearchRequest{
Expression: &Expression{
Operands: []Operand{
{"appstackIds", MATCH, appDetails.ApplicationId},
{
Field: "appstackIds",
Operator: MATCH,
Value: appDetails.ApplicationId,
},
},
Operator: AND,
},
Paging: &Paging{PageSize: pageSize, PageNumber: page},
}
if !withExpired {
req.Expression.Operands = append(req.Expression.Operands, Operand{
"validityEnd",
GTE,
time.Now().Format(time.RFC3339),
Field: "validityEnd",
Operator: GTE,
Value: time.Now().Format(time.RFC3339),
})
}
r, err := c.searchCertificates(req)
Expand Down
Loading

0 comments on commit 0890bb6

Please sign in to comment.