-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use Custom Cert Extensions as Cert Auth Constraint #3634
Use Custom Cert Extensions as Cert Auth Constraint #3634
Conversation
This change allows the use of custom certificate extensions as a configurable constraint for certificate authentication. Opening the ability to use CAs like Puppet or Vault it self to inject more detailed information into the certificate as a means of more narrowly defining access to specific profiles.
} | ||
|
||
// isControlRune returns true for control charaters for trimming extension values | ||
func (b *backend) isControlRune(r rune) bool { return r <= 32 || r == 127 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't put the function definition inline -- it's not our style.
// Build Client Extensions Map | ||
clientExtMap := map[string]string{} | ||
for _, ext := range clientCert.Extensions { | ||
clientExtMap[ext.Id.String()] = strings.TrimLeftFunc(string(ext.Value[:]), b.isControlRune) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you leave a comment on the purpose of this line? Why just trimming the left side?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When testing the conditions with certificates with various custom extensions, I noticed that there were control characters prefixing the value of the extension. For example using fmt.Printf("%q",string(ext.Value[:])
yields:
2.1.1.1: "\f\x16A UTF8String Extension"
2.1.1.2: "\f\x10A UTF8 Extension"
2.1.1.3: "\x16\x10An IA5 Extension"
2.1.1.4: "\x1a\x13A Visible Extension"
This is the result of generating the certificate using the .cnf below
in the following manner:
openssl req -new -out rootcawext.csr -config rootcawext.cnf -keyout rootcawextkey.pem -utf8
openssl x509 -req -in rootcawext.csr -CA rootcacert.pem -CAkey rootcakey.pem -out rootcawextcert.pem -sha256 -extensions req_v3 -extfile rootcawext.cnf
I tried some of the other custom extension formats, and they also resulted in prefix values not immediately obvious from the cnf file nor could I find any specific documentation regarding the manner in which these were added (probably looking in the wrong place). I did not see any examples with suffixes being added to the value.
Trimming the left hand side removed the control characters when present, but did not solve the problem entirely as some types used more than just control characters in the prefix. I had a hard time testing with certificates generated by Puppet due to the way the tests utilized the certificates and the inability to force the Puppet CA to generate the expected values, but I can check again to see if a cert with custom extensions created by Puppet or Vault also produce the (encoding?) prefixes.
In the end, I was trying to avoid writing custom extension constraints with were always bounded by wildcards ie *myvalue*
.
In general, the values within the .Extensions
of the certificate are not as clean as I expected. eg another cert used in the tests.
2.5.29.37: "0\x14\x06\b+\x06\x01\x05\x05\a\x03\x01\x06\b+\x06\x01\x05\x05\a\x03\x02"
2.5.29.14: "\x04\x14\x9b\xef\x9e\x1e\x9c\x8cޞ\xf4\xf1\xb8\x19&\xe4X\x11\xd5\xf5\xa3\xe5"
2.5.29.35: "0\x16\x80\x14\x9dijO\xfe\x871\xecr\xba%=\xff\xb1 \x1e﨓\x9b"
1.3.6.1.5.5.7.1.1: "0-0+\x06\b+\x06\x01\x05\x05\a0\x02\x86\x1fhttp://127.0.0.1:8200/v1/pki/ca"
2.5.29.17: "0\x18\x82\x10cert.example.com\x87\x04\u007f\x00\x00\x01"
2.5.29.31: "0(0&\xa0$\xa0\"\x86 http://127.0.0.1:8200/v1/pki/crl"
That expectation could just be a result of being new to working with certificates programmatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When viewing with openssl such a generated cert also looks strange:
2.1.1.1:
..A UTF8String Extension
2.1.1.2:
..A UTF8 Extension
2.1.1.3:
..An IA5 Extension
2.1.1.4:
..A Visible Extension
How did you come upon that syntax for specifying custom OIDs? The man page (https://www.openssl.org/docs/man1.1.0/apps/config.html) suggests that it's simple key-value.
I would argue however that even if what you're doing is correct, because the control characters are a part of that custom OID, eliding them is actually incorrect behavior. Instead, you should do a direct comparison on the byte slices. This does prevent a problem for ingressing such data for comparison -- we could require all such input to be base64 encoded, which isn't nice for command-line users; we could try to heuristically detect whether it's base64-encoded, which is fragile; or we could assume string unless it's defined to be base64 in some way, such as with a b64:
prefix, and hope that that never shows up in the real world. Or, we can for the moment just assume that both the values in the cert and supplied will be UTF-8 strings and punt anything further down the road if people actually need it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The syntax in question comes from: https://www.openssl.org/docs/manmaster/man5/x509v3_config.html
After a bunch of reading on ASN.1, and the x509 code, it appears that Custom Extensions are prefixed by an implicit bitstring tag 03 LENGTH
. What follows is the tag for the type written in the config eg 12 LENGTH
. As such, it would make sense that we drop the first two bytes of the value and assume string. (see latest commit)
// If we match all of the expected extensions, the requirement is satisfied | ||
matchedExts := 0 | ||
for _, requiredExt := range config.Entry.RequiredExtensions { | ||
reqExt := strings.Split(requiredExt, ":") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the value can be a UTF8 string it's probably safer to use SplitN here to ensure you only split the actual OID.
for _, requiredExt := range config.Entry.RequiredExtensions { | ||
reqExt := strings.Split(requiredExt, ":") | ||
clientExtValue, clientExtValueExists := clientExtMap[reqExt[0]] | ||
if glob.Glob(reqExt[1], clientExtValue) && clientExtValueExists { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize doing it this way helps keep timing constant but it's a bit more complicated that it needs to be, as I don't think timing is really a concern here (after all the certificate is public and the Vault role definition is authn/authz'd). You could simply shortcut and return false
if !clientExtValueExists
or the glob fails.
Use SplitN over Split for custom extension configuration to avoid missing values containing :. Simply return false on the first required custom extension faliure. Format oneline function correctly.
} | ||
|
||
// Test a self-signed client with custom extensions (root CA) that is trusted | ||
func TestBackend_extensions_signleCert(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: signle/single
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look good but I still don't like the eliding. I'd be interested in knowing where you found that syntax for the control file.
…st two bytes and assume the value is a string.
The syntax in question comes from: https://www.openssl.org/docs/manmaster/man5/x509v3_config.html The other syntax appears either out of date or I don't understand how it's supposed to work. I'm leaning towards the syntax has a different purpose in informing Openssl of custom extensions vs adding them to a CSR. It started to make sense when I read about puppet's use of custom extensions more closely here: https://puppet.com/docs/puppet/5.3/ssl_attributes_extensions.html#manually-checking-for-extensions-in-csrs-and-certificates
When I don't include the Based on docs for ASN.1 and some of the Openssl source, and the golang x509 code, it appears that Custom Extensions are prefixed by a tag |
The Puppet doc shows the Possibly this is because Puppet's CA strips these values off, but it does seem like they're unexpected in an issued certificate context. |
I've tried to dig down into the (puppet code)[https://github.com/puppetlabs/puppet] where it handles this. The example call they give for the CSR comes from When asking openssl to parse a certificate generated by puppet I get the following:
So puppet isn't removing them on certificate generation, but rather during the parsing/output step. |
Huh, so when puppet signs a CSR it drops the bytes, and when it generates its own cert it keeps them? That seems...not consistent :-) Here's my concern though: those bytes are encoding information about the string. By not verifying that those bytes properly match, technically, the value could be something different. I also have a concern around users: if they see that value from your parsed certificate, will they put in Which brings us to a third possibility of a way forward, which is to drop the tag values from the string that comes from the parsing code, and then also strip out |
Puppet isn't dropping the bytes when it signs the CSR. The puppet cert wrapper is making them regular strings as noted in this test: https://github.com/puppetlabs/puppet/blob/496cf2f9b60d2c6224e2e8358f4284f5afa0a00f/spec/unit/ssl/certificate_factory_spec.rb#L158 So, I've continued to go further and I have found that the function which The reason we see
Whereby, if the character at the position is < ' ' (ie our leading bytes of There is a command line option which will parse unknown extensions
This discussion is on going see openssl/openssl#4288 and goes back to openssl/openssl#1 . As a result, it appears to be safe to use the |
Wowza. Thanks for digging in so deeply! In hindsight, this is the obvious correct way to proceed, but you know what they say about hindsight... I'll re-review and hopefully we can get this in for 0.9.1! |
"required_extensions": &framework.FieldSchema{ | ||
Type: framework.TypeCommaStringSlice, | ||
Description: `A comma-separated list of extensions | ||
formatted as "$oid:value". All values much match. Supports globbing on $value.`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please note in the comment that it can be a comma-separated string or an array.
@@ -33,6 +33,8 @@ Sets a CA cert and associated parameters in a role name. | |||
the client certificate with a [globbed pattern] | |||
(https://github.com/ryanuber/go-glob/blob/master/README.md#example). Value is | |||
a comma-separated list of patterns. Authentication requires at least one Name matching at least one pattern. If not set, defaults to allowing all names. | |||
- `required_extensions` `(string: "")` - Require specific Custom Extension OIDs to exist and match the pattern. | |||
Value is a comma separated list of `oid:glob,oid:glob`. All conditions _must_ be met. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment here -- comma separated string or an array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Just some comment fixing.
I think I messed up getting up to date :( |
e2999b6
to
f5e1929
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
Thanks a bunch...merging! |
This change allows the use of custom certificate extensions as a configurable constraint for certificate authentication. Opening the ability to use CAs like Puppet or Vault itself to inject more
detailed information into the certificate as a means of more narrowly defining access to specific profiles.