Skip to content

Commit

Permalink
Add allowed_uri_sans_template
Browse files Browse the repository at this point in the history
Enables identity templating for the allowed_uri_sans field in PKI cert roles.

Implemented as suggested in #8509
  • Loading branch information
pbohman committed Oct 15, 2021
1 parent 41d9ab2 commit 9ee3d83
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 19 deletions.
122 changes: 122 additions & 0 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2917,6 +2917,128 @@ func TestBackend_URI_SANs(t *testing.T) {
}
}

func TestBackend_AllowedURISANsTemplate(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
},
LogicalBackends: map[string]logical.Factory{
"pki": Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client

// Write test policy for userpass auth method.
err := client.Sys().PutPolicy("test", `
path "pki/*" {
capabilities = ["update"]
}`)
if err != nil {
t.Fatal(err)
}

// Enable userpass auth method.
if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil {
t.Fatal(err)
}

// Configure test role for userpass.
if _, err := client.Logical().Write("auth/userpass/users/userpassname", map[string]interface{}{
"password": "test",
"policies": "test",
}); err != nil {
t.Fatal(err)
}

// Login userpass for test role and keep client token.
secret, err := client.Logical().Write("auth/userpass/login/userpassname", map[string]interface{}{
"password": "test",
})
if err != nil || secret == nil {
t.Fatal(err)
}
userpassToken := secret.Auth.ClientToken

// Get auth accessor for identity template.
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
userpassAccessor := auths["userpass/"].Accessor

// Mount PKI.
err = client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "60h",
},
})
if err != nil {
t.Fatal(err)
}

// Generate internal CA.
_, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
})
if err != nil {
t.Fatal(err)
}

// Write role PKI.
_, err = client.Logical().Write("pki/roles/test", map[string]interface{}{
"allowed_uri_sans": []string{"spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}",
"spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}/*", "spiffe://domain/foo"},
"allowed_uri_sans_template": true,
"require_cn": false,
})
if err != nil {
t.Fatal(err)
}

// Issue certificate with identity templating
client.SetToken(userpassToken)
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname, spiffe://domain/foo"})
if err != nil {
t.Fatal(err)
}

// Issue certificate with identity templating and glob
client.SetToken(userpassToken)
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname/bar"})
if err != nil {
t.Fatal(err)
}

// Issue certificate with non-matching identity template parameter
client.SetToken(userpassToken)
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/unknownuser"})
if err == nil {
t.Fatal(err)
}

// Set allowed_uri_sans_template to false.
_, err = client.Logical().Write("pki/roles/test", map[string]interface{}{
"allowed_uri_sans_template": false,
})
if err != nil {
t.Fatal(err)
}

// Issue certificate with userpassToken.
_, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/users/userpassname"})
if err == nil {
t.Fatal("expected error")
}
}

func TestBackend_AllowedDomainsTemplate(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
Expand Down
43 changes: 25 additions & 18 deletions builtin/logical/pki/cert_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,29 @@ func fetchCertBySerial(ctx context.Context, req *logical.Request, prefix, serial
return certEntry, nil
}

// Given a URI SAN, verify that it is allowed.
func validateURISAN(b *backend, data *inputBundle, uri string) bool {
valid := false
for _, allowed := range data.role.AllowedURISANs {
if data.role.AllowedURISANsTemplate {
isTemplate, _ := framework.ValidateIdentityTemplate(allowed)
if isTemplate && data.req.EntityID != "" {
tmpAllowed, err := framework.PopulateIdentityTemplate(allowed, data.req.EntityID, b.System())
if err != nil {
continue
}
allowed = tmpAllowed
}
}
validURI := glob.Glob(allowed, uri)
if validURI {
valid = true
break
}
}
return valid
}

// Given a set of requested names for a certificate, verifies that all of them
// match the various toggles set in the role for controlling issuance.
// If one does not pass, it is returned in the string argument.
Expand Down Expand Up @@ -956,15 +979,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn

// validate uri sans
for _, uri := range csr.URIs {
valid := false
for _, allowed := range data.role.AllowedURISANs {
validURI := glob.Glob(allowed, uri.String())
if validURI {
valid = true
break
}
}

valid := validateURISAN(b, data, uri.String())
if !valid {
return nil, errutil.UserError{
Err: fmt.Sprintf(
Expand All @@ -986,15 +1001,7 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn
}

for _, uri := range uriAlt {
valid := false
for _, allowed := range data.role.AllowedURISANs {
validURI := glob.Glob(allowed, uri)
if validURI {
valid = true
break
}
}

valid := validateURISAN(b, data, uri)
if !valid {
return nil, errutil.UserError{
Err: fmt.Sprintf(
Expand Down
12 changes: 11 additions & 1 deletion builtin/logical/pki/path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ Any valid URI is accepted, these values support globbing.`,
},
},

"allowed_other_sans": {
"allowed_uri_sans_template": &framework.FieldSchema{
Type: framework.TypeBool,
Description: `If set, Allowed URI SANs can be specified using identity template policies.
Non-templated URI SANs are also permitted.`,
Default: false,
},

"allowed_other_sans": &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `If set, an array of allowed other names to put in SANs. These values support globbing and must be in the format <oid>;<type>:<value>. Currently only "utf8" is a valid type. All values, including globbing values, must use this syntax, with the exception being a single "*" which allows any OID and any value (but type must still be utf8).`,
DisplayAttrs: &framework.DisplayAttributes{
Expand Down Expand Up @@ -558,6 +565,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data
AllowSubdomains: data.Get("allow_subdomains").(bool),
AllowGlobDomains: data.Get("allow_glob_domains").(bool),
AllowAnyName: data.Get("allow_any_name").(bool),
AllowedURISANsTemplate: data.Get("allowed_uri_sans_template").(bool),
EnforceHostnames: data.Get("enforce_hostnames").(bool),
AllowIPSANs: data.Get("allow_ip_sans").(bool),
AllowedURISANs: data.Get("allowed_uri_sans").([]string),
Expand Down Expand Up @@ -783,6 +791,7 @@ type roleEntry struct {
AllowedOtherSANs []string `json:"allowed_other_sans" mapstructure:"allowed_other_sans"`
AllowedSerialNumbers []string `json:"allowed_serial_numbers" mapstructure:"allowed_serial_numbers"`
AllowedURISANs []string `json:"allowed_uri_sans" mapstructure:"allowed_uri_sans"`
AllowedURISANsTemplate bool `json:"allowed_uri_sans_template"`
PolicyIdentifiers []string `json:"policy_identifiers" mapstructure:"policy_identifiers"`
ExtKeyUsageOIDs []string `json:"ext_key_usage_oids" mapstructure:"ext_key_usage_oids"`
BasicConstraintsValidForNonCA bool `json:"basic_constraints_valid_for_non_ca" mapstructure:"basic_constraints_valid_for_non_ca"`
Expand All @@ -804,6 +813,7 @@ func (r *roleEntry) ToResponseData() map[string]interface{} {
"allow_subdomains": r.AllowSubdomains,
"allow_glob_domains": r.AllowGlobDomains,
"allow_any_name": r.AllowAnyName,
"allowed_uri_sans_template": r.AllowedURISANsTemplate,
"enforce_hostnames": r.EnforceHostnames,
"allow_ip_sans": r.AllowIPSANs,
"server_flag": r.ServerFlag,
Expand Down
6 changes: 6 additions & 0 deletions builtin/logical/pki/path_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@ func TestPki_RoleNoStore(t *testing.T) {
t.Fatalf("allowed_domains_template should not be set by default")
}

// By default, allowed_uri_sans_template should be `false`
allowedURISANsTemplate := resp.Data["allowed_uri_sans_template"].(bool)
if allowedURISANsTemplate {
t.Fatalf("allowed_uri_sans_template should not be set by default")
}

// Make sure that setting no_store to `true` works properly
roleReq.Operation = logical.UpdateOperation
roleReq.Path = "roles/testrole_nostore"
Expand Down

0 comments on commit 9ee3d83

Please sign in to comment.