From 55de8f7fd70f6e14771f55077ae6385c8c7f8cc5 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Wed, 19 Jan 2022 22:00:26 -0500 Subject: [PATCH] Simple libidn2 wrapper This adds a simple libidn2 wrapper, including unit tests via GTest. --- contrib/libidn2/CMakeLists.txt | 62 +++++++++++++++ contrib/libidn2/libidn2_sapi.cc | 90 ++++++++++++++++++++++ contrib/libidn2/libidn2_sapi.h | 65 ++++++++++++++++ contrib/libidn2/tests/CMakeLists.txt | 20 +++++ contrib/libidn2/tests/libidn2_sapi_test.cc | 61 +++++++++++++++ 5 files changed, 298 insertions(+) create mode 100644 contrib/libidn2/CMakeLists.txt create mode 100644 contrib/libidn2/libidn2_sapi.cc create mode 100644 contrib/libidn2/libidn2_sapi.h create mode 100644 contrib/libidn2/tests/CMakeLists.txt create mode 100644 contrib/libidn2/tests/libidn2_sapi_test.cc diff --git a/contrib/libidn2/CMakeLists.txt b/contrib/libidn2/CMakeLists.txt new file mode 100644 index 0000000..bbcfd94 --- /dev/null +++ b/contrib/libidn2/CMakeLists.txt @@ -0,0 +1,62 @@ +# Copyright 2022 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 +# +# http://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. + +cmake_minimum_required(VERSION 3.13) + +project(test CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +set(SAPI_ROOT "${PROJECT_SOURCE_DIR}/../.." CACHE PATH "Path to the Sandboxed API source tree") +set(SAPI_ENABLE_EXAMPLES OFF CACHE BOOL "") +set(SAPI_ENABLE_TESTS OFF CACHE BOOL "") + +add_subdirectory("${SAPI_ROOT}" + "${CMAKE_BINARY_DIR}/sandboxed-api-build" + # Omit this to have the full Sandboxed API in IDE + EXCLUDE_FROM_ALL) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBIDN2 REQUIRED IMPORTED_TARGET libidn2) + +add_sapi_library(libidn2_sapi + FUNCTIONS idn2_lookup_u8 idn2_register_u8 + idn2_strerror idn2_strerror_name + idn2_free idn2_to_ascii_8z + idn2_to_unicode_8z8z + INPUTS "${LIBIDN2_INCLUDEDIR}/idn2.h" + LIBRARY idn2 + LIBRARY_NAME IDN2 + NAMESPACE "" +) + +target_include_directories(libidn2_sapi INTERFACE + "${PROJECT_BINARY_DIR}" +) + +add_library(libidn2_sapi_wrapper + libidn2_sapi.cc + libidn2_sapi.h +) + +target_link_libraries(libidn2_sapi_wrapper + # PUBLIC so that the include directories are included in the interface + PUBLIC libidn2_sapi sapi::base + PRIVATE idn2 +) + +if(SAPI_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/contrib/libidn2/libidn2_sapi.cc b/contrib/libidn2/libidn2_sapi.cc new file mode 100644 index 0000000..7fbfe66 --- /dev/null +++ b/contrib/libidn2/libidn2_sapi.cc @@ -0,0 +1,90 @@ +// Copyright 2022 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 +// +// http://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 +#include + +#include +#include +#include +#include "sandboxed_api/util/fileops.h" + +#include "libidn2_sapi.h" // NOLINT(build/include) +#include "libidn2_sapi.sapi.h" // NOLINT(build/include) + +static constexpr std::size_t kMaxDomainNameLength = 256; +static constexpr int kMinPossibleKnownError = -10000; + +absl::StatusOr IDN2Lib::ProcessErrors( + const absl::StatusOr& untrusted_res, sapi::v::GenericPtr& ptr) { + SAPI_RETURN_IF_ERROR(untrusted_res.status()); + int res = untrusted_res.value(); + if (res < 0) { + if (res == IDN2_MALLOC) { + return absl::ResourceExhaustedError("malloc() failed in libidn2"); + } + if (res > kMinPossibleKnownError) { + return absl::InvalidArgumentError(idn2_strerror(res)); + } + return absl::InvalidArgumentError("Unexpected error"); + } + ::sapi::v::RemotePtr p(reinterpret_cast(ptr.GetValue())); + auto maybe_untrusted_name = sandbox_->GetCString(p, kMaxDomainNameLength); + SAPI_RETURN_IF_ERROR(sandbox_->Free(&p)); + if (!maybe_untrusted_name.ok()) { + return maybe_untrusted_name.status(); + } + // FIXME: sanitize the result by checking that the return value is + // valid ASCII (for a-labels) or UTF-8 (for u-labels) and doesn't + // contain potentially malicious characters. + return *maybe_untrusted_name; +} + +absl::StatusOr IDN2Lib::idn2_register_u8(const char* ulabel, + const char* alabel) { + ::std::optional<::sapi::v::ConstCStr> alabel_ptr, ulabel_ptr; + if (ulabel) ulabel_ptr.emplace(ulabel); + if (alabel) alabel_ptr.emplace(alabel); + ::sapi::v::GenericPtr ptr; + ::sapi::v::NullPtr null_ptr; + const auto untrusted_res = api_.idn2_register_u8( + ulabel ? ulabel_ptr->PtrBefore() : &null_ptr, + alabel ? alabel_ptr->PtrBefore() : &null_ptr, ptr.PtrAfter(), + IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL); + return this->ProcessErrors(untrusted_res, ptr); +} + +absl::StatusOr IDN2Lib::SapiGeneric( + const char* data, + absl::StatusOr (IDN2Api::*cb)(sapi::v::Ptr* input, + sapi::v::Ptr* output, int flags)) { + ::sapi::v::ConstCStr src(data); + ::sapi::v::GenericPtr ptr; + + absl::StatusOr untrusted_res = ((api_).*(cb))( + src.PtrBefore(), ptr.PtrAfter(), IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL); + return this->ProcessErrors(untrusted_res, ptr); +} + +absl::StatusOr IDN2Lib::idn2_to_unicode_8z8z(const char* data) { + return IDN2Lib::SapiGeneric(data, &IDN2Api::idn2_to_unicode_8z8z); +} + +absl::StatusOr IDN2Lib::idn2_to_ascii_8z(const char* data) { + return IDN2Lib::SapiGeneric(data, &IDN2Api::idn2_to_ascii_8z); +} + +absl::StatusOr IDN2Lib::idn2_lookup_u8(const char* data) { + return IDN2Lib::SapiGeneric(data, &IDN2Api::idn2_lookup_u8); +} diff --git a/contrib/libidn2/libidn2_sapi.h b/contrib/libidn2/libidn2_sapi.h new file mode 100644 index 0000000..a45453a --- /dev/null +++ b/contrib/libidn2/libidn2_sapi.h @@ -0,0 +1,65 @@ +// Copyright 2022 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 +// +// http://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. + +#ifndef CONTRIB_LIBIDN2_LIBIDN2_SAPI_H_ +#define CONTRIB_LIBIDN2_LIBIDN2_SAPI_H_ + +#include +#include + +#include + +#include "libidn2_sapi.sapi.h" // NOLINT(build/include) +#include "sandboxed_api/util/fileops.h" +class Idn2SapiSandbox : public IDN2Sandbox { + public: + std::unique_ptr ModifyPolicy( + sandbox2::PolicyBuilder*) override { + return sandbox2::PolicyBuilder() + .AllowSystemMalloc() + .AllowRead() + .AllowStat() + .AllowWrite() + .AllowExit() + .AllowSyscalls({ + __NR_futex, + __NR_close, + __NR_lseek, + __NR_getpid, + }) + .BuildOrDie(); + } +}; + +class IDN2Lib { + public: + explicit IDN2Lib(Idn2SapiSandbox* sandbox) + : sandbox_(CHECK_NOTNULL(sandbox)), api_(sandbox_) {} + absl::StatusOr idn2_register_u8(const char* ulabel, + const char* alabel); + absl::StatusOr idn2_lookup_u8(const char* data); + absl::StatusOr idn2_to_ascii_8z(const char* ulabel); + absl::StatusOr idn2_to_unicode_8z8z(const char* ulabel); + + private: + absl::StatusOr SapiGeneric( + const char* data, + absl::StatusOr (IDN2Api::*cb)(sapi::v::Ptr* input, + sapi::v::Ptr* output, int flags)); + absl::StatusOr ProcessErrors(const absl::StatusOr& status, + sapi::v::GenericPtr& ptr); + Idn2SapiSandbox* sandbox_; + IDN2Api api_; +}; +#endif // CONTRIB_LIBIDN2_LIBIDN2_SAPI_H_ diff --git a/contrib/libidn2/tests/CMakeLists.txt b/contrib/libidn2/tests/CMakeLists.txt new file mode 100644 index 0000000..1c98e9a --- /dev/null +++ b/contrib/libidn2/tests/CMakeLists.txt @@ -0,0 +1,20 @@ +# Copyright 2022 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 +# +# http://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. + +add_executable(libidn2_sapi_test libidn2_sapi_test.cc) + +target_link_libraries(libidn2_sapi_test PRIVATE + libidn2_sapi_wrapper + sapi::test_main +) diff --git a/contrib/libidn2/tests/libidn2_sapi_test.cc b/contrib/libidn2/tests/libidn2_sapi_test.cc new file mode 100644 index 0000000..fc52adf --- /dev/null +++ b/contrib/libidn2/tests/libidn2_sapi_test.cc @@ -0,0 +1,61 @@ +// Copyright 2022 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 +// +// http://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 +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "../libidn2_sapi.h" +#include "sandboxed_api/testing.h" +#include "sandboxed_api/util/status_matchers.h" + +using ::testing::StrEq; +using ::testing::Not; +using ::sapi::IsOk; + +class Idn2SapiSandboxTest : public testing::Test { + protected: + static void SetUpTestSuite() { + sandbox_ = new Idn2SapiSandbox(); + ASSERT_THAT(sandbox_->Init(), IsOk()); + lib_ = new IDN2Lib(sandbox_); + } + static void TearDownTestSuite() { + delete lib_; + delete sandbox_; + } + static IDN2Lib* lib_; + private: + static Idn2SapiSandbox* sandbox_; +}; + +IDN2Lib* Idn2SapiSandboxTest::lib_; +Idn2SapiSandbox* Idn2SapiSandboxTest::sandbox_; + +TEST_F(Idn2SapiSandboxTest, WorksOkay) { + EXPECT_THAT(lib_->idn2_lookup_u8("β").value(), StrEq("xn--nxa")); + EXPECT_THAT(lib_->idn2_lookup_u8("ß").value(), StrEq("xn--zca")); + EXPECT_THAT(lib_->idn2_lookup_u8("straße.de").value(), + StrEq("xn--strae-oqa.de")); + EXPECT_THAT(lib_->idn2_to_unicode_8z8z("xn--strae-oqa.de").value(), + StrEq("straße.de")); + EXPECT_THAT(lib_->idn2_lookup_u8("--- "), Not(IsOk())); +} + +TEST_F(Idn2SapiSandboxTest, RegisterConversion) { + // I could not get this to succeed except on ASCII-only strings + EXPECT_THAT(lib_->idn2_register_u8("βgr", "xn--gr-e9b").value(), StrEq("xn--gr-e9b")); + EXPECT_THAT(lib_->idn2_register_u8("βgr", "xn--gr-e9"), Not(IsOk())); + EXPECT_THAT(lib_->idn2_register_u8("β.gr", nullptr), Not(IsOk())); +}