blob: 9a4dfabb4153e42663942e2d5891cff00cd6d38c [file]
// 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