Skip to content

Commit

Permalink
Merge pull request #119 from boxboat/ite7/cert-constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
adityasaky authored Sep 10, 2021
2 parents 48fefbd + 234eeb6 commit 02b98c8
Show file tree
Hide file tree
Showing 33 changed files with 2,086 additions and 544 deletions.
12 changes: 12 additions & 0 deletions in_toto/canonicaljson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,23 @@ func TestEncodeCanonical(t *testing.T) {
"int2": float64(42),
"string": `\"`,
},
Key{
KeyVal: KeyVal{
Certificate: "cert",
Private: "priv",
Public: "pub",
},
KeyIDHashAlgorithms: []string{"hash"},
KeyID: "id",
KeyType: "type",
Scheme: "scheme",
},
}
expectedResult := []string{
`{"keyid":"","keyid_hash_algorithms":null,"keytype":"","keyval":{"private":"","public":""},"scheme":""}`,
`{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"private":"priv","public":"pub"},"scheme":"scheme"}`,
`{"false":false,"int":3,"int2":42,"nil":null,"string":"\\\"","true":true}`,
`{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"certificate":"cert","private":"priv","public":"pub"},"scheme":"scheme"}`,
"",
}
for i := 0; i < len(objects); i++ {
Expand Down
156 changes: 156 additions & 0 deletions in_toto/certconstraint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package in_toto

import (
"crypto/x509"
"fmt"
"net/url"
)

const (
AllowAllConstraint = "*"
)

// CertificateConstraint defines the attributes a certificate must have to act as a functionary.
// A wildcard `*` allows any value in the specified attribute, where as an empty array or value
// asserts that the certificate must have nothing for that attribute. A certificate must have
// every value defined in a constraint to match.
type CertificateConstraint struct {
CommonName string `json:"common_name"`
DNSNames []string `json:"dns_names"`
Emails []string `json:"emails"`
Organizations []string `json:"organizations"`
Roots []string `json:"roots"`
URIs []string `json:"uris"`
}

// checkResult is a data structure used to hold
// certificate constraint errors
type checkResult struct {
errors []error
}

// newCheckResult initializes a new checkResult
func newCheckResult() *checkResult {
return &checkResult{
errors: make([]error, 0),
}
}

// evaluate runs a constraint check on a certificate
func (cr *checkResult) evaluate(cert *x509.Certificate, constraintCheck func(*x509.Certificate) error) *checkResult {
err := constraintCheck(cert)
if err != nil {
cr.errors = append(cr.errors, err)
}
return cr
}

// error reduces all of the errors into one error with a
// combined error message. If there are no errors, nil
// will be returned.
func (cr *checkResult) error() error {
if len(cr.errors) == 0 {
return nil
}
return fmt.Errorf("cert failed constraints check: %+q", cr.errors)
}

// Check tests the provided certificate against the constraint. An error is returned if the certificate
// fails any of the constraints. nil is returned if the certificate passes all of the constraints.
func (cc CertificateConstraint) Check(cert *x509.Certificate, rootCAIDs []string, rootCertPool, intermediateCertPool *x509.CertPool) error {
return newCheckResult().
evaluate(cert, cc.checkCommonName).
evaluate(cert, cc.checkDNSNames).
evaluate(cert, cc.checkEmails).
evaluate(cert, cc.checkOrganizations).
evaluate(cert, cc.checkRoots(rootCAIDs, rootCertPool, intermediateCertPool)).
evaluate(cert, cc.checkURIs).
error()
}

// checkCommonName verifies that the certificate's common name matches the constraint.
func (cc CertificateConstraint) checkCommonName(cert *x509.Certificate) error {
return checkCertConstraint("common name", []string{cc.CommonName}, []string{cert.Subject.CommonName})
}

// checkDNSNames verifies that the certificate's dns names matches the constraint.
func (cc CertificateConstraint) checkDNSNames(cert *x509.Certificate) error {
return checkCertConstraint("dns name", cc.DNSNames, cert.DNSNames)
}

// checkEmails verifies that the certificate's emails matches the constraint.
func (cc CertificateConstraint) checkEmails(cert *x509.Certificate) error {
return checkCertConstraint("email", cc.Emails, cert.EmailAddresses)
}

// checkOrganizations verifies that the certificate's organizations matches the constraint.
func (cc CertificateConstraint) checkOrganizations(cert *x509.Certificate) error {
return checkCertConstraint("organization", cc.Organizations, cert.Subject.Organization)
}

// checkRoots verifies that the certificate's roots matches the constraint.
// The certificates trust chain must also be verified.
func (cc CertificateConstraint) checkRoots(rootCAIDs []string, rootCertPool, intermediateCertPool *x509.CertPool) func(*x509.Certificate) error {
return func(cert *x509.Certificate) error {
_, err := VerifyCertificateTrust(cert, rootCertPool, intermediateCertPool)
if err != nil {
return fmt.Errorf("failed to verify roots: %w", err)
}
return checkCertConstraint("root", cc.Roots, rootCAIDs)
}
}

// checkURIs verifies that the certificate's URIs matches the constraint.
func (cc CertificateConstraint) checkURIs(cert *x509.Certificate) error {
return checkCertConstraint("uri", cc.URIs, urisToStrings(cert.URIs))
}

// urisToStrings is a helper that converts a list of URL objects to the string that represents them
func urisToStrings(uris []*url.URL) []string {
res := make([]string, 0, len(uris))
for _, uri := range uris {
res = append(res, uri.String())
}

return res
}

// checkCertConstraint tests that the provided test values match the allowed values of the constraint.
// All allowed values must be met one-to-one to be considered a successful match.
func checkCertConstraint(attributeName string, constraints, values []string) error {
// If the only constraint is to allow all, the check succeeds
if len(constraints) == 1 && constraints[0] == AllowAllConstraint {
return nil
}

if len(constraints) == 1 && constraints[0] == "" {
constraints = []string{}
}

if len(values) == 1 && values[0] == "" {
values = []string{}
}

// If no constraints are specified, but the certificate has values for the attribute, then the check fails
if len(constraints) == 0 && len(values) > 0 {
return fmt.Errorf("not expecting any %s(s), but cert has %d %s(s)", attributeName, len(values), attributeName)
}

unmet := NewSet(constraints...)
for _, v := range values {
// if the cert has a value we didn't expect, fail early
if !unmet.Has(v) {
return fmt.Errorf("cert has an unexpected %s %s given constraints %+q", attributeName, v, constraints)
}

// consider the constraint met
unmet.Remove(v)
}

// if we have any unmet left after going through each test value, fail.
if len(unmet) > 0 {
return fmt.Errorf("cert with %s(s) %+q did not pass all constraints %+q", attributeName, values, constraints)
}

return nil
}
Loading

0 comments on commit 02b98c8

Please sign in to comment.