Support for CPS URLs in Custom Policy Identifiers. (#15751)

* Support for CPS URLs in Custom Policy Identifiers.

* go fmt

* Add Changelog

* Fix panic in test-cases.

* Update builtin/logical/pki/path_roles.go

Fix intial nil identifiers.

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>

* Make valid policy OID so don't break ASN parse in test.

* Add test cases.

* go fmt.

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
This commit is contained in:
Kit Haines 2022-06-03 14:50:46 -04:00 committed by GitHub
parent f293d112e4
commit 4f532ecc4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 332 additions and 16 deletions

View File

@ -3,6 +3,7 @@ package pki
import ( import (
"context" "context"
"crypto/x509" "crypto/x509"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -385,7 +386,9 @@ for "generate_lease".`,
"policy_identifiers": { "policy_identifiers": {
Type: framework.TypeCommaStringSlice, Type: framework.TypeCommaStringSlice,
Description: `A comma-separated string or list of policy OIDs.`, Description: `A comma-separated string or list of policy OIDs, or a JSON list of qualified policy
information, which must include an oid, and may include a notice and/or cps url, using the form
[{"oid"="1.3.6.1.4.1.7.8","notice"="I am a user Notice"}, {"oid"="1.3.6.1.4.1.44947.1.2.4 ","cps"="https://example.com"}].`,
}, },
"basic_constraints_valid_for_non_ca": { "basic_constraints_valid_for_non_ca": {
@ -658,7 +661,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data
NoStore: data.Get("no_store").(bool), NoStore: data.Get("no_store").(bool),
RequireCN: data.Get("require_cn").(bool), RequireCN: data.Get("require_cn").(bool),
AllowedSerialNumbers: data.Get("allowed_serial_numbers").([]string), AllowedSerialNumbers: data.Get("allowed_serial_numbers").([]string),
PolicyIdentifiers: data.Get("policy_identifiers").([]string), PolicyIdentifiers: getPolicyIdentifier(data, nil),
BasicConstraintsValidForNonCA: data.Get("basic_constraints_valid_for_non_ca").(bool), BasicConstraintsValidForNonCA: data.Get("basic_constraints_valid_for_non_ca").(bool),
NotBeforeDuration: time.Duration(data.Get("not_before_duration").(int)) * time.Second, NotBeforeDuration: time.Duration(data.Get("not_before_duration").(int)) * time.Second,
NotAfter: data.Get("not_after").(string), NotAfter: data.Get("not_after").(string),
@ -747,11 +750,9 @@ func validateRole(b *backend, entry *roleEntry, ctx context.Context, s logical.S
} }
if len(entry.PolicyIdentifiers) > 0 { if len(entry.PolicyIdentifiers) > 0 {
for _, oidstr := range entry.PolicyIdentifiers { _, err := certutil.CreatePolicyInformationExtensionFromStorageStrings(entry.PolicyIdentifiers)
_, err := certutil.StringToOid(oidstr)
if err != nil { if err != nil {
return logical.ErrorResponse(fmt.Sprintf("%q could not be parsed as a valid oid for a policy identifier", oidstr)), nil return nil, err
}
} }
} }
@ -848,7 +849,7 @@ func (b *backend) pathRolePatch(ctx context.Context, req *logical.Request, data
NoStore: getWithExplicitDefault(data, "no_store", oldEntry.NoStore).(bool), NoStore: getWithExplicitDefault(data, "no_store", oldEntry.NoStore).(bool),
RequireCN: getWithExplicitDefault(data, "require_cn", oldEntry.RequireCN).(bool), RequireCN: getWithExplicitDefault(data, "require_cn", oldEntry.RequireCN).(bool),
AllowedSerialNumbers: getWithExplicitDefault(data, "allowed_serial_numbers", oldEntry.AllowedSerialNumbers).([]string), AllowedSerialNumbers: getWithExplicitDefault(data, "allowed_serial_numbers", oldEntry.AllowedSerialNumbers).([]string),
PolicyIdentifiers: getWithExplicitDefault(data, "policy_identifiers", oldEntry.PolicyIdentifiers).([]string), PolicyIdentifiers: getPolicyIdentifier(data, &oldEntry.PolicyIdentifiers),
BasicConstraintsValidForNonCA: getWithExplicitDefault(data, "basic_constraints_valid_for_non_ca", oldEntry.BasicConstraintsValidForNonCA).(bool), BasicConstraintsValidForNonCA: getWithExplicitDefault(data, "basic_constraints_valid_for_non_ca", oldEntry.BasicConstraintsValidForNonCA).(bool),
NotBeforeDuration: getTimeWithExplicitDefault(data, "not_before_duration", oldEntry.NotBeforeDuration), NotBeforeDuration: getTimeWithExplicitDefault(data, "not_before_duration", oldEntry.NotBeforeDuration),
NotAfter: getWithExplicitDefault(data, "not_after", oldEntry.NotAfter).(string), NotAfter: getWithExplicitDefault(data, "not_after", oldEntry.NotAfter).(string),
@ -1116,3 +1117,45 @@ const pathListRolesHelpDesc = `Roles will be listed by the role name.`
const pathRoleHelpSyn = `Manage the roles that can be created with this backend.` const pathRoleHelpSyn = `Manage the roles that can be created with this backend.`
const pathRoleHelpDesc = `This path lets you manage the roles that can be created with this backend.` const pathRoleHelpDesc = `This path lets you manage the roles that can be created with this backend.`
const policyIdentifiersParam = "policy_identifiers"
func getPolicyIdentifier(data *framework.FieldData, defaultIdentifiers *[]string) []string {
policyIdentifierEntry, ok := data.GetOk(policyIdentifiersParam)
if !ok {
// No Entry for policy_identifiers
if defaultIdentifiers != nil {
return *defaultIdentifiers
}
return data.Get(policyIdentifiersParam).([]string)
}
// Could Be A JSON Entry
policyIdentifierJsonEntry := data.Raw[policyIdentifiersParam]
policyIdentifierJsonString, ok := policyIdentifierJsonEntry.(string)
if ok {
policyIdentifiers, err := parsePolicyIdentifiersFromJson(policyIdentifierJsonString)
if err == nil {
return policyIdentifiers
}
}
// Else could Just Be A List of OIDs
return policyIdentifierEntry.([]string)
}
func parsePolicyIdentifiersFromJson(policyIdentifiers string) ([]string, error) {
var entries []certutil.PolicyIdentifierWithQualifierEntry
var policyIdentifierList []string
err := json.Unmarshal([]byte(policyIdentifiers), &entries)
if err != nil {
return policyIdentifierList, err
}
policyIdentifierList = make([]string, 0, len(entries))
for _, entry := range entries {
policyString, err := json.Marshal(entry)
if err != nil {
return policyIdentifierList, err
}
policyIdentifierList = append(policyIdentifierList, string(policyString))
}
return policyIdentifierList, nil
}

View File

@ -2,13 +2,19 @@ package pki
import ( import (
"context" "context"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt" "fmt"
"testing" "testing"
"time" "time"
"github.com/go-errors/errors"
"github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -928,8 +934,8 @@ func TestPki_RolePatch(t *testing.T) {
}, },
{ {
Field: "policy_identifiers", Field: "policy_identifiers",
Before: []string{"1.2.3.4.5"}, Before: []string{"1.3.6.1.4.1.1.1"},
Patched: []string{"5.4.3.2.1"}, Patched: []string{"1.3.6.1.4.1.1.2"},
}, },
{ {
Field: "basic_constraints_valid_for_non_ca", Field: "basic_constraints_valid_for_non_ca",
@ -1019,3 +1025,141 @@ func TestPki_RolePatch(t *testing.T) {
} }
} }
} }
func TestPKI_RolePolicyInformation_Flat(t *testing.T) {
type TestCase struct {
Input interface{}
ASN interface{}
OidList []string
}
expectedSimpleAsnExtension := "MBYwCQYHKwYBBAEBATAJBgcrBgEEAQEC"
expectedSimpleOidList := append(*new([]string), "1.3.6.1.4.1.1.1", "1.3.6.1.4.1.1.2")
testCases := []TestCase{
{
Input: "1.3.6.1.4.1.1.1,1.3.6.1.4.1.1.2",
ASN: expectedSimpleAsnExtension,
OidList: expectedSimpleOidList,
},
{
Input: "[{\"oid\":\"1.3.6.1.4.1.1.1\"},{\"oid\":\"1.3.6.1.4.1.1.2\"}]",
ASN: expectedSimpleAsnExtension,
OidList: expectedSimpleOidList,
},
{
Input: "[{\"oid\":\"1.3.6.1.4.1.7.8\",\"notice\":\"I am a user Notice\"},{\"oid\":\"1.3.6.1.44947.1.2.4\",\"cps\":\"https://example.com\"}]",
ASN: "MF8wLQYHKwYBBAEHCDAiMCAGCCsGAQUFBwICMBQMEkkgYW0gYSB1c2VyIE5vdGljZTAuBgkrBgGC3xMBAgQwITAfBggrBgEFBQcCARYTaHR0cHM6Ly9leGFtcGxlLmNvbQ==",
OidList: append(*new([]string), "1.3.6.1.4.1.7.8", "1.3.6.1.44947.1.2.4"),
},
}
b, storage := createBackendWithStorage(t)
caData := map[string]interface{}{
"common_name": "myvault.com",
"ttl": "5h",
"ip_sans": "127.0.0.1",
}
caReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "root/generate/internal",
Storage: storage,
Data: caData,
}
caResp, err := b.HandleRequest(context.Background(), caReq)
if err != nil || (caResp != nil && caResp.IsError()) {
t.Fatalf("bad: err: %v resp: %#v", err, caResp)
}
for index, testCase := range testCases {
var roleResp *logical.Response
var issueResp *logical.Response
var err error
// Create/update the role
roleData := map[string]interface{}{}
roleData[policyIdentifiersParam] = testCase.Input
roleReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "roles/testrole",
Storage: storage,
Data: roleData,
}
roleResp, err = b.HandleRequest(context.Background(), roleReq)
if err != nil || (roleResp != nil && roleResp.IsError()) {
t.Fatalf("bad [%d], setting policy identifier %v err: %v resp: %#v", index, testCase.Input, err, roleResp)
}
// Issue Using this role
issueData := map[string]interface{}{}
issueData["common_name"] = "localhost"
issueData["ttl"] = "2s"
issueReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "issue/testrole",
Storage: storage,
Data: issueData,
}
issueResp, err = b.HandleRequest(context.Background(), issueReq)
if err != nil || (issueResp != nil && issueResp.IsError()) {
t.Fatalf("bad [%d], setting policy identifier %v err: %v resp: %#v", index, testCase.Input, err, issueResp)
}
// Validate the OIDs
policyIdentifiers, err := getPolicyIdentifiersOffCertificate(*issueResp)
if err != nil {
t.Fatalf("bad [%d], getting policy identifier from %v err: %v resp: %#v", index, testCase.Input, err, issueResp)
}
if len(policyIdentifiers) != len(testCase.OidList) {
t.Fatalf("bad [%d], wrong certificate policy identifier from %v len expected: %d got %d", index, testCase.Input, len(testCase.OidList), len(policyIdentifiers))
}
for i, identifier := range policyIdentifiers {
if identifier != testCase.OidList[i] {
t.Fatalf("bad [%d], wrong certificate policy identifier from %v expected: %v got %v", index, testCase.Input, testCase.OidList[i], policyIdentifiers[i])
}
}
// Validate the ASN
certificateAsn, err := getPolicyInformationExtensionOffCertificate(*issueResp)
if err != nil {
t.Fatalf("bad [%d], getting extension from %v err: %v resp: %#v", index, testCase.Input, err, issueResp)
}
certificateB64 := make([]byte, len(certificateAsn)*2)
base64.StdEncoding.Encode(certificateB64, certificateAsn)
certificateString := string(certificateB64[:])
assert.Contains(t, certificateString, testCase.ASN)
}
}
func getPolicyIdentifiersOffCertificate(resp logical.Response) ([]string, error) {
stringCertificate := resp.Data["certificate"].(string)
block, _ := pem.Decode([]byte(stringCertificate))
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
policyIdentifierStrings := make([]string, len(certificate.PolicyIdentifiers))
for index, asnOid := range certificate.PolicyIdentifiers {
policyIdentifierStrings[index] = asnOid.String()
}
return policyIdentifierStrings, nil
}
func getPolicyInformationExtensionOffCertificate(resp logical.Response) ([]byte, error) {
stringCertificate := resp.Data["certificate"].(string)
block, _ := pem.Decode([]byte(stringCertificate))
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
for _, extension := range certificate.Extensions {
if extension.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 32}) {
return extension.Value, nil
}
}
return *new([]byte), errors.New("No Policy Information Extension Found")
}

3
changelog/15751.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
secrets/pki: Add support for CPS URLs and User Notice to Policy Information
```

View File

@ -446,18 +446,30 @@ func ParsePublicKeyPEM(data []byte) (interface{}, error) {
return nil, errors.New("data does not contain any valid public keys") return nil, errors.New("data does not contain any valid public keys")
} }
// addPolicyIdentifiers adds certificate policies extension // AddPolicyIdentifiers adds certificate policies extension, based on CreationBundle
//
func AddPolicyIdentifiers(data *CreationBundle, certTemplate *x509.Certificate) { func AddPolicyIdentifiers(data *CreationBundle, certTemplate *x509.Certificate) {
for _, oidstr := range data.Params.PolicyIdentifiers { oidOnly := true
oid, err := StringToOid(oidstr) for _, oidStr := range data.Params.PolicyIdentifiers {
oid, err := StringToOid(oidStr)
if err == nil { if err == nil {
certTemplate.PolicyIdentifiers = append(certTemplate.PolicyIdentifiers, oid) certTemplate.PolicyIdentifiers = append(certTemplate.PolicyIdentifiers, oid)
} }
if err != nil {
oidOnly = false
}
}
if !oidOnly { // Because all policy information is held in the same extension, when we use an extra extension to
// add policy qualifier information, that overwrites any information in the PolicyIdentifiers field on the Cert
// Template, so we need to reparse all the policy identifiers here
extension, err := CreatePolicyInformationExtensionFromStorageStrings(data.Params.PolicyIdentifiers)
if err == nil {
// If this errors out, don't add it, rely on the OIDs parsed into PolicyIdentifiers above
certTemplate.ExtraExtensions = append(certTemplate.ExtraExtensions, *extension)
}
} }
} }
// addExtKeyUsageOids adds custom extended key usage OIDs to certificate // AddExtKeyUsageOids adds custom extended key usage OIDs to certificate
func AddExtKeyUsageOids(data *CreationBundle, certTemplate *x509.Certificate) { func AddExtKeyUsageOids(data *CreationBundle, certTemplate *x509.Certificate) {
for _, oidstr := range data.Params.ExtKeyUsageOIDs { for _, oidstr := range data.Params.ExtKeyUsageOIDs {
oid, err := StringToOid(oidstr) oid, err := StringToOid(oidstr)

View File

@ -17,7 +17,10 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"net" "net"
@ -894,3 +897,114 @@ func (p *KeyBundle) ToPrivateKeyPemString() (string, error) {
return "", errutil.InternalError{Err: "No Private Key Bytes to Wrap"} return "", errutil.InternalError{Err: "No Private Key Bytes to Wrap"}
} }
// PolicyIdentifierWithQualifierEntry Structure for Internal Storage
type PolicyIdentifierWithQualifierEntry struct {
PolicyIdentifierOid string `json:"oid",mapstructure:"oid"`
CPS string `json:"cps,omitempty",mapstructure:"cps"`
Notice string `json:"notice,omitempty",mapstructure:"notice"`
}
// GetPolicyIdentifierFromString parses out the internal structure of a Policy Identifier
func GetPolicyIdentifierFromString(policyIdentifier string) (*PolicyIdentifierWithQualifierEntry, error) {
if policyIdentifier == "" {
return nil, nil
}
entry := &PolicyIdentifierWithQualifierEntry{}
// Either a OID, or a JSON Entry: First check OID:
_, err := StringToOid(policyIdentifier)
if err == nil {
entry.PolicyIdentifierOid = policyIdentifier
return entry, nil
}
// Now Check If JSON Entry
jsonErr := json.Unmarshal([]byte(policyIdentifier), &entry)
if jsonErr != nil { // Neither, if we got here
return entry, errors.New(fmt.Sprintf("Policy Identifier %q is neither a valid OID: %s, Nor JSON Policy Identifier: %s", policyIdentifier, err.Error(), jsonErr.Error()))
}
return entry, nil
}
// Policy Identifier with Qualifier Structure for ASN Marshalling:
var policyInformationOid = asn1.ObjectIdentifier{2, 5, 29, 32}
type policyInformation struct {
PolicyIdentifier asn1.ObjectIdentifier
Qualifiers []interface{} `asn1:"tag:optional,omitempty"`
}
var cpsPolicyQualifierID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 1}
type cpsUrlPolicyQualifier struct {
PolicyQualifierID asn1.ObjectIdentifier
Qualifier string `asn1:"tag:optional,ia5"`
}
var userNoticePolicyQualifierID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 2}
type userNoticePolicyQualifier struct {
PolicyQualifierID asn1.ObjectIdentifier
Qualifier userNotice
}
type userNotice struct {
ExplicitText string `asn1:"tag:optional,utf8"`
}
func createPolicyIdentifierWithQualifier(entry PolicyIdentifierWithQualifierEntry) (*policyInformation, error) {
// Each Policy is Identified by a Unique ID, as designated here:
policyOid, err := StringToOid(entry.PolicyIdentifierOid)
if err != nil {
return nil, err
}
pi := policyInformation{
PolicyIdentifier: policyOid,
}
if entry.CPS != "" {
qualifier := cpsUrlPolicyQualifier{
PolicyQualifierID: cpsPolicyQualifierID,
Qualifier: entry.CPS,
}
pi.Qualifiers = append(pi.Qualifiers, qualifier)
}
if entry.Notice != "" {
qualifier := userNoticePolicyQualifier{
PolicyQualifierID: userNoticePolicyQualifierID,
Qualifier: userNotice{
ExplicitText: entry.Notice,
},
}
pi.Qualifiers = append(pi.Qualifiers, qualifier)
}
return &pi, nil
}
// CreatePolicyInformationExtensionFromStorageStrings parses the stored policyIdentifiers, which might be JSON Policy
// Identifier with Qualifier Entries or String OIDs, and returns an extension if everything parsed correctly, and an
// error if constructing
func CreatePolicyInformationExtensionFromStorageStrings(policyIdentifiers []string) (*pkix.Extension, error) {
var policyInformationList []policyInformation
for _, policyIdentifierStr := range policyIdentifiers {
policyIdentifierEntry, err := GetPolicyIdentifierFromString(policyIdentifierStr)
if err != nil {
return nil, err
}
if policyIdentifierEntry != nil { // Okay to skip empty entries if there is no error
policyInformationStruct, err := createPolicyIdentifierWithQualifier(*policyIdentifierEntry)
if err != nil {
return nil, err
}
policyInformationList = append(policyInformationList, *policyInformationStruct)
}
}
asn1Bytes, err := asn1.Marshal(policyInformationList)
if err != nil {
return nil, err
}
return &pkix.Extension{
Id: policyInformationOid,
Critical: false,
Value: asn1Bytes,
}, nil
}