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:
parent
28189e765b
commit
504a8efd01
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
auth/cert: Add certificate extensions as metadata
|
||||
```
|
Loading…
Reference in New Issue