diff --git a/docs/policy.md b/docs/policy.md index 1c9c9ad8..a5167d4c 100644 --- a/docs/policy.md +++ b/docs/policy.md @@ -77,8 +77,42 @@ Policies are JSON documents that are signed and wrapped in a DSSE envelope. The | Key | Type | Description | | --- | ---- | ----------- | +| `commonname` | string | Common name that the certifiate's subject must have | +| `dnsnames` | array of strings | DNS names that the certificate must have | +| `emails` | array of strings | Email addresses that the certificate must have | +| `organizations` | array of strings | Organizations that the certificate must have | +| `uris` | array of strings | URIs that the certificate must have | | `roots` | array of strings | Array of Key IDs the signer's certificate must belong to to be trusted. | +Every attribute of the certificate must match the attributes defined by the constraint exactly. A certificate must match +at least one constraint to pass the policy. Wildcards are allowed if they are the only elemnt in the constraint. + +Example of a constraint that would allow any certificate, as long as it belongs to a root defined in the policy: + +``` +{ + "commonname": "*", + "dnsnames": ["*"], + "emails": ["*"], + "organizations": ["*"], + "uris": ["*"], + "roots": ["*"] +} +``` + +SPIFFE IDs are defined as URIs on the certificate, so a policy that would enforce a SPIFFE ID may look like: + +``` +{ + "commonname": "*", + "dnsnames": ["*"], + "emails": ["*"], + "organizations": ["*"], + "uris": ["spiffe://example.com/step1"], + "roots": ["*"] +} +``` + ### `attestation` Object | Key | Type | Description | @@ -112,8 +146,8 @@ deny[msg] { { "expires": "2022-12-17T23:57:40-05:00", "steps": { - "clone": { - "name": "clone", + "clone": { + "name": "clone", "attestations": [ { "type": "https://witness.testifysec.com/attestations/material/v0.1", @@ -134,10 +168,10 @@ deny[msg] { "publickeyid": "ae2dcc989ea9c109a36e8eba5c4bc16d8fafcfe8e1a614164670d50aedacd647" } ] - }, + }, "build": { "name": "build", - "artifactsFrom": ["clone"], + "artifactsFrom": ["clone"], "attestations": [ { "type": "https://witness.testifysec.com/attestations/material/v0.1", @@ -161,6 +195,17 @@ deny[msg] { { "type": "publickey", "publickeyid": "ae2dcc989ea9c109a36e8eba5c4bc16d8fafcfe8e1a614164670d50aedacd647" + }, + { + "type": "root", + "certConstraint": { + "commonname": "*", + "dnsnames": ["*"], + "emails": ["*"], + "organizations": ["*"], + "uris": ["spiffe://example.com/step1"], + "roots": ["ae2dcc989ea9c109a36e8eba5c4bc16d8fafcfe8e1a614164670d50aedacd647"] + } } ] } @@ -170,6 +215,11 @@ deny[msg] { "keyid": "ae2dcc989ea9c109a36e8eba5c4bc16d8fafcfe8e1a614164670d50aedacd647", "key": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQWYyOW9QUDhVZ2hCeUc4NTJ1QmRPeHJKS0tuN01NNWhUYlA5ZXNnT1ovazA9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=" } + }, + "roots": { + "949aaab542a02514f27f41ed8e443bb54bbd9b062ca3ce1da2492170d8fffe98": { + "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURhekNDQWxPZ0F3SUJBZ0lVSnlobzI5ckorTXZYdGhGZjRncnV3UWhUZVNNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JURUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTQpHRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpEQWVGdzB5TWpBeU1qTXlNalV4TkRoYUZ3MHlOekF5Ck1qSXlNalV4TkRoYU1FVXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwVGIyMWxMVk4wWVhSbE1TRXcKSHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0VpTUEwR0NTcUdTSWIzRFFFQgpBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ3VnVnNVYlV1cHB6S3ArOUxyckxLeGFrc0JlVTRiei9lQ0w1ZXo0bEppClFhcm1vcVRDeWI0WlVqVTNTSCsxYVdLSU9aM2kyeUZmL0hYRktNemh5SHFWZnpzbDVJUEo5TzVTR0huK3FldnoKVzBTMVdQeEN4MS9KdlFoUFNaQ21adWhaMmI5NFVYdXhCL2tSWGRiNnhYdnVReVFPMDYybTQrTkZWYVhBWWZjTQprVUlBSnpQTUZUSHhKOUQ1dWdaMWlSV0VHUUQ1d2kwNS9ZRG5yZHR3N2J3V3ZkOW4yL3c1UHUvUU1iVHZ4NWxlCnNFK2U1ZWZZd1NZLzBvT2dWRHBHVG9TVStpeDMrYWVlVjFSL1IvNm81NlJ0LzQ5eG9KWjF5bCtyQ3ByOUswN3AKL0FOSk9HTE5oYlRXVGp1N1lTSUxtbnYreVJwRUdUTnptU1lpNEFFTStZYm5BZ01CQUFHalV6QlJNQjBHQTFVZApEZ1FXQkJRemppS2pzR1NZNjUvNTFlQVJINVpEdXFIOUtEQWZCZ05WSFNNRUdEQVdnQlF6amlLanNHU1k2NS81CjFlQVJINVpEdXFIOUtEQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQmgKUXhBNWExMUJ4VXh6Q1hObDg4ZUxkaDg5NlNudkdwYkNTZVhxQzJKS0w2QkxHVHg4NC9SZmMxNzVyZ2VUTW9YcQpEWjA1Nm9xc1FPZ2czWVRXWEJDMTJJbmVnUW40Wm90L2cydWk3cTJOZ0NZNWNSSG9qZnhQd2JxbS9uU2k1eXNSClFCQTZuMUJ3cUlZclBpVVBvcE9YY1BIQVJ4SEwzUitIOHRpWCtyM1hRM3FZdnNuTUpOL3JlcGJOQjJKVi9TL28KT0llT1U5Y1RJRnRHNWNNd2RHcTdMeVlkK095NkRiNjN5aDNkNS82bEZOVElqdlZXaHhzS280U3dxZlhuOXY4TApia2xTOFB0Mm12MVMxa2thZGhMT1FqaGlBQ1N2UHB6OW5USXdXWTJUYTcvNGpFR0I3ZTF3aU8wZ0dhbFJhVXQyClpmYmt3eXFSQWxXUXNBcDJqZS8wCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" + } } } ``` diff --git a/pkg/policy/constraints.go b/pkg/policy/constraints.go new file mode 100644 index 00000000..c4b95470 --- /dev/null +++ b/pkg/policy/constraints.go @@ -0,0 +1,138 @@ +// Copyright 2022 The Witness Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package policy + +import ( + "fmt" + "net/url" + + "github.com/testifysec/witness/pkg/cryptoutil" +) + +const ( + AllowAllConstraint = "*" +) + +type CertConstraint struct { + CommonName string `json:"commonname"` + DNSNames []string `json:"dnsnames"` + Emails []string `json:"emails"` + Organizations []string `json:"organizations"` + URIs []string `json:"uris"` + Roots []string `json:"roots"` +} + +func (cc CertConstraint) Check(verifier *cryptoutil.X509Verifier, trustBundles map[string]TrustBundle) error { + errs := make([]error, 0) + cert := verifier.Certificate() + + if err := checkCertConstraint("common name", []string{cc.CommonName}, []string{cert.Subject.CommonName}); err != nil { + errs = append(errs, err) + } + + if err := checkCertConstraint("dns name", cc.DNSNames, cert.DNSNames); err != nil { + errs = append(errs, err) + } + + if err := checkCertConstraint("email", cc.Emails, cert.EmailAddresses); err != nil { + errs = append(errs, err) + } + + if err := checkCertConstraint("organization", cc.Organizations, cert.Subject.Organization); err != nil { + errs = append(errs, err) + } + + if err := checkCertConstraint("uri", cc.URIs, urisToStrings(cert.URIs)); err != nil { + errs = append(errs, err) + } + + if err := cc.checkTrustBundles(verifier, trustBundles); err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return ErrConstraintCheckFailed{errs} + } + + return nil +} + +func (cc CertConstraint) checkTrustBundles(verifier *cryptoutil.X509Verifier, trustBundles map[string]TrustBundle) error { + if len(cc.Roots) == 1 && cc.Roots[0] == AllowAllConstraint { + for _, bundle := range trustBundles { + if err := verifier.BelongsToRoot(bundle.Root); err == nil { + return nil + } + } + } else { + for _, rootID := range cc.Roots { + if bundle, ok := trustBundles[rootID]; ok { + if err := verifier.BelongsToRoot(bundle.Root); err == nil { + return nil + } + } + } + } + + return fmt.Errorf("cert doesn't belong to any root specified by constraint %+q", cc.Roots) +} + +func urisToStrings(uris []*url.URL) []string { + res := make([]string, 0) + for _, uri := range uris { + res = append(res, uri.String()) + } + + return res +} + +func checkCertConstraint(attribute string, constraints, values []string) error { + // If our only constraint is the AllowAllConstraint it's a pass + if len(constraints) == 1 && constraints[0] == AllowAllConstraint { + return nil + } + + // treat a single empty string the same as a constraint on an empty attribute + if len(constraints) == 1 && constraints[0] == "" { + constraints = []string{} + } + + if len(values) == 1 && values[0] == "" { + values = []string{} + } + + if len(constraints) == 0 && len(values) > 0 { + return fmt.Errorf("not expecting any %s(s), but cert has %d %s(s)", attribute, len(values), attribute) + } + + unmet := make(map[string]struct{}) + for _, constraint := range constraints { + unmet[constraint] = struct{}{} + } + + for _, value := range values { + if _, ok := unmet[value]; !ok { + return fmt.Errorf("cert has an unexpected %s %s given constraints %+q", attribute, value, constraints) + } + + delete(unmet, value) + } + + if len(unmet) > 0 { + return fmt.Errorf("cert with %s(s) %+qDid not pass all constraints %+q", attribute, values, constraints) + } + + return nil +} diff --git a/pkg/policy/constraints_test.go b/pkg/policy/constraints_test.go new file mode 100644 index 00000000..86105030 --- /dev/null +++ b/pkg/policy/constraints_test.go @@ -0,0 +1,478 @@ +// Copyright 2022 The Witness Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package policy + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/testifysec/witness/pkg/cryptoutil" +) + +type checkConstraintAttributeCase struct { + Constraints []string + Values []string + Expected bool +} + +func TestCheckCertConstraint(t *testing.T) { + cases := []checkConstraintAttributeCase{ + { + Constraints: []string{"test1", "test2"}, + Values: []string{"test2", "test1"}, + Expected: true, + }, + { + Constraints: []string{"test1", "test2"}, + Values: []string{"test2"}, + Expected: false, + }, + { + Constraints: []string{AllowAllConstraint}, + Values: []string{"any", "thing", "goes"}, + Expected: true, + }, + { + Constraints: []string{}, + Values: []string{}, + Expected: true, + }, + { + Constraints: []string{}, + Values: []string{"test1"}, + Expected: false, + }, + { + Constraints: []string{""}, + Values: []string{""}, + Expected: true, + }, + { + Constraints: []string{""}, + Values: []string{"test1"}, + Expected: false, + }, + { + Constraints: []string{"test1", "test2"}, + Values: []string{"test1", "test2", "test3"}, + Expected: false, + }, + } + + for _, c := range cases { + err := checkCertConstraint("constraint", c.Constraints, c.Values) + assert.Equal(t, c.Expected, err == nil, fmt.Sprintf("Constraints: %v, Values: %v", c.Constraints, c.Values)) + } +} + +type constraintCheckCase struct { + Constraint CertConstraint + Cert *x509.Certificate + Expected bool +} + +func TestConstraintCheck(t *testing.T) { + testCertSubject := pkix.Name{ + CommonName: "step1.example.com", + Organization: []string{"example"}, + } + testCertEmails := []string{"example@example.com"} + testCertDNSNames := []string{"example.com"} + testCertURI, _ := url.Parse("spiffe://example.com/step1") + testCertURIs := []*url.URL{testCertURI} + testertValidity := 1 * time.Hour + testCertPublicKeyAlgorithm := x509.Ed25519 + testCertTemplate := &x509.Certificate{ + Subject: testCertSubject, + EmailAddresses: testCertEmails, + DNSNames: testCertDNSNames, + URIs: testCertURIs, + } + + testCert, testIntermediateCert, testRootCert, err := createTestCert(testCertTemplate, testCertPublicKeyAlgorithm, testertValidity) + require.NoError(t, err) + verifier, err := cryptoutil.NewX509Verifier(testCert, []*x509.Certificate{testIntermediateCert}, []*x509.Certificate{testRootCert}, time.Time{}) + require.NoError(t, err) + trustBundles := map[string]TrustBundle{ + "example": { + Root: testRootCert, + Intermediates: []*x509.Certificate{testIntermediateCert}, + }, + } + + cases := []constraintCheckCase{ + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "step1.example.com", + DNSNames: []string{"example.com"}, + Emails: []string{"example@example.com"}, + Organizations: []string{"example"}, + Roots: []string{"example"}, + URIs: []string{"spiffe://example.com/step1"}, + }, + Expected: true, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "*", + DNSNames: []string{"*"}, + Emails: []string{"*"}, + Organizations: []string{"*"}, + Roots: []string{"*"}, + URIs: []string{"*"}, + }, + Expected: true, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "", + DNSNames: []string{}, + Emails: []string{}, + Organizations: []string{}, + Roots: []string{}, + URIs: []string{}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "", + DNSNames: []string{""}, + Emails: []string{""}, + Organizations: []string{""}, + Roots: []string{""}, + URIs: []string{""}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "", + DNSNames: []string{"example.com"}, + Emails: []string{"example@example.com"}, + Organizations: []string{"example"}, + Roots: []string{"example"}, + URIs: []string{"spiffe://example.com/step1"}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "step1.example.com", + DNSNames: []string{}, + Emails: []string{"example@example.com"}, + Organizations: []string{"example"}, + Roots: []string{"example"}, + URIs: []string{"spiffe://example.com/step1"}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "step1.example.com", + DNSNames: []string{"example.com"}, + Emails: []string{}, + Organizations: []string{"example"}, + Roots: []string{"example"}, + URIs: []string{"spiffe://example.com/step1"}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "step1.example.com", + DNSNames: []string{"example.com"}, + Emails: []string{"example@example.com"}, + Organizations: []string{}, + Roots: []string{"example"}, + URIs: []string{"spiffe://example.com/step1"}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "step1.example.com", + DNSNames: []string{"example.com"}, + Emails: []string{"example@example.com"}, + Organizations: []string{"example"}, + Roots: []string{}, + URIs: []string{"spiffe://example.com/step1"}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "*", + DNSNames: []string{"*"}, + Emails: []string{"*"}, + Organizations: []string{"*"}, + Roots: []string{"example2"}, + URIs: []string{"*"}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "step1.example.com", + DNSNames: []string{"example.com"}, + Emails: []string{"example@example.com"}, + Organizations: []string{"example"}, + Roots: []string{"example"}, + URIs: []string{}, + }, + Expected: false, + }, + { + Cert: testCert, + Constraint: CertConstraint{ + CommonName: "*", + DNSNames: []string{"*"}, + Emails: []string{"*"}, + Organizations: []string{"*"}, + Roots: []string{"*"}, + URIs: []string{"spiffe://example.com/step2"}, + }, + Expected: false, + }, + } + + for _, c := range cases { + err := c.Constraint.Check(verifier, trustBundles) + assert.Equal(t, c.Expected, err == nil, fmt.Sprintf("Constraint: %v, Errors: %s", c.Constraint, err)) + } +} + +func createTestCert(template *x509.Certificate, publicKeyAlgorithm x509.PublicKeyAlgorithm, validity time.Duration) (*x509.Certificate, *x509.Certificate, *x509.Certificate, error) { + rootCertSubject := pkix.Name{ + CommonName: "Root CA", + } + rootCertMaxPathLen := 1 + rootCertValidity := 10 * 365 * 24 * time.Hour // 10 years + rootCertPublicKeyAlgorithm := x509.Ed25519 + rootCertTemplate := &x509.Certificate{ + Subject: rootCertSubject, + MaxPathLen: rootCertMaxPathLen, + } + rootCert, _, rootKey, err := createSelfSignedCA(rootCertTemplate, rootCertPublicKeyAlgorithm, rootCertValidity) + if err != nil { + return nil, nil, nil, err + } + + intermediateCertSubject := pkix.Name{ + CommonName: "Intermediate CA", + } + intermediateCertMaxPathLen := 0 + intermediateCertValidity := 10 * 365 * 24 * time.Hour + intermediateCertPublicKeyAlgorithm := x509.Ed25519 + intermediateCertTemplate := &x509.Certificate{ + Subject: intermediateCertSubject, + MaxPathLen: intermediateCertMaxPathLen, + } + intermediateCert, _, intermediateKey, err := createCA(intermediateCertTemplate, rootCert, rootKey, intermediateCertPublicKeyAlgorithm, intermediateCertValidity) + if err != nil { + return nil, nil, nil, err + } + + endEntityCert, _, _, err := createEndEntityCert(template, intermediateCert, intermediateKey, publicKeyAlgorithm, validity) + if err != nil { + return nil, nil, nil, err + } + + return endEntityCert, intermediateCert, rootCert, nil +} + +func createSelfSignedCA(template *x509.Certificate, publicKeyAlgorithm x509.PublicKeyAlgorithm, validity time.Duration) (*x509.Certificate, []byte, crypto.PrivateKey, error) { + if template.Subject.CommonName == "" { + return nil, nil, nil, fmt.Errorf("subject common name must be set") + } + + if template.MaxPathLen <= 0 { + return nil, nil, nil, fmt.Errorf("maxPathLen must be set and greater than 0") + } + + serialNumber, err := createSerialNumber() + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create certificate serial number: %w", err) + } + + template.SerialNumber = serialNumber + template.NotBefore = time.Now() + template.NotAfter = time.Now().Add(validity) + template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature + template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth} + template.BasicConstraintsValid = true + template.IsCA = true + template.MaxPathLenZero = false + + publicKey, privateKey, err := createKeyPair(publicKeyAlgorithm) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create key pair: %w", err) + } + + cert, certPEM, err := createCert(template, template, publicKey, privateKey) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create cert: %w", err) + } + + return cert, certPEM, privateKey, nil +} + +func createCA(template, issuerCert *x509.Certificate, issuerPrivateKey crypto.PrivateKey, publicKeyAlgorithm x509.PublicKeyAlgorithm, validity time.Duration) (*x509.Certificate, []byte, crypto.PrivateKey, error) { + if template.Subject.CommonName == "" { + return nil, nil, nil, fmt.Errorf("subject common name must be set") + } + + serialNumber, err := createSerialNumber() + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create certificate serial number: %w", err) + } + + var maxPathLenZero bool + if template.MaxPathLen > 0 { + maxPathLenZero = false + } else { + maxPathLenZero = true + } + + template.SerialNumber = serialNumber + template.NotBefore = time.Now() + template.NotAfter = time.Now().Add(validity) + template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature + template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth} + template.BasicConstraintsValid = true + template.IsCA = true + template.MaxPathLenZero = maxPathLenZero + + publicKey, privateKey, err := createKeyPair(publicKeyAlgorithm) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create key pair: %w", err) + } + + cert, certPEM, err := createCert(template, issuerCert, publicKey, issuerPrivateKey) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create cert: %w", err) + } + + return cert, certPEM, privateKey, nil +} + +func createEndEntityCert(template, issuerCert *x509.Certificate, issuerPrivateKey crypto.PrivateKey, publicKeyAlgorithm x509.PublicKeyAlgorithm, validity time.Duration) (*x509.Certificate, []byte, crypto.PrivateKey, error) { + if template.Subject.CommonName == "" { + return nil, nil, nil, fmt.Errorf("subject common name must be set") + } + + serialNumber, err := createSerialNumber() + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create certificate serial number: %w", err) + } + + template.SerialNumber = serialNumber + template.NotBefore = time.Now() + template.NotAfter = time.Now().Add(validity) + template.KeyUsage = x509.KeyUsageDigitalSignature + template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth} + template.IsCA = false + template.MaxPathLenZero = true + + publicKey, privateKey, err := createKeyPair(publicKeyAlgorithm) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create key pair: %w", err) + } + + cert, certPEM, err := createCert(template, issuerCert, publicKey, issuerPrivateKey) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create cert: %w", err) + } + + return cert, certPEM, privateKey, nil +} + +func createKeyPair(publicKeyAlgorithm x509.PublicKeyAlgorithm) (crypto.PublicKey, crypto.PrivateKey, error) { + var publicKey crypto.PublicKey + var privateKey crypto.PrivateKey + + switch publicKeyAlgorithm { + + case x509.RSA: + rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate rsa private key: %w", err) + } + publicKey = &rsaPrivateKey.PublicKey + privateKey = rsaPrivateKey + + case x509.Ed25519: + ed25519PublicKey, ed25519PrivateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate ed25519 key pair: %w", err) + } + publicKey = ed25519PublicKey + privateKey = ed25519PrivateKey + + default: + return nil, nil, fmt.Errorf("unsupported public key algorithm") + } + + return publicKey, privateKey, nil +} + +func createCert(template, issuer *x509.Certificate, subjectPublicKey crypto.PublicKey, issuerPrivateKey crypto.PrivateKey) (*x509.Certificate, []byte, error) { + certBytes, err := x509.CreateCertificate(rand.Reader, template, issuer, subjectPublicKey, issuerPrivateKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to create certificate: %w", err) + } + + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + b := pem.Block{Type: "CERTIFICATE", Bytes: certBytes} + certPEM := pem.EncodeToMemory(&b) + + return cert, certPEM, err +} + +func createSerialNumber() (*big.Int, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 256) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %w", err) + } + return serialNumber, nil +} diff --git a/pkg/policy/errors.go b/pkg/policy/errors.go index 35604300..b1e57c8f 100644 --- a/pkg/policy/errors.go +++ b/pkg/policy/errors.go @@ -91,3 +91,11 @@ type ErrPolicyDenied struct { func (e ErrPolicyDenied) Error() string { return fmt.Sprintf("policy was denied due to:\n%v", strings.Join(e.Reasons, "\n -")) } + +type ErrConstraintCheckFailed struct { + errs []error +} + +func (e ErrConstraintCheckFailed) Error() string { + return fmt.Sprintf("cert failed constraints check: %+q", e.errs) +} diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 91b2fb3e..7e18585e 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -186,7 +186,6 @@ func (p Policy) checkFunctionaries(verifiedStatements []VerifiedStatement) (map[ continue } - outerLoop: for _, functionary := range step.Functionaries { if functionary.PublicKeyID != "" && functionary.PublicKeyID == verifierID { collectionsByStep[step.Name] = append(collectionsByStep[collection.Name], collection) @@ -204,19 +203,14 @@ func (p Policy) checkFunctionaries(verifiedStatements []VerifiedStatement) (map[ continue } - for _, rootID := range functionary.CertConstraint.Roots { - bundle, ok := trustBundles[rootID] - if !ok { - log.Debugf("(policy) skipping verifier: could not get trust bundle for step %v and root ID %v", step, rootID) - continue - } - - if err := x509Verifier.BelongsToRoot(bundle.Root); err == nil { - collectionsByStep[step.Name] = append(collectionsByStep[step.Name], collection) - break outerLoop - } + if err := functionary.CertConstraint.Check(x509Verifier, trustBundles); err != nil { + log.Debugf("(policy) skipping verifier: verifier with ID %v doesn't meet certificate constraint: %w", verifierID, err) + continue } + + collectionsByStep[step.Name] = append(collectionsByStep[step.Name], collection) } + } } diff --git a/pkg/policy/step.go b/pkg/policy/step.go index 20ae527d..4bf62f2a 100644 --- a/pkg/policy/step.go +++ b/pkg/policy/step.go @@ -44,10 +44,6 @@ type RegoPolicy struct { Name string `json:"name"` } -type CertConstraint struct { - Roots []string `json:"roots"` -} - // StepResult contains information about the verified collections for each step. // Passed contains the collections that passed any rego policies and all expected attestations exist. // Rejected contains the rejected collections and the error that caused them to be rejected.