| // 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 "crypto/default/cms/ber_parser.h" | |
| #include <cstring> | |
| #include <limits> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "crypto/default/cms/cms_error_code.h" | |
| #include "openssl/base.h" | |
| #include "openssl/bytestring.h" | |
| namespace credentio_cms { | |
| BerParser::BerParser(const uint8_t* data, size_t length) { | |
| CBS cbs; | |
| CBS_init(&cbs, data, length); | |
| scopes_.push_back(Scope(cbs, false)); | |
| start_ = data; | |
| } | |
| void BerParser::SetElementName(absl::string_view text) { | |
| scopes_.back().element_name = text; | |
| } | |
| bool BerParser::ok() const { return status_ == ErrorCode::OK; } | |
| ErrorCode BerParser::status() const { return status_; } | |
| std::string BerParser::error_message() const { return error_message_; } | |
| // Checks that the next element has the expected tag. If the element is | |
| // constructed the parser will read from it until CloseTag is called. | |
| void BerParser::GetTag(unsigned expected_tag) { | |
| unsigned tag = 0; | |
| GetTagInternal(&tag, nullptr, nullptr, false /* raw */); | |
| CompareTag(tag, expected_tag, nullptr /* out */); | |
| } | |
| bool BerParser::GetOptionalTag(unsigned expected_tag) { | |
| if (!Peek(expected_tag)) { | |
| return false; | |
| } | |
| GetTag(expected_tag); | |
| return ok(); | |
| } | |
| void BerParser::GetElementData(unsigned expected_tag, CBS* out) { | |
| if (!IsValid(out)) { | |
| return; | |
| } | |
| unsigned tag = 0; | |
| GetTagInternal(&tag, out, nullptr /* headers */, false /* raw tag */); | |
| CompareTag(tag, expected_tag, out); | |
| } | |
| bool BerParser::GetOptionalElementData(unsigned expected_tag, CBS* out) { | |
| if (!IsValid(out) || !Peek(expected_tag)) { | |
| return false; | |
| } | |
| GetElementData(expected_tag, out); | |
| return ok(); | |
| } | |
| void BerParser::GetAnyElementAndSkipChildren(CBS* out) { | |
| if (!IsValid(out)) { | |
| return; | |
| } | |
| unsigned tag; | |
| GetTagInternal(&tag, nullptr, out, true /* raw */); | |
| } | |
| bool BerParser::Peek(unsigned expected_tag) const { | |
| if (!ok()) { | |
| return false; | |
| } | |
| const Scope& scope = scopes_.back(); | |
| return CBS_peek_asn1_tag(&scope.data, expected_tag) == 1; | |
| } | |
| bool BerParser::IsNullOrEmpty() { | |
| if (!ok()) { | |
| return true; | |
| } | |
| Scope& scope = scopes_.back(); | |
| if (CBS_len(&scope.data) == 0) { | |
| return true; | |
| } | |
| if (CBS_peek_asn1_tag(&scope.data, CBS_ASN1_NULL) == 1) { | |
| if (CBS_get_asn1(&scope.data, nullptr, CBS_ASN1_NULL) != 1) { | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, "Failed to read NULL element "); | |
| } | |
| return true; | |
| } | |
| return scope.indefinite && IsEndOfContent(); | |
| } | |
| int BerParser::GetInt() { | |
| if (!ok()) { | |
| // No-op if there was already an error. | |
| return 0; | |
| } | |
| Scope& scope = scopes_.back(); | |
| uint64_t v; | |
| if (CBS_get_asn1_uint64(&scope.data, &v) != 1 || | |
| v > std::numeric_limits<int>::max()) { | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, "Failed to get an int "); | |
| return 0; | |
| } | |
| return static_cast<int>(v); | |
| } | |
| void BerParser::EndConstructed() { | |
| if (!ok()) { | |
| return; | |
| } | |
| if (scopes_.size() < 2) { | |
| // There should always be at least one element in the scope stack, this | |
| // would pop the last one. | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, "Unexpected EndConstructed call "); | |
| return; | |
| } | |
| Scope& scope = scopes_.back(); | |
| if (!scope.indefinite) { | |
| if (CBS_len(&scope.data) > 0) { | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, | |
| "Found data remaining while expecting the end of the current " | |
| "element "); | |
| } | |
| scopes_.pop_back(); | |
| return; | |
| } | |
| if (!IsEndOfContent() || CBS_skip(&scope.data, 2) != 1) { | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, | |
| "Error while expecting the end of an indefinite length element "); | |
| return; | |
| } | |
| const auto end = CBS_data(&scope.data); | |
| scopes_.pop_back(); | |
| const auto previous = CBS_data(&scopes_.back().data); | |
| if (end < previous || CBS_skip(&scopes_.back().data, end - previous) != 1) { | |
| SetError( | |
| ErrorCode::ASN1_PARSING_ERROR, | |
| "Failed to seek past the end of the current indefinite length tag "); | |
| } | |
| } | |
| bool BerParser::IsValid(CBS* out) { | |
| if (out == nullptr) { | |
| SetError(ErrorCode::INVALID_PARAMETER, | |
| "Caller error, null pointer passed to IsValid "); | |
| return false; | |
| } | |
| CBS_init(out, nullptr, 0); | |
| return status_ == ErrorCode::OK; | |
| } | |
| bool BerParser::IsEndOfContent() { | |
| Scope& scope = scopes_.back(); | |
| return CBS_len(&scope.data) >= 2 && | |
| memcmp(CBS_data(&scope.data), "\0\0", 2) == 0; | |
| } | |
| void BerParser::GetTagInternal(unsigned* tag, CBS* value, CBS* header_and_value, | |
| bool raw_tag) { | |
| if (!ok()) { | |
| return; | |
| } | |
| Scope& scope = scopes_.back(); | |
| size_t header_length; | |
| CBS out; | |
| int indefinite_length; | |
| if (CBS_get_any_ber_asn1_element(&scope.data, &out, tag, &header_length, | |
| /*out_ber_found=*/nullptr, | |
| &indefinite_length) != 1) { | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, "Parsing error "); | |
| return; | |
| } | |
| if (indefinite_length && (value != nullptr || header_and_value != nullptr)) { | |
| SetError( | |
| ErrorCode::ASN1_PARSING_ERROR, | |
| "Indefinite length not supported when the element data is needed "); | |
| return; | |
| } | |
| if (header_and_value != nullptr) { | |
| *header_and_value = out; | |
| } | |
| if (value != nullptr) { | |
| *value = out; | |
| if (CBS_skip(value, header_length) != 1) { | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, "Failed to skip the header "); | |
| return; | |
| } | |
| } | |
| if (raw_tag || (*tag & CBS_ASN1_CONSTRUCTED) == 0) { | |
| return; | |
| } | |
| // If the tag is constructed, start a new scope containing either the | |
| // element we just read (definite length) or all the data remaining if the | |
| // length is unknown. | |
| if (indefinite_length) { | |
| scopes_.push_back(Scope(scope.data, indefinite_length)); | |
| } else { | |
| if (CBS_skip(&out, header_length) != 1) { | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, "Failed to read the header "); | |
| return; | |
| } | |
| scopes_.push_back(Scope(out, indefinite_length)); | |
| } | |
| } | |
| void BerParser::CompareTag(unsigned tag, unsigned expected_tag, CBS* out) { | |
| if (ok() && tag != expected_tag) { | |
| std::string message = | |
| absl::StrCat("Got tag ", tag, " instead of tag ", expected_tag); | |
| SetError(ErrorCode::ASN1_PARSING_ERROR, message); | |
| if (out != nullptr) { | |
| CBS_init(out, nullptr, 0); | |
| } | |
| } | |
| } | |
| void BerParser::SetError(ErrorCode status, absl::string_view text) { | |
| if (!ok()) { | |
| // Don't clear existing errors. | |
| return; | |
| } | |
| status_ = status; | |
| error_message_ = | |
| absl::StrCat(text, " at offset: ", GetCurrentOffset(), " in scope: "); | |
| for (const auto& scope : scopes_) { | |
| if (!scope.element_name.empty()) { | |
| absl::StrAppend(&error_message_, scope.element_name, " > "); | |
| } | |
| } | |
| } | |
| size_t BerParser::GetCurrentOffset() { | |
| if (scopes_.empty()) { | |
| return 0; | |
| } | |
| return CBS_data(&scopes_.back().data) - start_; | |
| } | |
| } // namespace credentio_cms |