diff --git a/builtin/credential/cert/backend_test.go b/builtin/credential/cert/backend_test.go index 47d7ae05d544..4a0f8b404757 100644 --- a/builtin/credential/cert/backend_test.go +++ b/builtin/credential/cert/backend_test.go @@ -14,6 +14,7 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" + "github.com/hashicorp/go-sockaddr" "io" "io/ioutil" "math/big" @@ -28,8 +29,6 @@ import ( "time" "github.com/go-test/deep" - "github.com/hashicorp/go-sockaddr" - "golang.org/x/net/http2" cleanhttp "github.com/hashicorp/go-cleanhttp" @@ -1306,6 +1305,12 @@ 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{names: "example.com", ext: "hex:2.5.29.17:*87047F000002*"}, false), + testAccStepLoginInvalid(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "hex:2.5.29.17:*87047F000001*"}, false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.5.29.17:"}, false), + testAccStepLogin(t, connState), testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: false}, 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"}, false), diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index d59c5b4a9195..8be7dfdf5249 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -10,6 +10,7 @@ import ( "crypto/x509" "encoding/asn1" "encoding/base64" + "encoding/hex" "encoding/pem" "errors" "fmt" @@ -507,18 +508,43 @@ func (b *backend) matchesCertificateExtensions(clientCert *x509.Certificate, con // 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. clientExtMap := make(map[string]string, len(clientCert.Extensions)) + hexExtMap := make(map[string]string, len(clientCert.Extensions)) + for _, ext := range clientCert.Extensions { var parsedValue string - asn1.Unmarshal(ext.Value, &parsedValue) - clientExtMap[ext.Id.String()] = parsedValue + _, err := asn1.Unmarshal(ext.Value, &parsedValue) + if err != nil { + clientExtMap[ext.Id.String()] = "" + } else { + clientExtMap[ext.Id.String()] = parsedValue + } + + hexExtMap[ext.Id.String()] = hex.EncodeToString(ext.Value) } - // If any of the required extensions don'log match the constraint fails + + // If any of the required extensions don't match the constraint fails for _, requiredExt := range config.Entry.RequiredExtensions { reqExt := strings.SplitN(requiredExt, ":", 2) - clientExtValue, clientExtValueOk := clientExtMap[reqExt[0]] - if !clientExtValueOk || !glob.Glob(reqExt[1], clientExtValue) { + if len(reqExt) != 2 { return false } + + if reqExt[0] == "hex" { + reqHexExt := strings.SplitN(reqExt[1], ":", 2) + if len(reqHexExt) != 2 { + return false + } + + clientExtValue, clientExtValueOk := hexExtMap[reqHexExt[0]] + if !clientExtValueOk || !glob.Glob(strings.ToLower(reqHexExt[1]), clientExtValue) { + return false + } + } else { + clientExtValue, clientExtValueOk := clientExtMap[reqExt[0]] + if !clientExtValueOk || !glob.Glob(reqExt[1], clientExtValue) { + return false + } + } } return true } diff --git a/changelog/21830.txt b/changelog/21830.txt new file mode 100644 index 000000000000..6e1972d447be --- /dev/null +++ b/changelog/21830.txt @@ -0,0 +1,3 @@ +```release-note:improvement +auth/cert: Adds support for requiring hexadecimal-encoded non-string certificate extension values +``` \ No newline at end of file diff --git a/website/content/api-docs/auth/cert.mdx b/website/content/api-docs/auth/cert.mdx index 0604b161edb8..21d006b5a5b4 100644 --- a/website/content/api-docs/auth/cert.mdx +++ b/website/content/api-docs/auth/cert.mdx @@ -59,8 +59,9 @@ Sets a CA cert and associated parameters in a role name. - `required_extensions` `(string: "" or array: [])` - Require specific Custom Extension OIDs to exist and match the pattern. Value is a comma separated string or array of `oid:value`. Expects the extension value to be some type - of ASN1 encoded string. All conditions _must_ be met. Supports globbing on - `value`. + of ASN1 encoded string. All conditions _must_ be met. To match on the hex-encoded + value of the extension, including non-string extensions, use the format + `hex::`.Supports globbing on `value`. - `allowed_metadata_extensions` `(array:[])` - A comma separated string or array of oid extensions. Upon successful authentication, these extensions will be added as metadata if they are present in the certificate. The