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