| // Copyright 2026 Google LLC | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // | |
| #include "utils/distinguished_name.h" | |
| #include <cstddef> | |
| #include <string> | |
| #include <utility> | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/escaping.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/strings/strip.h" | |
| namespace credentio { | |
| namespace { | |
| // Consumes an OID. | |
| // | |
| // oid = 1*DIGIT *("." 1*DIGIT) | |
| // (but we only enforce that it starts with a digit and contains digits and ".") | |
| absl::StatusOr<std::string> ConsumeOid(absl::string_view* s) { | |
| if (s->empty() || !absl::ascii_isdigit(s->front())) { | |
| return absl::InvalidArgumentError("expected OID value starting with digit"); | |
| } | |
| size_t len = s->find_first_not_of("0123456789."); | |
| std::string oid = std::string(s->substr(0, len)); | |
| s->remove_prefix(oid.size()); | |
| return oid; | |
| } | |
| // Consumes an attribute type (e.g., "CN" or "1.2.3"). | |
| // | |
| // attributeType = (ALPHA 1*keychar) / oid | |
| // keychar = ALPHA / DIGIT / "-" | |
| absl::StatusOr<std::string> ConsumeAttr(absl::string_view* s) { | |
| if (s->empty()) { | |
| return absl::InvalidArgumentError("empty attribute name"); | |
| } | |
| if (absl::ascii_isdigit(s->front())) { | |
| return ConsumeOid(s); | |
| } | |
| if (!absl::ascii_isalpha(s->front())) { | |
| return absl::InvalidArgumentError( | |
| "expected attribute name starting with alpha"); | |
| } | |
| std::string value; | |
| while (!s->empty()) { | |
| if (absl::ascii_isalpha(s->front()) || absl::ascii_isdigit(s->front()) || | |
| s->front() == '-') { | |
| value.push_back(s->front()); | |
| s->remove_prefix(1); | |
| } else { | |
| break; | |
| } | |
| } | |
| return value; | |
| } | |
| // Consumes a #aabbccddee string, returning decoded value. | |
| absl::StatusOr<std::string> ConsumeHexStringValue(absl::string_view* s) { | |
| absl::string_view temp = *s; | |
| if (!absl::ConsumePrefix(&temp, "#")) { | |
| return absl::InvalidArgumentError("expected hex string starting with #"); | |
| } | |
| absl::string_view digits = | |
| temp.substr(0, temp.find_first_not_of("0123456789abcdefABCDEF")); | |
| temp.remove_prefix(digits.length()); | |
| std::string value; | |
| if (!absl::HexStringToBytes(digits, &value)) { | |
| return absl::InvalidArgumentError("cannot parse hex string"); | |
| } | |
| *s = temp; | |
| return value; | |
| } | |
| // Consumes a backslash escape, returning decoded character. | |
| // | |
| // pair = "\" ( special / "\" / QUOTATION / hexpair ) | |
| // stringchar = <any character except one of special, "\" or QUOTATION > | |
| // | |
| // hexstring = 1*hexpair | |
| // hexpair = hexchar hexchar | |
| // | |
| // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" | |
| // / "a" / "b" / "c" / "d" / "e" / "f" | |
| // special = "," / "=" / "+" / "<" / ">" / "#" / ";" | |
| absl::StatusOr<std::string> ConsumeBackslashEscape(absl::string_view* s) { | |
| absl::string_view temp = *s; | |
| if (!absl::ConsumePrefix(&temp, "\\")) { | |
| return absl::InvalidArgumentError( | |
| "expected backslash escape starting with \\"); | |
| } | |
| if (temp.empty()) { | |
| return absl::InvalidArgumentError("backslash at end of input"); | |
| } | |
| if (absl::ascii_isxdigit(absl::ascii_toupper(temp.front()))) { | |
| std::string decoded_hex; | |
| if (temp.length() < 2 || | |
| !absl::HexStringToBytes(temp.substr(0, 2), &decoded_hex)) { | |
| return absl::InvalidArgumentError("cannot decode hex backslash escape"); | |
| } | |
| temp.remove_prefix(2); | |
| *s = temp; | |
| return decoded_hex; | |
| } | |
| if (!absl::StrContains(",=+<>#;\"\\", temp.front())) { | |
| return absl::InvalidArgumentError("invalid backslash escape"); | |
| } | |
| std::string value = std::string(temp.substr(0, 1)); | |
| temp.remove_prefix(1); | |
| *s = temp; | |
| return value; | |
| } | |
| // Consumes a double-quoted string, returning decoded value. | |
| // string = ... / QUOTATION *( quotechar / pair ) QUOTATION ; only from v2 | |
| // quotechar = <any character except "\" or QUOTATION > | |
| absl::StatusOr<std::string> ConsumeQuotedStringValue(absl::string_view* s) { | |
| absl::string_view temp = *s; | |
| if (!absl::ConsumePrefix(&temp, "\"")) { | |
| return absl::InvalidArgumentError( | |
| "expected quoted string starting with \""); | |
| } | |
| std::string value; | |
| while (!temp.empty()) { | |
| if (absl::ConsumePrefix(&temp, "\"")) { | |
| *s = temp; | |
| return value; | |
| } | |
| if (temp.front() == '\\') { | |
| ABSL_ASSIGN_OR_RETURN(auto unescaped, ConsumeBackslashEscape(&temp)); | |
| absl::StrAppend(&value, unescaped); | |
| } else { | |
| value.push_back(temp.front()); | |
| temp.remove_prefix(1); | |
| } | |
| } | |
| return absl::InvalidArgumentError("closing '\"' not found"); | |
| } | |
| // Consumes an attribute value and returns the decoded value. | |
| // | |
| // attributeValue = string | |
| // string = *( stringchar / pair ) | |
| // / "#" hexstring | |
| // / QUOTATION *( quotechar / pair ) QUOTATION ; only from v2 | |
| absl::StatusOr<std::string> ConsumeValue(absl::string_view* s) { | |
| if (!s->empty() && s->front() == '#') { | |
| return ConsumeHexStringValue(s); | |
| } | |
| if (!s->empty() && s->front() == '"') { | |
| return ConsumeQuotedStringValue(s); | |
| } | |
| absl::string_view temp = *s; | |
| std::string value; | |
| while (!temp.empty() && !absl::StrContains(",=+<>#;\"", temp.front())) { | |
| if (temp.front() == '\\') { | |
| ABSL_ASSIGN_OR_RETURN(auto unescaped, ConsumeBackslashEscape(&temp)); | |
| absl::StrAppend(&value, unescaped); | |
| } else { | |
| value.push_back(temp.front()); | |
| temp.remove_prefix(1); | |
| } | |
| } | |
| *s = temp; | |
| return value; | |
| } | |
| // Consumes an attribute/value (e.g., "FOO=Bar") and returns the pair. | |
| absl::StatusOr<std::pair<std::string, std::string>> ConsumeAttrAndValue( | |
| absl::string_view* s) { | |
| absl::string_view temp = *s; | |
| ABSL_ASSIGN_OR_RETURN(auto attr, ConsumeAttr(&temp)); | |
| if (!absl::ConsumePrefix(&temp, "=")) { | |
| return absl::InvalidArgumentError("expected '=' after attribute name"); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(auto value, ConsumeValue(&temp)); | |
| *s = temp; | |
| return std::make_pair(attr, value); | |
| } | |
| } // namespace | |
| absl::StatusOr<absl::flat_hash_map<std::string, std::string>> | |
| ParseDistinguishedName(absl::string_view distinguished_name) { | |
| absl::flat_hash_map<std::string, std::string> attrs; | |
| while (!distinguished_name.empty()) { | |
| auto attr_and_value = ConsumeAttrAndValue(&distinguished_name); | |
| if (!attr_and_value.ok()) { | |
| return absl::Status(attr_and_value.status().code(), | |
| absl::StrCat("cannot parse RFC2253 DN: ", | |
| attr_and_value.status().message())); | |
| } | |
| const auto& [attr, value] = *attr_and_value; | |
| for (const auto& [existing_attr, _] : attrs) { | |
| if (absl::EqualsIgnoreCase(existing_attr, attr)) { | |
| return absl::InvalidArgumentError("Duplicate attribute in RFC2253 DN"); | |
| } | |
| } | |
| attrs[attr] = value; | |
| if (distinguished_name.empty()) { | |
| break; | |
| } | |
| if (absl::ConsumePrefix(&distinguished_name, "+")) { | |
| return absl::UnimplementedError("multi-valued RDN not supported"); | |
| } | |
| if (!absl::ConsumePrefix(&distinguished_name, ",")) { | |
| return absl::InvalidArgumentError("Cannot parse RFC2253 DN"); | |
| } | |
| } | |
| return attrs; | |
| } | |
| } // namespace credentio |