blob: 81edcae0a6d8825b096b7213af0c7120261a0bca [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 "tools/asset_writer.h"
#include <cstdint>
#include <fstream>
#include <functional>
#include <ios>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/log/absl_check.h"
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "crypto/default/default_crypto_read_handler.h"
#include "formats/bmff/test_utils.h"
#include "formats/gif/constants.h"
#include "formats/gif/test_builder.h"
#include "formats/jpeg/testing/jpeg_creator.h"
#include "formats/png/test_utils.h"
#include "formats/riff/create_riff.h"
#include "formats/tiff/constants.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "proto/generator.pb.h"
#include "proto/manifest.pb.h"
#include "proto/validation_status.pb.h"
#include "riegeli/bytes/cfile_reader.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/string_reader.h"
#include "riegeli/bytes/string_writer.h"
#include "riegeli/endian/endian_writing.h"
#include "testing/crypto_testing.h"
#include "testing/local_key_crypto_write_handler.h"
#include "testing/test_file_utils.h"
#include "validator/asset_validator_impl.h"
#include "validator/validator_options.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::testing::IsEmpty;
using ::testing::Not;
constexpr absl::string_view kPptxMediaType =
"application/"
"vnd.openxmlformats-officedocument.presentationml.presentation";
// Signs with the claim signer and timestamps with the local TSA of `pki`.
LocalKeyCryptoWriteHandlerOptions CreateWriteOptions(const TestPki& pki) {
LocalKeyCryptoWriteHandlerOptions options;
options.claim_signer.key_pem = pki.claim_signer.key_pem;
options.claim_signer.certificate_chain = {pki.claim_signer.cert_der};
options.tsa.key_pem = pki.tsa.key_pem;
options.tsa.cert_pem = pki.tsa.cert_pem;
return options;
}
// Returns a validator that enforces claim signer and TSA trust against the
// root CA of `pki`.
std::unique_ptr<AssetValidatorImpl> CreateTestValidator(const TestPki& pki) {
DefaultCryptoReadHandlerOptions crypto_options;
crypto_options.claim_signer_trust_anchors_pem = pki.root_ca.cert_pem;
crypto_options.tsa_trust_anchors_pem = pki.root_ca.cert_pem;
auto crypto_read_handler = CreateDefaultCryptoReadHandler(crypto_options);
ABSL_CHECK_OK(crypto_read_handler);
return std::make_unique<AssetValidatorImpl>(ValidatorOptions{
.crypto_read_handler = *std::move(crypto_read_handler),
});
}
GenerateManifestStoreRequest CreateRequest() {
GenerateManifestStoreRequest request;
request.mutable_actions()->mutable_created();
return request;
}
// Minimal unsigned test assets, one per supported container format.
absl::StatusOr<std::string> CreatePngAsset() {
return CreatePng({Chunk{.type = 0x49444154, .payload = "pixel_data"}});
}
absl::StatusOr<std::string> CreateJpegAsset() { return CreateJpeg({}); }
absl::StatusOr<std::string> CreateGifAsset() {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionApplication,
"other_app_info", "misc_data");
builder.AddBlock(kGifTrailerIntroducer);
return builder.GetAsset();
}
absl::StatusOr<std::string> CreateBmffAsset() {
return absl::StrCat(
credentio_testing::Box("ftyp", {"isom\x00\x00\x02\x00isom", 12}),
credentio_testing::Box("moov", ""),
credentio_testing::Box("mdat", "video_payload"));
}
absl::StatusOr<std::string> CreateTiffAsset() {
std::string tiff_bytes;
riegeli::StringWriter<> writer(&tiff_bytes);
riegeli::WriteBigEndian<uint32_t>(kTiffBigEndianHeader, writer);
riegeli::WriteBigEndian<uint32_t>(8, writer); // Offset to first IFD
riegeli::WriteBigEndian<uint16_t>(1, writer); // 1 entry
riegeli::WriteBigEndian<uint16_t>(0x0100, writer);
riegeli::WriteBigEndian<uint16_t>(kTiffTypeShort, writer);
riegeli::WriteBigEndian<uint32_t>(1, writer);
riegeli::WriteBigEndian<uint32_t>(10, writer);
riegeli::WriteBigEndian<uint32_t>(0, writer); // next IFD offset
if (!writer.Close()) return writer.status();
return tiff_bytes;
}
absl::StatusOr<std::string> CreateWebpAsset() {
return credentio_riff::RiffChunk("WEBP",
credentio_riff::Chunk("VP8 ", "image_data"));
}
absl::StatusOr<std::string> CreateMp3Asset() {
constexpr char kMp3Bytes[] = {'\xFF', '\xF3', '\x40', '\xC0',
'\x01', '\x02', '\x03', '\x04'};
return std::string(kMp3Bytes, sizeof(kMp3Bytes));
}
absl::StatusOr<std::string> CreatePdfAsset() {
return std::string(R"(%PDF-1.7
%%%%%
1 0 obj
/test_name
endobj
2 0 obj
123.456
endobj
3 0 obj
<</Type /Catalog>>
endobj
xref
0 4
0000000000 65535 f
0000000015 00000 n
0000000041 00000 n
0000000064 00000 n
trailer
<</Root 3 0 R /Size 4>>
startxref
98
%%EOF
)");
}
absl::StatusOr<std::string> CreatePptxAsset() {
return credentio_testing::GetContents(
"credentio/formats/zip/testing/"
"no_c2pa_032_asset.pptx");
}
struct FormatTestCase {
std::string test_name;
// File extension used by `WriteAssetFile` to infer the media type.
std::string extension;
std::string media_type;
std::function<absl::StatusOr<std::string>()> create_asset;
};
class AssetWriterFormatTest : public testing::TestWithParam<FormatTestCase> {
protected:
// Checks that `signed_asset` has an active manifest that validates against
// `pki` without failures.
void ExpectSignedAndValid(riegeli::Reader& signed_asset, const TestPki& pki) {
auto validation_result =
CreateTestValidator(pki)->Validate(signed_asset, GetParam().media_type);
ABSL_ASSERT_OK(validation_result);
const ValidationStatusSet& validation =
(*validation_result)->proto().active_manifest().validation();
EXPECT_THAT(validation.failures(), IsEmpty());
EXPECT_THAT(validation.successes(), Not(IsEmpty()));
}
};
TEST_P(AssetWriterFormatTest, WriteAsset) {
const FormatTestCase& test_case = GetParam();
absl::StatusOr<std::string> asset_bytes = test_case.create_asset();
ABSL_ASSERT_OK(asset_bytes);
riegeli::StringReader<> source(*asset_bytes);
std::string output_bytes;
riegeli::StringWriter<> destination(&output_bytes);
TestPki pki = CreateTestPki();
ASSERT_THAT(WriteAsset(CreateWriteOptions(pki), CreateRequest(),
test_case.media_type, source, destination),
IsOk());
ASSERT_TRUE(destination.Close());
riegeli::StringReader<> result_reader(output_bytes);
ExpectSignedAndValid(result_reader, pki);
}
// Unlike `WriteAsset` above, this writes to a real file through
// `riegeli::CFileWriter`, which e.g. `ZipWriter` must be able to read back.
TEST_P(AssetWriterFormatTest, WriteAssetFile) {
const FormatTestCase& test_case = GetParam();
absl::StatusOr<std::string> asset_bytes = test_case.create_asset();
ABSL_ASSERT_OK(asset_bytes);
const std::string input_path =
absl::StrCat(testing::TempDir(), "/", test_case.test_name, "_input.",
test_case.extension);
const std::string output_path =
absl::StrCat(testing::TempDir(), "/", test_case.test_name, "_output.",
test_case.extension);
{
std::ofstream out(input_path, std::ios::binary);
out << *asset_bytes;
ASSERT_TRUE(out.good());
}
TestPki pki = CreateTestPki();
ASSERT_THAT(WriteAssetFile(CreateWriteOptions(pki), CreateRequest(),
input_path, output_path),
IsOk());
riegeli::CFileReader<> result_reader(output_path);
ASSERT_TRUE(result_reader.ok());
ExpectSignedAndValid(result_reader, pki);
}
INSTANTIATE_TEST_SUITE_P(
AllFormats, AssetWriterFormatTest,
testing::ValuesIn(std::vector<FormatTestCase>{
{"Png", "png", "image/png", CreatePngAsset},
{"Jpeg", "jpg", "image/jpeg", CreateJpegAsset},
{"Gif", "gif", "image/gif", CreateGifAsset},
{"Bmff", "mp4", "video/mp4", CreateBmffAsset},
{"Tiff", "tiff", "image/tiff", CreateTiffAsset},
{"Webp", "webp", "image/webp", CreateWebpAsset},
{"Mp3", "mp3", "audio/mpeg", CreateMp3Asset},
{"Pdf", "pdf", "application/pdf", CreatePdfAsset},
{"Pptx", "pptx", std::string(kPptxMediaType), CreatePptxAsset},
}),
[](const testing::TestParamInfo<FormatTestCase>& info) {
return info.param.test_name;
});
TEST(AssetWriterTest, RejectsInPlaceWrite) {
absl::StatusOr<std::string> png_bytes = CreatePngAsset();
ABSL_ASSERT_OK(png_bytes);
const std::string path =
absl::StrCat(testing::TempDir(), "/test_in_place.png");
{
std::ofstream out(path, std::ios::binary);
out << *png_bytes;
}
TestPki pki = CreateTestPki();
EXPECT_THAT(
WriteAssetFile(CreateWriteOptions(pki), CreateRequest(), path, path),
StatusIs(absl::StatusCode::kInvalidArgument));
// The input asset must be left untouched.
std::ifstream in(path, std::ios::binary);
const std::string contents((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
EXPECT_EQ(contents, *png_bytes);
}
TEST(AssetWriterTest, RejectsUnsupportedMediaType) {
riegeli::StringReader<> source("fake_unknown_bytes");
std::string output_bytes;
riegeli::StringWriter<> destination(&output_bytes);
TestPki pki = CreateTestPki();
EXPECT_THAT(WriteAsset(CreateWriteOptions(pki), CreateRequest(),
"application/octet-stream", source, destination),
StatusIs(absl::StatusCode::kUnimplemented));
}
} // namespace
} // namespace credentio