auth/cert: Add certificate extensions as metadata (#13348)

* auth/cert: Add certificate extensions as metadata

Signed-off-by: Peter Verraedt <peter.verraedt@kuleuven.be>

* Add changelog for #13348

Signed-off-by: Peter Verraedt <peter.verraedt@kuleuven.be>
This commit is contained in:
Peter Verraedt 2022-01-03 22:38:16 +01:00 committed by GitHub
parent 28189e765b
commit 504a8efd01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 103 additions and 7 deletions

View File

@ -1107,6 +1107,10 @@ func TestBackend_ext_singleCert(t *testing.T) {
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}),
},
})
}
@ -1556,6 +1560,40 @@ func testAccStepLoginDefaultLease(t *testing.T, connState tls.ConnectionState) l
}
}
func testAccStepLoginWithMetadata(t *testing.T, connState tls.ConnectionState, certName string, metadata map[string]string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login",
Unauthenticated: true,
ConnState: &connState,
Check: func(resp *logical.Response) error {
// Check for fixed metadata too
metadata["cert_name"] = certName
metadata["common_name"] = connState.PeerCertificates[0].Subject.CommonName
metadata["serial_number"] = connState.PeerCertificates[0].SerialNumber.String()
metadata["subject_key_id"] = certutil.GetHexFormatted(connState.PeerCertificates[0].SubjectKeyId, ":")
metadata["authority_key_id"] = certutil.GetHexFormatted(connState.PeerCertificates[0].AuthorityKeyId, ":")
for key, expected := range metadata {
value, ok := resp.Auth.Metadata[key]
if !ok {
t.Fatalf("missing metadata key: %s", key)
}
if value != expected {
t.Fatalf("expected metadata key %s to equal %s, but got: %s", key, expected, value)
}
}
fn := logicaltest.TestCheckAuth([]string{"default", "foo"})
return fn(resp)
},
Data: map[string]interface{}{
"metadata": metadata,
},
}
}
func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
return testAccStepLoginWithNameInvalid(t, connState, "")
}
@ -1633,6 +1671,7 @@ type allowed struct {
uris string // allowed uris in SAN extension of the certificate
organizational_units string // allowed OUs in the certificate
ext string // required extensions in the certificate
metadata_ext string // allowed metadata extensions to add to identity alias
}
func testAccStepCert(
@ -1652,6 +1691,7 @@ func testAccStepCert(
"allowed_uri_sans": testData.uris,
"allowed_organizational_units": testData.organizational_units,
"required_extensions": testData.ext,
"allowed_metadata_extensions": testData.metadata_ext,
"lease": 1000,
},
Check: func(resp *logical.Response) error {

View File

@ -114,6 +114,14 @@ formatted as "oid:value". Expects the extension value to be some type of ASN1 en
All values much match. Supports globbing on "value".`,
},
"allowed_metadata_extensions": {
Type: framework.TypeCommaStringSlice,
Description: `A comma-separated string or array of oid extensions.
Upon successfull authentication, these extensions will be added as metadata if they are present
in the certificate. The metadata key will be the string consisting of the oid numbers
separated by a dash (-) instead of a dot (.) to allow usage in ACL templates.`,
},
"display_name": {
Type: framework.TypeString,
Description: `The display name to use for clients using this
@ -243,6 +251,7 @@ func (b *backend) pathCertRead(ctx context.Context, req *logical.Request, d *fra
"allowed_uri_sans": cert.AllowedURISANs,
"allowed_organizational_units": cert.AllowedOrganizationalUnits,
"required_extensions": cert.RequiredExtensions,
"allowed_metadata_extensions": cert.AllowedMetadataExtensions,
}
cert.PopulateTokenData(data)
@ -309,6 +318,9 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr
if requiredExtensionsRaw, ok := d.GetOk("required_extensions"); ok {
cert.RequiredExtensions = requiredExtensionsRaw.([]string)
}
if allowedMetadataExtensionsRaw, ok := d.GetOk("allowed_metadata_extensions"); ok {
cert.AllowedMetadataExtensions = allowedMetadataExtensionsRaw.([]string)
}
// Get tokenutil fields
if err := cert.ParseTokenFields(req, d); err != nil {
@ -424,6 +436,7 @@ type CertEntry struct {
AllowedURISANs []string
AllowedOrganizationalUnits []string
RequiredExtensions []string
AllowedMetadataExtensions []string
BoundCIDRs []*sockaddr.SockAddrMarshaler
}

View File

@ -89,19 +89,27 @@ func (b *backend) pathLogin(ctx context.Context, req *logical.Request, data *fra
skid := base64.StdEncoding.EncodeToString(clientCerts[0].SubjectKeyId)
akid := base64.StdEncoding.EncodeToString(clientCerts[0].AuthorityKeyId)
metadata := map[string]string{
"cert_name": matched.Entry.Name,
"common_name": clientCerts[0].Subject.CommonName,
"serial_number": clientCerts[0].SerialNumber.String(),
"subject_key_id": certutil.GetHexFormatted(clientCerts[0].SubjectKeyId, ":"),
"authority_key_id": certutil.GetHexFormatted(clientCerts[0].AuthorityKeyId, ":"),
}
// Add metadata from allowed_metadata_extensions when present,
// with sanitized oids (dash-separated instead of dot-separated) as keys.
for k, v := range b.certificateExtensionsMetadata(clientCerts[0], matched) {
metadata[k] = v
}
auth := &logical.Auth{
InternalData: map[string]interface{}{
"subject_key_id": skid,
"authority_key_id": akid,
},
DisplayName: matched.Entry.DisplayName,
Metadata: map[string]string{
"cert_name": matched.Entry.Name,
"common_name": clientCerts[0].Subject.CommonName,
"serial_number": clientCerts[0].SerialNumber.String(),
"subject_key_id": certutil.GetHexFormatted(clientCerts[0].SubjectKeyId, ":"),
"authority_key_id": certutil.GetHexFormatted(clientCerts[0].AuthorityKeyId, ":"),
},
Metadata: metadata,
Alias: &logical.Alias{
Name: clientCerts[0].Subject.CommonName,
},
@ -409,6 +417,38 @@ func (b *backend) matchesCertificateExtensions(clientCert *x509.Certificate, con
return true
}
// certificateExtensionsMetadata returns the metadata from configured
// metadata extensions
func (b *backend) certificateExtensionsMetadata(clientCert *x509.Certificate, config *ParsedCert) map[string]string {
// If no metadata extensions are configured, return an empty map
if len(config.Entry.AllowedMetadataExtensions) == 0 {
return map[string]string{}
}
// Build a map with the accepted oid strings as keys, and the metadata keys as values.
allowedOidMap := make(map[string]string, len(config.Entry.AllowedMetadataExtensions))
for _, oidString := range config.Entry.AllowedMetadataExtensions {
// Avoid dots in metadata keys and put dashes instead,
// to allow use policy templates.
allowedOidMap[oidString] = strings.ReplaceAll(oidString, ".", "-")
}
// Collect the metadata from accepted certificate extensions.
metadata := make(map[string]string, len(config.Entry.AllowedMetadataExtensions))
for _, ext := range clientCert.Extensions {
if metadataKey, ok := allowedOidMap[ext.Id.String()]; ok {
// x509 Writes Extensions in ASN1 with a bitstring tag, which results in the field
// including its ASN.1 type tag bytes. For the sake of simplicity, assume string type
// and drop the tag bytes. And get the number of bytes from the tag.
var parsedValue string
asn1.Unmarshal(ext.Value, &parsedValue)
metadata[metadataKey] = parsedValue
}
}
return metadata
}
// loadTrustedCerts is used to load all the trusted certificates from the backend
func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage, certName string) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert) {
pool = x509.NewCertPool()

3
changelog/13348.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
auth/cert: Add certificate extensions as metadata
```