| // 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 "tsp/timestamp_parsing.h" | |
| #include <cstdint> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_join.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/time/time.h" | |
| #include "crypto/algorithms.h" | |
| #include "crypto/cbs_utils.h" | |
| #include "openssl/base.h" | |
| #include "openssl/bytestring.h" | |
| #include "openssl/obj.h" | |
| #include "tsp/constants.h" | |
| #include "tsp/status_codes.h" | |
| namespace credentio { | |
| namespace { | |
| // See RFC 3161, section 2.4.2. | |
| struct FailureInfo { | |
| int value; | |
| absl::string_view description; | |
| }; | |
| constexpr FailureInfo kFailureInfo[] = { | |
| {0, R"((badAlg) unrecognized or unsupported Algorithm Identifier)"}, | |
| {2, R"((badRequest) transaction not permitted or supported)"}, | |
| {5, R"((badDataFormat) the data submitted has the wrong format)"}, | |
| {14, R"((timeNotAvailable) the TSA's time source is not available)"}, | |
| {15, | |
| R"((unacceptedPolicy) the requested TSA policy is not supported by the TSA)"}, | |
| {16, | |
| R"((unacceptedExtension) the requested extension is not supported by the TSA)"}, | |
| {17, | |
| R"((addInfoNotAvailable) the additional information requested could not be understood or is not available)"}, | |
| {25, | |
| R"((systemFailure) the request cannot be handled due to system failure)"}, | |
| }; | |
| // Converts a CBS containing a FailureInfo bitstring to a human-readable string. | |
| std::string FailureInfoString(const CBS& failure_info) { | |
| std::vector<absl::string_view> elements; | |
| for (const auto& info : kFailureInfo) { | |
| if (CBS_asn1_bitstring_has_bit(&failure_info, info.value)) { | |
| elements.push_back(info.description); | |
| } | |
| } | |
| return absl::StrJoin(elements, "; "); | |
| } | |
| } // namespace | |
| absl::StatusOr<absl::string_view> ParseTimestampResp( | |
| absl::string_view timestamp_resp) { | |
| CBS timestamp_resp_cbs; | |
| CBS_init(×tamp_resp_cbs, | |
| reinterpret_cast<const uint8_t*>(timestamp_resp.data()), | |
| timestamp_resp.length()); | |
| CBS seq; | |
| if (!CBS_get_asn1(×tamp_resp_cbs, &seq, CBS_ASN1_SEQUENCE)) { | |
| return absl::InvalidArgumentError( | |
| "cannot parse top-level DER SEQUENCE while parsing TimeStampResp"); | |
| } | |
| CBS pki_status_info; | |
| if (!CBS_get_asn1(&seq, &pki_status_info, CBS_ASN1_SEQUENCE)) { | |
| return absl::InvalidArgumentError("cannot parse TimeStampResp.status"); | |
| } | |
| int64_t raw_status_code; | |
| if (!CBS_get_asn1_int64(&pki_status_info, &raw_status_code)) { | |
| return absl::InvalidArgumentError( | |
| "cannot parse TimeStampResp.status.status"); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(TspPkiStatus status_code, | |
| GetTspPkiStatus(raw_status_code)); | |
| if (!IsTspPkiStatusOk(status_code)) { | |
| CBS status_string; | |
| std::string status_string_joined; | |
| if (CBS_peek_asn1_tag(&pki_status_info, CBS_ASN1_SEQUENCE) && | |
| CBS_get_asn1(&pki_status_info, &status_string, CBS_ASN1_SEQUENCE)) { | |
| CBS status_string_element; | |
| std::vector<std::string> status_string_elements; | |
| while (CBS_get_asn1(&status_string, &status_string_element, | |
| CBS_ASN1_UTF8STRING)) { | |
| status_string_elements.push_back(std::string( | |
| reinterpret_cast<const char*>(CBS_data(&status_string_element)), | |
| CBS_len(&status_string_element))); | |
| } | |
| status_string_joined = absl::StrJoin(status_string_elements, "; "); | |
| } | |
| CBS failure_info; | |
| std::string failure_info_string; | |
| if (CBS_get_asn1(&pki_status_info, &failure_info, CBS_ASN1_BITSTRING)) { | |
| failure_info_string = FailureInfoString(failure_info); | |
| } | |
| std::string error_message = absl::StrCat( | |
| "TimestampResp.status contains an error; status=", status_code); | |
| if (!failure_info_string.empty()) { | |
| absl::StrAppend(&error_message, "; ", failure_info_string); | |
| } | |
| if (!status_string_joined.empty()) { | |
| absl::StrAppend(&error_message, "; ", status_string_joined); | |
| } | |
| return absl::InvalidArgumentError(error_message); | |
| } | |
| // We extract the entire TimeStampToken element (including the header | |
| // bytes) because the CMS library expects a complete DER construct. | |
| CBS ts_token; | |
| if (!CBS_get_asn1_element(&seq, &ts_token, CBS_ASN1_SEQUENCE)) { | |
| return absl::InvalidArgumentError( | |
| "cannot extract TimeStampToken from TimeStampResp"); | |
| } | |
| return absl::string_view(reinterpret_cast<const char*>(ts_token.data), | |
| ts_token.len); | |
| } | |
| absl::Status ParseTstInfo(CBS cbs, absl::Time* time, | |
| std::string* message_imprint_hash, | |
| HashAlgorithm* hash_algorithm, std::string* nonce) { | |
| CBS sequence_cbs; | |
| if (!CBS_get_asn1(&cbs, /*out=*/&sequence_cbs, CBS_ASN1_SEQUENCE)) { | |
| return absl::InvalidArgumentError("could not read top-level sequence"); | |
| } | |
| if (!CBS_get_asn1(&sequence_cbs, /*out=*/nullptr, CBS_ASN1_INTEGER)) { | |
| return absl::InvalidArgumentError("could not skip `version`"); | |
| } | |
| if (!CBS_get_asn1(&sequence_cbs, /*out=*/nullptr, CBS_ASN1_OBJECT)) { | |
| return absl::InvalidArgumentError("could not skip `policy`"); | |
| } | |
| // Format of MessageImprint defined by RFC 3161 section 2.4.1 | |
| CBS messageimprint_cbs; | |
| if (!CBS_get_asn1(&sequence_cbs, /*out=*/&messageimprint_cbs, | |
| CBS_ASN1_SEQUENCE)) { | |
| return absl::InvalidArgumentError("could not read `messageImprint`"); | |
| } | |
| // Format of AlgorithmIdentifier defined by RFC 5280 section 4.1.1.2 | |
| CBS algorithm_identifier_cbs; | |
| if (!CBS_get_asn1(&messageimprint_cbs, /*out=*/&algorithm_identifier_cbs, | |
| CBS_ASN1_SEQUENCE)) { | |
| return absl::InvalidArgumentError( | |
| "could not read `messageImprint.hashAlgorithm`"); | |
| } | |
| CBS oid_cbs; | |
| if (!CBS_get_asn1(&algorithm_identifier_cbs, /*out=*/&oid_cbs, | |
| CBS_ASN1_OBJECT)) { | |
| return absl::InvalidArgumentError( | |
| "could not read `messageImprint.hashAlgorithm.algorithm`"); | |
| } | |
| ABSL_ASSIGN_OR_RETURN( | |
| *hash_algorithm, NidToHashAlgorithm(OBJ_cbs2nid(&oid_cbs)), | |
| _.SetPrepend() << "unsupported message imprint hash algorithm: "); | |
| CBS hash_cbs; | |
| if (!CBS_get_asn1(&messageimprint_cbs, &hash_cbs, CBS_ASN1_OCTETSTRING)) { | |
| return absl::InvalidArgumentError( | |
| "could not read `messageImprint.hashedMessage`"); | |
| } | |
| *message_imprint_hash = ToStringView(hash_cbs); | |
| if (!CBS_get_asn1(&sequence_cbs, /*out=*/nullptr, CBS_ASN1_INTEGER)) { | |
| return absl::InvalidArgumentError("could not skip `serialNumber`"); | |
| } | |
| // Note that we can't use the OpenSSL function to decode the time value | |
| // because it enforces the limits imposed in RFC 5280, which is more | |
| // restrictive than RFC 3161. | |
| CBS gentime_cbs; | |
| if (!CBS_get_asn1(&sequence_cbs, /*out=*/&gentime_cbs, | |
| CBS_ASN1_GENERALIZEDTIME)) { | |
| return absl::InvalidArgumentError("could not read `genTime`"); | |
| } | |
| absl::string_view gentime_view( | |
| reinterpret_cast<const char*>(gentime_cbs.data), gentime_cbs.len); | |
| if (!absl::EndsWith(gentime_view, "Z")) { | |
| return absl::InvalidArgumentError( | |
| "`genTime` value is not specified in UTC (needs \"Z\" suffix)"); | |
| } | |
| if (gentime_view.find_first_of('Z') != gentime_view.length() - 1) { | |
| return absl::InvalidArgumentError( | |
| "`genTime` is not a valid timestamp (contains \"Z\" that is not at " | |
| "end)"); | |
| } | |
| std::string error; | |
| if (!absl::ParseTime(kGenTimeFormat, gentime_view, time, &error)) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("could not parse `genTime` value: ", error)); | |
| } | |
| // Skip over `accuracy` if exists. | |
| if (CBS_peek_asn1_tag(&sequence_cbs, CBS_ASN1_SEQUENCE)) { | |
| if (!CBS_get_asn1(&sequence_cbs, /*out=*/nullptr, CBS_ASN1_SEQUENCE)) { | |
| return absl::InvalidArgumentError("could not skip `accuracy`"); | |
| } | |
| } | |
| // Skip over `ordering` if exists. | |
| if (CBS_peek_asn1_tag(&sequence_cbs, CBS_ASN1_BOOLEAN)) { | |
| int ordering; | |
| if (!CBS_get_asn1_bool(&sequence_cbs, &ordering)) { | |
| return absl::InvalidArgumentError("could not skip `ordering`"); | |
| } | |
| } | |
| // Retrieve `nonce` if exists. | |
| if (CBS_peek_asn1_tag(&sequence_cbs, CBS_ASN1_INTEGER)) { | |
| CBS nonce_cbs; | |
| if (CBS_get_asn1_element(&sequence_cbs, &nonce_cbs, CBS_ASN1_INTEGER)) { | |
| *nonce = std::string(reinterpret_cast<const char*>(CBS_data(&nonce_cbs)), | |
| CBS_len(&nonce_cbs)); | |
| } else { | |
| return absl::InvalidArgumentError("could not read `nonce`"); | |
| } | |
| } else { | |
| nonce->clear(); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace credentio |