| // 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 |