diff --git a/.clang-tidy b/.clang-tidy index 2c9859e6b..a885be834 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,9 +2,11 @@ Checks: '*, -altera-*, -fuchsia-*, + -abseil-string-find-startswith, -bugprone-easily-swappable-parameters, -bugprone-exception-escape, -cert-err58-cpp, + -cppcoreguidelines-avoid-const-or-ref-data-members, -cppcoreguidelines-avoid-magic-numbers, -cppcoreguidelines-avoid-non-const-global-variables, -cppcoreguidelines-non-private-member-variables-in-classes, @@ -20,6 +22,8 @@ Checks: '*, -llvmlibc-implementation-in-namespace, -llvmlibc-restrict-system-libc-headers, -misc-non-private-member-variables-in-classes, + -misc-use-anonymous-namespace, + -misc-no-recursion, -modernize-use-nodiscard, -modernize-use-trailing-return-type, -readability-function-cognitive-complexity, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 5ff051c7d..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Build and Test - -on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: '30 2 * * 1' - -env: - CTEST_OUTPUT_ON_FAILURE: 1 - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-latest, ubuntu-latest, macos-latest] - include: - - os: windows-latest - vcpkg-cmake-file: "$env:VCPKG_INSTALLATION_ROOT\\scripts\\buildsystems\\vcpkg.cmake" - ctest-target: RUN_TESTS - - os: ubuntu-latest - vcpkg-cmake-file: "$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" - ctest-target: test - - os: macos-latest - vcpkg-cmake-file: "$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" - ctest-target: test - - env: - CMAKE_BUILD_DIR: ${{ github.workspace }}/build - CMAKE_TEST_DIR: ${{ github.workspace }}/build/test - - steps: - - uses: actions/checkout@v2 - - - name: dependencies (macos) - if: ${{ matrix.os == 'macos-latest' }} - run: | - brew install llvm pkg-config - ln -s "/usr/local/opt/llvm/bin/clang-format" "/usr/local/bin/clang-format" - ln -s "/usr/local/opt/llvm/bin/clang-tidy" "/usr/local/bin/clang-tidy" - - - name: Restore cache - uses: actions/cache@v2 - with: - path: | - ${{ env.CMAKE_BUILD_DIR }}/vcpkg_installed - key: ${{ runner.os }}-${{ hashFiles( '**/vcpkg.json' ) }} - - - name: Configure to use clang-tidy and sanitizers - run: | - cmake -B "${{ env.CMAKE_BUILD_DIR }}" -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON -DCMAKE_TOOLCHAIN_FILE="${{ matrix.vcpkg-cmake-file}}" . - - - name: Build - run: | - cmake --build "${{ env.CMAKE_BUILD_DIR }}" - - - name: Unit tests - run: | - cmake --build "${{ env.CMAKE_BUILD_DIR }}" --target "${{ matrix.ctest-target}}" diff --git a/.github/workflows/build_openssl3.yml b/.github/workflows/build_openssl3.yml deleted file mode 100644 index 9899ec44a..000000000 --- a/.github/workflows/build_openssl3.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Build and Test (OpenSSL 3) - -on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: '30 3 * * 1' - -env: - CTEST_OUTPUT_ON_FAILURE: 1 - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-latest, ubuntu-latest, macos-latest] - include: - - os: windows-latest - vcpkg-cmake-file: "$env:VCPKG_INSTALLATION_ROOT\\scripts\\buildsystems\\vcpkg.cmake" - ossl3-vcpkg-dir: "alternatives\\openssl_3" - ctest-target: RUN_TESTS - - os: ubuntu-latest - vcpkg-cmake-file: "$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" - ossl3-vcpkg-dir: "alternatives/openssl_3" - ctest-target: test - - os: macos-latest - vcpkg-cmake-file: "$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" - ossl3-vcpkg-dir: "alternatives/openssl_3" - ctest-target: test - - env: - CMAKE_BUILD_DIR: ${{ github.workspace }}/build - CMAKE_TEST_DIR: ${{ github.workspace }}/build/test - - steps: - - uses: actions/checkout@v2 - - - name: dependencies (macos) - if: ${{ matrix.os == 'macos-latest' }} - run: | - brew install llvm pkg-config - ln -s "/usr/local/opt/llvm/bin/clang-format" "/usr/local/bin/clang-format" - ln -s "/usr/local/opt/llvm/bin/clang-tidy" "/usr/local/bin/clang-tidy" - - - name: Restore cache - uses: actions/cache@v2 - with: - path: | - ${{ env.CMAKE_BUILD_DIR }}/vcpkg_installed - key: ${{ runner.os }}-${{ hashFiles( '**/vcpkg.json' ) }} - - - name: Configure to use clang-tidy and sanitizers - run: | - cmake -B "${{ env.CMAKE_BUILD_DIR }}" -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON -DVCPKG_MANIFEST_DIR="${{ matrix.ossl3-vcpkg-dir }}" -DCMAKE_TOOLCHAIN_FILE="${{ matrix.vcpkg-cmake-file}}" . - - - name: Build - run: | - cmake --build "${{ env.CMAKE_BUILD_DIR }}" - - - name: Unit tests - run: | - cmake --build "${{ env.CMAKE_BUILD_DIR }}" --target "${{ matrix.ctest-target}}" diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml deleted file mode 100644 index b4525d2c3..000000000 --- a/.github/workflows/compat.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build for older MacOS - -on: - push: - branches: - - main - pull_request: - branches: - - main - -env: - CTEST_OUTPUT_ON_FAILURE: 1 - MACOSX_DEPLOYMENT_TARGET: 10.11 - -jobs: - build: - runs-on: macos-latest - - env: - CMAKE_BUILD_DIR: ${{ github.workspace }}/build - TOOLCHAIN_FILE: $VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake - - steps: - - uses: actions/checkout@v2 - - - name: dependencies - run: | - brew install llvm pkg-config - ln -s "/usr/local/opt/llvm/bin/clang-format" "/usr/local/bin/clang-format" - ln -s "/usr/local/opt/llvm/bin/clang-tidy" "/usr/local/bin/clang-tidy" - - - name: Restore cache - uses: actions/cache@v2 - with: - path: | - ${{ env.CMAKE_BUILD_DIR }}/vcpkg_installed - key: ${{ runner.os }}-${{ hashFiles( '**/vcpkg.json' ) }} - - - name: build the library - run: | - cmake -B "${{ env.CMAKE_BUILD_DIR }}" -DCMAKE_TOOLCHAIN_FILE="${{ env.TOOLCHAIN_FILE }}" . - cmake --build "${{ env.CMAKE_BUILD_DIR }}" --target mlspp diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml deleted file mode 100644 index deecc25be..000000000 --- a/.github/workflows/interop.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Test interoperability - -on: - push: - branches: - - main - pull_request: - branches: - - main - -env: - CTEST_OUTPUT_ON_FAILURE: 1 - -jobs: - build: - runs-on: ubuntu-latest - - env: - CMAKE_BUILD_DIR: ${{ github.workspace }}/build - TOOLCHAIN_FILE: $VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake - - steps: - - uses: actions/checkout@v2 - - - name: Restore cache - uses: actions/cache@v2 - with: - path: | - ${{ env.CMAKE_BUILD_DIR }}/vcpkg_installed - key: ${{ runner.os }}-${{ hashFiles( '**/vcpkg.json' ) }} - - - name: Build the library - run: | - cmake -B build -DCMAKE_TOOLCHAIN_FILE="${{ env.TOOLCHAIN_FILE }}" . - cmake --build build - - - name: Build interop harness - run: | - cd cmd/interop - cmake -B build -DSANITIZERS=ON -DCMAKE_TOOLCHAIN_FILE="${{ env.TOOLCHAIN_FILE }}" . - cmake --build build - - - name: Test self-interop - run: | - make -C cmd/interop self-test - - - name: Test interop on test vectors - run: | - make -C cmd/interop interop-test - - - name: Test gRPC live interop with self - run: | - cd cmd/interop - ./grpc-self-test.sh diff --git a/.github/workflows/main_ci.yml b/.github/workflows/main_ci.yml new file mode 100644 index 000000000..8f6c31686 --- /dev/null +++ b/.github/workflows/main_ci.yml @@ -0,0 +1,189 @@ +name: MLSPP CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + CTEST_OUTPUT_ON_FAILURE: 1 + CMAKE_BUILD_DIR: ${{ github.workspace }}/build + CMAKE_BUILD_OPENSSL3_DIR: ${{ github.workspace }}/build_openssl3 + CMAKE_TEST_OPENSSL3_DIR: ${{ github.workspace }}/build_openssl3/test + CMAKE_TEST_DIR: ${{ github.workspace }}/build/test + VCPKG_BINARY_SOURCES: files,${{ github.workspace }}/build/cache,readwrite + +jobs: + formatting-check: + name: Formatting Check + runs-on: ubuntu-latest + strategy: + matrix: + path: + - 'include' + - 'src' + - 'test' + - 'cmd' + - 'lib' + steps: + - uses: actions/checkout@v3 + + - name: Run clang-format style check for C/C++ programs + uses: jidicula/clang-format-action@v4.11.0 + with: + clang-format-version: '16' + check-path: ${{ matrix.path }} + fallback-style: 'Mozilla' + + quick-linux-interop-check: + needs: formatting-check + name: Quick Linux Check and Interop + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Dependencies (Ubuntu) + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + sudo apt-get install -y linux-headers-$(uname -r) + + - name: Restore cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/build/cache + key: VCPKG-BinaryCache-${{ runner.os }} + + - name: Build (OpenSSL 1.1) + run: | + cmake -B "${{ env.CMAKE_BUILD_DIR }}" -DTESTING=ON -DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" + cmake --build "${{ env.CMAKE_BUILD_DIR }}" --target all --parallel 2 + + - name: Unit Test (OpenSSL 1.1) + run: | + cmake --build "${{ env.CMAKE_BUILD_DIR }}" --target test + + - name: Build (Interop Harness) + run: | + cd cmd/interop + cmake -B build -DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" + cmake --build build + + - name: Test self-interop + run: | + make -C cmd/interop self-test + + - name: Test interop on test vectors + run: | + make -C cmd/interop interop-test + + - name: Test gRPC live interop with self + run: | + cd cmd/interop + ./grpc-self-test.sh + + - name: Build (OpenSSL 3) + run: | + cmake -B "${{ env.CMAKE_BUILD_OPENSSL3_DIR }}" -DTESTING=ON -DVCPKG_MANIFEST_DIR="alternatives/openssl_3" -DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" + cmake --build "${{ env.CMAKE_BUILD_OPENSSL3_DIR }}" + + - name: Unit Test (OpenSSL 3) + run: | + cmake --build "${{ env.CMAKE_BUILD_OPENSSL3_DIR }}" --target test + + platform-sanitizer-tests: + if: github.event.pull_request.draft == false + needs: quick-linux-interop-check + name: Build and test platforms using sanitizers and clang-tidy + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + include: + - os: windows-latest + vcpkg-cmake-file: "$env:VCPKG_INSTALLATION_ROOT\\scripts\\buildsystems\\vcpkg.cmake" + ossl3-vcpkg-dir: "alternatives\\openssl_3" + ctest-target: RUN_TESTS + - os: ubuntu-latest + vcpkg-cmake-file: "$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" + ossl3-vcpkg-dir: "alternatives/openssl_3" + ctest-target: test + - os: macos-latest + vcpkg-cmake-file: "$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" + ossl3-vcpkg-dir: "alternatives/openssl_3" + ctest-target: test + + steps: + - uses: actions/checkout@v3 + + - name: Dependencies (macOs) + if: ${{ matrix.os == 'macos-latest' }} + run: | + brew install llvm pkg-config + ln -s "/usr/local/opt/llvm/bin/clang-format" "/usr/local/bin/clang-format" + ln -s "/usr/local/opt/llvm/bin/clang-tidy" "/usr/local/bin/clang-tidy" + + - name: Dependencies (Ubuntu) + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + sudo apt-get install -y linux-headers-$(uname -r) + + - name: Restore cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/build/cache + key: VCPKG-BinaryCache-${{ runner.os }} + + - name: Build (OpenSSL1.1) + run: | + cmake -B "${{ env.CMAKE_BUILD_DIR }}" -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON -DCMAKE_TOOLCHAIN_FILE="${{ matrix.vcpkg-cmake-file}}" + cmake --build "${{ env.CMAKE_BUILD_DIR }}" --parallel 2 + + - name: Unit Test (OpenSSL1.1) + run: | + cmake --build "${{ env.CMAKE_BUILD_DIR }}" --target "${{ matrix.ctest-target}}" + + - name: Build (OpenSSL3) + run: | + cmake -B "${{ env.CMAKE_BUILD_OPENSSL3_DIR }}" -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON -DVCPKG_MANIFEST_DIR="${{ matrix.ossl3-vcpkg-dir }}" -DCMAKE_TOOLCHAIN_FILE="${{ matrix.vcpkg-cmake-file}}" + cmake --build "${{ env.CMAKE_BUILD_OPENSSL3_DIR }}" + + - name: Unit Test (OpenSSL3) + run: | + cmake --build "${{ env.CMAKE_BUILD_OPENSSL3_DIR }}" --target "${{ matrix.ctest-target}}" + + old-macos-compatibility: + if: github.event.pull_request.draft == false + needs: quick-linux-interop-check + name: Build for older MacOS + runs-on: macos-latest + + env: + CMAKE_BUILD_DIR: ${{ github.workspace }}/build + TOOLCHAIN_FILE: $VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake + VCPKG_BINARY_SOURCES: files,${{ github.workspace }}/build/cache,readwrite + MACOSX_DEPLOYMENT_TARGET: 10.11 + + steps: + - uses: actions/checkout@v3 + + - name: dependencies + run: | + brew install llvm pkg-config + ln -s "/usr/local/opt/llvm/bin/clang-format" "/usr/local/bin/clang-format" + ln -s "/usr/local/opt/llvm/bin/clang-tidy" "/usr/local/bin/clang-tidy" + + - name: Restore cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/build/cache + key: VCPKG-BinaryCache-${{ runner.os }} + + - name: Build + run: | + cmake -B "${{ env.CMAKE_BUILD_DIR }}" -DCMAKE_TOOLCHAIN_FILE="${{ env.TOOLCHAIN_FILE }}" + cmake --build "${{ env.CMAKE_BUILD_DIR }}" --target mlspp --parallel 2 + diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml deleted file mode 100644 index c4280cfee..000000000 --- a/.github/workflows/style.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Formatting Check -on: [push] -jobs: - formatting-check: - name: Formatting Check - runs-on: ubuntu-latest - strategy: - matrix: - path: - - 'include' - - 'src' - - 'test' - - 'cmd' - - 'lib' - steps: - - uses: actions/checkout@v2 - - name: Run clang-format style check for C/C++ programs. - uses: jidicula/clang-format-action@v4.6.2 - with: - clang-format-version: '14' - check-path: ${{ matrix.path }} diff --git a/.gitignore b/.gitignore index 4d0232c0a..d42f84a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ out *.swp .vs/** .vscode/** +include/namespace.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 755e3e773..2b0d82149 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,13 +8,33 @@ project(mlspp option(TESTING "Build tests" OFF) option(CLANG_TIDY "Perform linting with clang-tidy" OFF) option(SANITIZERS "Enable sanitizers" OFF) +option(MLS_NAMESPACE_SUFFIX "Namespace Suffix for CXX and CMake Export") + +if(MLS_NAMESPACE_SUFFIX) + set(MLS_CXX_NAMESPACE "mls_${MLS_NAMESPACE_SUFFIX}" CACHE STRING "Top-level Namespace for CXX") + set(MLS_EXPORT_NAMESPACE "MLSPP${MLS_NAMESPACE_SUFFIX}" CACHE STRING "Namespace for CMake Export") +else() + set(MLS_CXX_NAMESPACE "mls" CACHE STRING "Top-level Namespace for CXX") + set(MLS_EXPORT_NAMESPACE "MLSPP" CACHE STRING "Namespace for CMake Export") +endif() +message(STATUS "CXX Namespace: ${MLS_CXX_NAMESPACE}") +message(STATUS "CMake Export Namespace: ${MLS_EXPORT_NAMESPACE}") + ### ### Global Config ### set_property(GLOBAL PROPERTY USE_FOLDERS ON) +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/namespace.h.in" + "${CMAKE_CURRENT_SOURCE_DIR}/include/namespace.h" + @ONLY +) + include(CheckCXXCompilerFlag) +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -115,11 +135,13 @@ target_link_libraries(${LIB_NAME} bytes tls_syntax hpke) target_include_directories(${LIB_NAME} PUBLIC $ - $ + $ PRIVATE ${OPENSSL_INCLUDE_DIR} ) +install(TARGETS ${LIB_NAME} EXPORT mlspp-targets) + ### ### Tests ### @@ -131,5 +153,57 @@ endif() ### Exports ### set(CMAKE_EXPORT_PACKAGE_REGISTRY ON) -export(TARGETS mlspp tls_syntax hpke bytes mls_vectors third_party NAMESPACE MLSPP:: FILE MLSPPConfig.cmake) -export(PACKAGE MLSPP) +export( + EXPORT + mlspp-targets + NAMESPACE + ${MLS_EXPORT_NAMESPACE}:: + FILE + ${MLS_EXPORT_NAMESPACE}Targets.cmake) +export(PACKAGE ${MLS_EXPORT_NAMESPACE}) + +configure_package_config_file(cmake/config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/${MLS_EXPORT_NAMESPACE}Config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_DATADIR}/${MLS_EXPORT_NAMESPACE} + NO_SET_AND_CHECK_MACRO) + +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/${MLS_EXPORT_NAMESPACE}ConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) + +### +### Install +### + +install( + EXPORT + mlspp-targets + NAMESPACE + ${MLS_EXPORT_NAMESPACE}:: + FILE + ${MLS_EXPORT_NAMESPACE}Targets.cmake + DESTINATION + ${CMAKE_INSTALL_DATADIR}/${MLS_EXPORT_NAMESPACE}) + +install( + DIRECTORY + include/ + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}) + +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/${MLS_EXPORT_NAMESPACE}Config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/${MLS_EXPORT_NAMESPACE}ConfigVersion.cmake + DESTINATION + ${CMAKE_INSTALL_DATADIR}/${MLS_EXPORT_NAMESPACE}) + +install( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE + DESTINATION + ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME} + RENAME + copyright) + diff --git a/Makefile b/Makefile index 29745db09..206d326e0 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ BUILD_DIR=build TEST_DIR=build/test CLANG_FORMAT=clang-format -i CLANG_TIDY=OFF +OPENSSL3_MANIFEST=alternatives/openssl_3 -.PHONY: all dev test ctest dtest dbtest libs test-libs test-all everything ci clean cclean format +.PHONY: all dev dev3 test ctest dtest dbtest libs test-libs test-all everything ci ci3 clean cclean format all: ${BUILD_DIR} cmake --build ${BUILD_DIR} --target mlspp @@ -22,6 +23,10 @@ dev: # too slow, and we can run them in CI cmake -B${BUILD_DIR} -DTESTING=ON -DCMAKE_BUILD_TYPE=Debug . +dev3: + # Like `dev`, but using OpenSSL 3 + cmake -B${BUILD_DIR} -DTESTING=ON -DCMAKE_BUILD_TYPE=Debug -DVCPKG_MANIFEST_DIR=${OPENSSL3_MANIFEST} . + test: ${BUILD_DIR} test/* cmake --build ${BUILD_DIR} --target mlspp_test @@ -53,7 +58,12 @@ everything: ${BUILD_DIR} ci: cmake -B ${BUILD_DIR} -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON \ - -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE="${VCPKG_TOOLCHAIN_FILE}" . + -DCMAKE_BUILD_TYPE=Debug . + +ci3: + # Like `ci`, but using OpenSSL 3 + cmake -B ${BUILD_DIR} -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON \ + -DCMAKE_BUILD_TYPE=Debug -DVCPKG_MANIFEST_DIR=${OPENSSL3_MANIFEST} . clean: cmake --build ${BUILD_DIR} --target clean @@ -65,4 +75,5 @@ format: find include -iname "*.h" -or -iname "*.cpp" | xargs ${CLANG_FORMAT} find src -iname "*.h" -or -iname "*.cpp" | xargs ${CLANG_FORMAT} find test -iname "*.h" -or -iname "*.cpp" | xargs ${CLANG_FORMAT} + find cmd -iname "*.h" -or -iname "*.cpp" | xargs ${CLANG_FORMAT} find lib -iname "*.h" -or -iname "*.cpp" | grep -v "test_vectors.cpp" | xargs ${CLANG_FORMAT} diff --git a/README.md b/README.md index 144f3df80..c3089d271 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![MLSPP CI](https://github.com/cisco/mlspp/actions/workflows/main_ci.yml/badge.svg)](https://github.com/cisco/mlspp/actions/workflows/main_ci.yml) + MLS++ ===== diff --git a/alternatives/openssl_3/vcpkg.json b/alternatives/openssl_3/vcpkg.json index 4b4d76574..97c9cc3e8 100644 --- a/alternatives/openssl_3/vcpkg.json +++ b/alternatives/openssl_3/vcpkg.json @@ -7,7 +7,8 @@ "name": "openssl", "version>=": "3.0.7" }, - "doctest" + "doctest", + "nlohmann-json" ], "builtin-baseline": "5908d702d61cea1429b223a0b7a10ab86bad4c78", "overrides": [ diff --git a/cmake/config.cmake.in b/cmake/config.cmake.in new file mode 100644 index 000000000..82145af77 --- /dev/null +++ b/cmake/config.cmake.in @@ -0,0 +1,4 @@ +@PACKAGE_INIT@ + +include(${CMAKE_CURRENT_LIST_DIR}/@MLS_EXPORT_NAMESPACE@Targets.cmake) +check_required_components(mlspp) diff --git a/cmake/namespace.h.in b/cmake/namespace.h.in new file mode 100644 index 000000000..79dfad2cf --- /dev/null +++ b/cmake/namespace.h.in @@ -0,0 +1,4 @@ +#pragma once + +// Configurable top-level MLS namespace +#define MLS_NAMESPACE @MLS_CXX_NAMESPACE@ diff --git a/cmd/interop/Makefile b/cmd/interop/Makefile index 78d6d6420..3c505c861 100644 --- a/cmd/interop/Makefile +++ b/cmd/interop/Makefile @@ -34,10 +34,10 @@ interop-test: ${BUILD_DIR}/${APP_NAME} ./${BUILD_DIR}/${APP_NAME} -ver 9 <${TEST_VECTOR_DIR}/welcome.json ./${BUILD_DIR}/${APP_NAME} -ver 10 <${TEST_VECTOR_DIR}/tree-operations.json ./${BUILD_DIR}/${APP_NAME} -ver 11 <${TEST_VECTOR_DIR}/treekem.json - # TODO(RLB) Uncomment once the messages test vectors are fixed - #./${BUILD_DIR}/${APP_NAME} -ver 12 <${TEST_VECTOR_DIR}/messages.json + ./${BUILD_DIR}/${APP_NAME} -ver 12 <${TEST_VECTOR_DIR}/messages.json ./${BUILD_DIR}/${APP_NAME} -ver 13 <${TEST_VECTOR_DIR}/passive-client-welcome.json ./${BUILD_DIR}/${APP_NAME} -ver 13 <${TEST_VECTOR_DIR}/passive-client-handling-commit.json + ./${BUILD_DIR}/${APP_NAME} -ver 13 <${TEST_VECTOR_DIR}/passive-client-random.json format: clang-format -i -style=Mozilla src/*.cpp src/*.h diff --git a/cmd/interop/src/json_details.h b/cmd/interop/src/json_details.h index 95c479411..bac888b9a 100644 --- a/cmd/interop/src/json_details.h +++ b/cmd/interop/src/json_details.h @@ -43,7 +43,8 @@ struct adl_serializer> }; // LeafCount, NodeCount, etc. -// XXX(RLB): For some reason, just defining this for mls::Uint32 didn't work. +// XXX(RLB): For some reason, just defining this for MLS_NAMESPACE::Uint32 +// didn't work. template struct uint_serializer { @@ -54,26 +55,25 @@ struct uint_serializer #define UINT_SERIALIZER(T) \ template<> \ struct adl_serializer : uint_serializer \ - { \ - }; + {}; -UINT_SERIALIZER(mls::LeafCount) -UINT_SERIALIZER(mls::NodeCount) -UINT_SERIALIZER(mls::LeafIndex) -UINT_SERIALIZER(mls::NodeIndex) +UINT_SERIALIZER(MLS_NAMESPACE::LeafCount) +UINT_SERIALIZER(MLS_NAMESPACE::NodeCount) +UINT_SERIALIZER(MLS_NAMESPACE::LeafIndex) +UINT_SERIALIZER(MLS_NAMESPACE::NodeIndex) -// mls::Ciphersuite +// MLS_NAMESPACE::Ciphersuite template<> -struct adl_serializer +struct adl_serializer { - static void to_json(json& j, const mls::CipherSuite& v) + static void to_json(json& j, const MLS_NAMESPACE::CipherSuite& v) { j = v.cipher_suite(); } - static void from_json(const json& j, mls::CipherSuite& v) + static void from_json(const json& j, MLS_NAMESPACE::CipherSuite& v) { - v = mls::CipherSuite(j.get()); + v = MLS_NAMESPACE::CipherSuite(j.get()); } }; @@ -94,43 +94,44 @@ struct asymmetric_key_serializer #define ASYMM_KEY_SERIALIZER(T) \ template<> \ struct adl_serializer : asymmetric_key_serializer \ - { \ - }; + {}; -ASYMM_KEY_SERIALIZER(mls::HPKEPublicKey) -ASYMM_KEY_SERIALIZER(mls::HPKEPrivateKey) -ASYMM_KEY_SERIALIZER(mls::SignaturePublicKey) -ASYMM_KEY_SERIALIZER(mls::SignaturePrivateKey) +ASYMM_KEY_SERIALIZER(MLS_NAMESPACE::HPKEPublicKey) +ASYMM_KEY_SERIALIZER(MLS_NAMESPACE::HPKEPrivateKey) +ASYMM_KEY_SERIALIZER(MLS_NAMESPACE::SignaturePublicKey) +ASYMM_KEY_SERIALIZER(MLS_NAMESPACE::SignaturePrivateKey) // Other TLS-serializable things template struct tls_serializer { - static void to_json(json& j, const T& v) { j = bytes(tls::marshal(v)); } + static void to_json(json& j, const T& v) + { + j = bytes(MLS_NAMESPACE::tls::marshal(v)); + } static void from_json(const json& j, T& v) { - v = tls::get(j.get()); + v = MLS_NAMESPACE::tls::get(j.get()); } }; #define TLS_SERIALIZER(T) \ template<> \ struct adl_serializer : tls_serializer \ - { \ - }; - -TLS_SERIALIZER(mls::TreeKEMPublicKey) -TLS_SERIALIZER(mls::AuthenticatedContent) -TLS_SERIALIZER(mls::Credential) -TLS_SERIALIZER(mls::Proposal) -TLS_SERIALIZER(mls::Commit) -TLS_SERIALIZER(mls::ApplicationData) -TLS_SERIALIZER(mls::MLSMessage) -TLS_SERIALIZER(mls::LeafNode) -TLS_SERIALIZER(mls::UpdatePath) -TLS_SERIALIZER(mls::KeyPackage) -TLS_SERIALIZER(mls::Welcome) + {}; + +TLS_SERIALIZER(MLS_NAMESPACE::TreeKEMPublicKey) +TLS_SERIALIZER(MLS_NAMESPACE::AuthenticatedContent) +TLS_SERIALIZER(MLS_NAMESPACE::Credential) +TLS_SERIALIZER(MLS_NAMESPACE::Proposal) +TLS_SERIALIZER(MLS_NAMESPACE::Commit) +TLS_SERIALIZER(MLS_NAMESPACE::ApplicationData) +TLS_SERIALIZER(MLS_NAMESPACE::MLSMessage) +TLS_SERIALIZER(MLS_NAMESPACE::LeafNode) +TLS_SERIALIZER(MLS_NAMESPACE::UpdatePath) +TLS_SERIALIZER(MLS_NAMESPACE::KeyPackage) +TLS_SERIALIZER(MLS_NAMESPACE::Welcome) } // namespace nlohmann @@ -290,10 +291,13 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WelcomeTestVector, welcome) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TreeOperationsTestVector, + cipher_suite, tree_before, + tree_hash_before, proposal, proposal_sender, - tree_after) + tree_after, + tree_hash_after) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TreeKEMTestVector::PathSecret, node, diff --git a/cmd/interop/src/main.cpp b/cmd/interop/src/main.cpp index 0ef89714d..4864c9826 100644 --- a/cmd/interop/src/main.cpp +++ b/cmd/interop/src/main.cpp @@ -48,7 +48,7 @@ make_test_vector(uint64_t type) case TestVectorClass::crypto_basics: { auto cases = std::vector(); - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { cases.emplace_back(suite); } @@ -59,7 +59,7 @@ make_test_vector(uint64_t type) auto cases = std::vector(); auto generations = std::vector{ 1, 15 }; - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { cases.emplace_back(suite, 15, generations); } @@ -69,7 +69,7 @@ make_test_vector(uint64_t type) case TestVectorClass::message_protection: { auto cases = std::vector(); - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { cases.emplace_back(suite); } @@ -79,7 +79,7 @@ make_test_vector(uint64_t type) case TestVectorClass::key_schedule: { auto cases = std::vector(); - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { cases.emplace_back(suite, n); } @@ -89,7 +89,7 @@ make_test_vector(uint64_t type) case TestVectorClass::pre_shared_keys: { auto cases = std::vector(); - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { cases.emplace_back(suite, 5); } @@ -99,7 +99,7 @@ make_test_vector(uint64_t type) case TestVectorClass::tree_validation: { auto cases = std::vector(); - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { for (const auto& tree_structure : all_tree_structures) { cases.emplace_back(suite, tree_structure); } @@ -111,7 +111,7 @@ make_test_vector(uint64_t type) case TestVectorClass::transcript_hash: { auto cases = std::vector(); - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { cases.emplace_back(suite); } @@ -121,7 +121,7 @@ make_test_vector(uint64_t type) case TestVectorClass::welcome: { auto cases = std::vector(); - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { cases.emplace_back(suite); } @@ -131,7 +131,8 @@ make_test_vector(uint64_t type) case TestVectorClass::tree_modifications: { auto cases = std::vector(); - auto suite = mls::CipherSuite::ID::X25519_AES128GCM_SHA256_Ed25519; + auto suite = + MLS_NAMESPACE::CipherSuite::ID::X25519_AES128GCM_SHA256_Ed25519; for (auto scenario : TreeOperationsTestVector::all_scenarios) { cases.emplace_back(suite, scenario); } @@ -142,7 +143,7 @@ make_test_vector(uint64_t type) case TestVectorClass::treekem: { auto cases = std::vector(); - for (const auto& suite : mls::all_supported_suites) { + for (const auto& suite : MLS_NAMESPACE::all_supported_suites) { for (const auto& tree_structure : treekem_test_tree_structures) { cases.emplace_back(suite, tree_structure); } diff --git a/cmd/interop/src/mls_client_impl.cpp b/cmd/interop/src/mls_client_impl.cpp index ffaf45be5..8fcaaa2b9 100644 --- a/cmd/interop/src/mls_client_impl.cpp +++ b/cmd/interop/src/mls_client_impl.cpp @@ -4,7 +4,7 @@ using grpc::StatusCode; using nlohmann::json; -using namespace bytes_ns; +using namespace MLS_NAMESPACE::bytes_ns; static inline std::string bytes_to_string(const std::vector& data) @@ -19,9 +19,9 @@ string_to_bytes(const std::string& str) } static inline std::string -marshal_message(mls::MLSMessage&& msg) +marshal_message(MLS_NAMESPACE::MLSMessage&& msg) { - return bytes_to_string(tls::marshal(msg)); + return bytes_to_string(MLS_NAMESPACE::tls::marshal(msg)); } template @@ -29,14 +29,14 @@ T unmarshal_message(const std::string& str) { auto data = string_to_bytes(str); - auto msg = tls::get(data); + auto msg = MLS_NAMESPACE::tls::get(data); return var::get(msg.message); } -static inline mls::CipherSuite +static inline MLS_NAMESPACE::CipherSuite mls_suite(uint32_t suite_id) { - return static_cast(suite_id); + return static_cast(suite_id); } // Map C++ exceptions to gRPC errors @@ -84,7 +84,7 @@ MLSClientImpl::SupportedCiphersuites( SupportedCiphersuitesResponse* reply) { reply->clear_ciphersuites(); - for (const auto suite : mls::all_supported_suites) { + for (const auto suite : MLS_NAMESPACE::all_supported_suites) { reply->add_ciphersuites(static_cast(suite)); } return Status::OK; @@ -382,28 +382,30 @@ MLSClientImpl::HandleReInitWelcome(ServerContext* /* context */, // Factory for key packages MLSClientImpl::KeyPackageWithSecrets -MLSClientImpl::new_key_package(mls::CipherSuite cipher_suite, +MLSClientImpl::new_key_package(MLS_NAMESPACE::CipherSuite cipher_suite, const bytes& identity) { - auto init_priv = mls::HPKEPrivateKey::generate(cipher_suite); - auto encryption_priv = mls::HPKEPrivateKey::generate(cipher_suite); - auto signature_priv = mls::SignaturePrivateKey::generate(cipher_suite); - auto cred = mls::Credential::basic(identity); - - auto key_package = mls::KeyPackage(cipher_suite, - init_priv.public_key, - { - cipher_suite, - encryption_priv.public_key, - signature_priv.public_key, - cred, - mls::Capabilities::create_default(), - mls::Lifetime::create_default(), - {}, - signature_priv, - }, - {}, - signature_priv); + auto init_priv = MLS_NAMESPACE::HPKEPrivateKey::generate(cipher_suite); + auto encryption_priv = MLS_NAMESPACE::HPKEPrivateKey::generate(cipher_suite); + auto signature_priv = + MLS_NAMESPACE::SignaturePrivateKey::generate(cipher_suite); + auto cred = MLS_NAMESPACE::Credential::basic(identity); + + auto key_package = + MLS_NAMESPACE::KeyPackage(cipher_suite, + init_priv.public_key, + { + cipher_suite, + encryption_priv.public_key, + signature_priv.public_key, + cred, + MLS_NAMESPACE::Capabilities::create_default(), + MLS_NAMESPACE::Lifetime::create_default(), + {}, + signature_priv, + }, + {}, + signature_priv); return { init_priv, encryption_priv, signature_priv, key_package }; } @@ -445,7 +447,7 @@ MLSClientImpl::Free(ServerContext* /* context */, uint32_t MLSClientImpl::store_join(KeyPackageWithSecrets&& kp_priv) { - auto join_id = tls::get(kp_priv.key_package.ref()); + auto join_id = MLS_NAMESPACE::tls::get(kp_priv.key_package.ref()); auto entry = CachedJoin{ std::move(kp_priv), {} }; join_cache.emplace(std::make_pair(join_id, std::move(entry))); return join_id; @@ -461,7 +463,7 @@ MLSClientImpl::load_join(uint32_t join_id) } // Cached group state -mls::MessageOpts +MLS_NAMESPACE::MessageOpts MLSClientImpl::CachedState::message_opts() const { return { encrypt_handshake, {}, 0 }; @@ -475,21 +477,23 @@ MLSClientImpl::CachedState::reset_pending() } std::string -MLSClientImpl::CachedState::marshal(const mls::MLSMessage& msg) +MLSClientImpl::CachedState::marshal(const MLS_NAMESPACE::MLSMessage& msg) { - return bytes_to_string(tls::marshal(msg)); + return bytes_to_string(MLS_NAMESPACE::tls::marshal(msg)); } -mls::MLSMessage +MLS_NAMESPACE::MLSMessage MLSClientImpl::CachedState::unmarshal(const std::string& wire) { - return tls::get(string_to_bytes(wire)); + return MLS_NAMESPACE::tls::get( + string_to_bytes(wire)); } uint32_t -MLSClientImpl::store_state(mls::State&& state, bool encrypt_handshake) +MLSClientImpl::store_state(MLS_NAMESPACE::State&& state, bool encrypt_handshake) { - auto state_id = tls::get(state.epoch_authenticator()); + auto state_id = + MLS_NAMESPACE::tls::get(state.epoch_authenticator()); state_id += state.index().val; auto entry = CachedState{ std::move(state), encrypt_handshake, {}, {} }; @@ -507,9 +511,9 @@ MLSClientImpl::load_state(uint32_t state_id) } uint32_t -MLSClientImpl::store_signer(mls::SignaturePrivateKey&& priv) +MLSClientImpl::store_signer(MLS_NAMESPACE::SignaturePrivateKey&& priv) { - auto signer_id = tls::get(priv.public_key.data); + auto signer_id = MLS_NAMESPACE::tls::get(priv.public_key.data); auto entry = CachedSigner{ std::move(priv) }; signer_cache.emplace(std::make_pair(signer_id, std::move(entry))); return signer_id; @@ -525,7 +529,8 @@ MLSClientImpl::load_signer(uint32_t signer_id) } MLSClientImpl::CachedState* -MLSClientImpl::find_state(const bytes& group_id, const mls::epoch_t epoch) +MLSClientImpl::find_state(const bytes& group_id, + const MLS_NAMESPACE::epoch_t epoch) { auto entry = std::find_if( state_cache.begin(), state_cache.end(), [&](const auto& entry) { @@ -549,10 +554,10 @@ MLSClientImpl::remove_state(uint32_t state_id) uint32_t MLSClientImpl::store_reinit(KeyPackageWithSecrets&& kp_priv, - mls::State::Tombstone&& tombstone, + MLS_NAMESPACE::State::Tombstone&& tombstone, bool encrypt_handshake) { - auto reinit_id = tls::get(kp_priv.key_package.ref()); + auto reinit_id = MLS_NAMESPACE::tls::get(kp_priv.key_package.ref()); auto entry = CachedReInit{ std::move(kp_priv), std::move(tombstone), encrypt_handshake }; reinit_cache.emplace(std::make_pair(reinit_id, std::move(entry))); @@ -580,10 +585,11 @@ MLSClientImpl::group_context_extensions_proposal( const GroupContextExtensionsProposalRequest* request, ProposalResponse* response) { - auto ext_list = mls::ExtensionList{}; + auto ext_list = MLS_NAMESPACE::ExtensionList{}; for (int i = 0; i < request->extensions_size(); i++) { auto ext = request->extensions(i); - auto ext_type = static_cast(ext.extension_type()); + auto ext_type = + static_cast(ext.extension_type()); auto ext_data = string_to_bytes(ext.extension_data()); ext_list.add(ext_type, ext_data); } @@ -595,12 +601,12 @@ MLSClientImpl::group_context_extensions_proposal( return Status::OK; } -mls::LeafIndex -MLSClientImpl::find_member(const mls::TreeKEMPublicKey& tree, +MLS_NAMESPACE::LeafIndex +MLSClientImpl::find_member(const MLS_NAMESPACE::TreeKEMPublicKey& tree, const std::string& identity) { const auto id = string_to_bytes(identity); - auto index = mls::LeafIndex{ 0 }; + auto index = MLS_NAMESPACE::LeafIndex{ 0 }; for (; index < tree.size; index.val++) { const auto maybe_leaf = tree.leaf_node(index); if (!maybe_leaf) { @@ -608,7 +614,7 @@ MLSClientImpl::find_member(const mls::TreeKEMPublicKey& tree, } const auto& leaf = opt::get(maybe_leaf); - const auto& basic = leaf.credential.get(); + const auto& basic = leaf.credential.get(); if (basic.identity == id) { break; } @@ -621,60 +627,63 @@ MLSClientImpl::find_member(const mls::TreeKEMPublicKey& tree, return index; } -mls::Proposal -MLSClientImpl::proposal_from_description(mls::CipherSuite suite, - const bytes& group_id, - const mls::TreeKEMPublicKey& tree, - const ProposalDescription& desc) +MLS_NAMESPACE::Proposal +MLSClientImpl::proposal_from_description( + MLS_NAMESPACE::CipherSuite suite, + const bytes& group_id, + const MLS_NAMESPACE::TreeKEMPublicKey& tree, + const ProposalDescription& desc) { if (desc.proposal_type() == "add") { const auto kp_msg_data = string_to_bytes(desc.key_package()); - const auto kp_msg = tls::get(kp_msg_data); - const auto kp = var::get(kp_msg.message); - return { mls::Add{ kp } }; + const auto kp_msg = + MLS_NAMESPACE::tls::get(kp_msg_data); + const auto kp = var::get(kp_msg.message); + return { MLS_NAMESPACE::Add{ kp } }; } if (desc.proposal_type() == "remove") { const auto removed_index = find_member(tree, desc.removed_id()); - return { mls::Remove{ removed_index } }; + return { MLS_NAMESPACE::Remove{ removed_index } }; } if (desc.proposal_type() == "externalPSK") { const auto external_psk_id = string_to_bytes(desc.psk_id()); - const auto psk_id = mls::PreSharedKeyID{ - { mls::ExternalPSK{ external_psk_id } }, - mls::random_bytes(suite.secret_size()), + const auto psk_id = MLS_NAMESPACE::PreSharedKeyID{ + { MLS_NAMESPACE::ExternalPSK{ external_psk_id } }, + MLS_NAMESPACE::random_bytes(suite.secret_size()), }; - return { mls::PreSharedKey{ psk_id } }; + return { MLS_NAMESPACE::PreSharedKey{ psk_id } }; } if (desc.proposal_type() == "resumptionPSK") { const auto epoch = desc.epoch_id(); - const auto psk_id = mls::PreSharedKeyID{ - { mls::ResumptionPSK{ - mls::ResumptionPSKUsage::application, group_id, epoch } }, - mls::random_bytes(suite.secret_size()), + const auto psk_id = MLS_NAMESPACE::PreSharedKeyID{ + { MLS_NAMESPACE::ResumptionPSK{ + MLS_NAMESPACE::ResumptionPSKUsage::application, group_id, epoch } }, + MLS_NAMESPACE::random_bytes(suite.secret_size()), }; - return { mls::PreSharedKey{ psk_id } }; + return { MLS_NAMESPACE::PreSharedKey{ psk_id } }; } - auto ext_list = mls::ExtensionList{}; + auto ext_list = MLS_NAMESPACE::ExtensionList{}; for (int i = 0; i < desc.extensions_size(); i++) { auto ext = desc.extensions(i); - auto ext_type = static_cast(ext.extension_type()); + auto ext_type = + static_cast(ext.extension_type()); auto ext_data = string_to_bytes(ext.extension_data()); ext_list.add(ext_type, ext_data); } if (desc.proposal_type() == "groupContextExtensions") { - return { mls::GroupContextExtensions{ ext_list } }; + return { MLS_NAMESPACE::GroupContextExtensions{ ext_list } }; } if (desc.proposal_type() == "reinit") { - return { mls::ReInit{ + return { MLS_NAMESPACE::ReInit{ string_to_bytes(desc.group_id()), - mls::ProtocolVersion::mls10, - static_cast(desc.cipher_suite()), + MLS_NAMESPACE::ProtocolVersion::mls10, + static_cast(desc.cipher_suite()), ext_list } }; } @@ -690,23 +699,23 @@ MLSClientImpl::create_group(const CreateGroupRequest* request, auto cipher_suite = mls_suite(request->cipher_suite()); auto identity = string_to_bytes(request->identity()); - auto leaf_priv = mls::HPKEPrivateKey::generate(cipher_suite); - auto sig_priv = mls::SignaturePrivateKey::generate(cipher_suite); - auto cred = mls::Credential::basic(identity); + auto leaf_priv = MLS_NAMESPACE::HPKEPrivateKey::generate(cipher_suite); + auto sig_priv = MLS_NAMESPACE::SignaturePrivateKey::generate(cipher_suite); + auto cred = MLS_NAMESPACE::Credential::basic(identity); - auto leaf_node = mls::LeafNode{ + auto leaf_node = MLS_NAMESPACE::LeafNode{ cipher_suite, leaf_priv.public_key, sig_priv.public_key, cred, - mls::Capabilities::create_default(), - mls::Lifetime::create_default(), + MLS_NAMESPACE::Capabilities::create_default(), + MLS_NAMESPACE::Lifetime::create_default(), {}, sig_priv, }; - auto state = - mls::State(group_id, cipher_suite, leaf_priv, sig_priv, leaf_node, {}); + auto state = MLS_NAMESPACE::State( + group_id, cipher_suite, leaf_priv, sig_priv, leaf_node, {}); auto state_id = store_state(std::move(state), request->encrypt_handshake()); response->set_state_id(state_id); @@ -726,7 +735,8 @@ MLSClientImpl::create_key_package(const CreateKeyPackageRequest* request, response->set_encryption_priv(bytes_to_string(kp_priv.encryption_priv.data)); response->set_signature_priv(bytes_to_string(kp_priv.signature_priv.data)); - auto key_package = tls::marshal(mls::MLSMessage{ kp_priv.key_package }); + auto key_package = MLS_NAMESPACE::tls::marshal( + MLS_NAMESPACE::MLSMessage{ kp_priv.key_package }); response->set_key_package(bytes_to_string(key_package)); auto join_id = store_join(std::move(kp_priv)); @@ -744,20 +754,21 @@ MLSClientImpl::join_group(const JoinGroupRequest* request, return Status(StatusCode::INVALID_ARGUMENT, "Unknown transaction ID"); } - auto welcome = unmarshal_message(request->welcome()); - auto ratchet_tree = std::optional{}; + auto welcome = unmarshal_message(request->welcome()); + auto ratchet_tree = std::optional{}; auto ratchet_tree_data = string_to_bytes(request->ratchet_tree()); if (!ratchet_tree_data.empty()) { - ratchet_tree = tls::get(ratchet_tree_data); + ratchet_tree = MLS_NAMESPACE::tls::get( + ratchet_tree_data); } - auto state = mls::State(join->kp_priv.init_priv, - std::move(join->kp_priv.encryption_priv), - std::move(join->kp_priv.signature_priv), - join->kp_priv.key_package, - welcome, - ratchet_tree, - join->external_psks); + auto state = MLS_NAMESPACE::State(join->kp_priv.init_priv, + std::move(join->kp_priv.encryption_priv), + std::move(join->kp_priv.signature_priv), + join->kp_priv.key_package, + welcome, + ratchet_tree, + join->external_psks); auto epoch_authenticator = state.epoch_authenticator(); auto state_id = store_state(std::move(state), request->encrypt_handshake()); @@ -772,45 +783,47 @@ MLSClientImpl::external_join(const ExternalJoinRequest* request, ExternalJoinResponse* response) { const auto group_info_msg = - unmarshal_message(request->group_info()); + unmarshal_message(request->group_info()); const auto suite = group_info_msg.group_context.cipher_suite; - auto init_priv = mls::HPKEPrivateKey::generate(suite); - auto leaf_priv = mls::HPKEPrivateKey::generate(suite); - auto sig_priv = mls::SignaturePrivateKey::generate(suite); + auto init_priv = MLS_NAMESPACE::HPKEPrivateKey::generate(suite); + auto leaf_priv = MLS_NAMESPACE::HPKEPrivateKey::generate(suite); + auto sig_priv = MLS_NAMESPACE::SignaturePrivateKey::generate(suite); auto identity = string_to_bytes(request->identity()); - auto cred = mls::Credential::basic(identity); + auto cred = MLS_NAMESPACE::Credential::basic(identity); - auto leaf = mls::LeafNode{ + auto leaf = MLS_NAMESPACE::LeafNode{ suite, leaf_priv.public_key, sig_priv.public_key, cred, - mls::Capabilities::create_default(), - mls::Lifetime::create_default(), + MLS_NAMESPACE::Capabilities::create_default(), + MLS_NAMESPACE::Lifetime::create_default(), {}, sig_priv, }; - auto kp = mls::KeyPackage(suite, init_priv.public_key, leaf, {}, sig_priv); + auto kp = + MLS_NAMESPACE::KeyPackage(suite, init_priv.public_key, leaf, {}, sig_priv); // Import an external tree if present - auto ratchet_tree = std::optional{}; + auto ratchet_tree = std::optional{}; auto ratchet_tree_data = string_to_bytes(request->ratchet_tree()); if (!ratchet_tree_data.empty()) { - ratchet_tree = tls::get(ratchet_tree_data); + ratchet_tree = MLS_NAMESPACE::tls::get( + ratchet_tree_data); } // If required, find our prior appearance and remove it - auto remove_prior = std::optional{}; + auto remove_prior = std::optional{}; if (request->remove_prior()) { // Find the tree we're going to look at // XXX(RLB): This replicates logic in State::import_tree, but we need to // do it out here since this is where the knowledge of which leaf to // remove resides. - auto tree = mls::TreeKEMPublicKey(suite); + auto tree = MLS_NAMESPACE::TreeKEMPublicKey(suite); auto maybe_tree_extn = - group_info_msg.extensions.find(); + group_info_msg.extensions.find(); if (ratchet_tree) { tree = opt::get(ratchet_tree); } else if (maybe_tree_extn) { @@ -820,14 +833,14 @@ MLSClientImpl::external_join(const ExternalJoinRequest* request, } // Scan through to find a matching identity - for (auto i = mls::LeafIndex{ 0 }; i < tree.size; i.val++) { + for (auto i = MLS_NAMESPACE::LeafIndex{ 0 }; i < tree.size; i.val++) { const auto maybe_leaf = tree.leaf_node(i); if (!maybe_leaf) { continue; } const auto& leaf = opt::get(maybe_leaf); - const auto& cred = leaf.credential.get(); + const auto& cred = leaf.credential.get(); if (cred.identity != identity) { continue; } @@ -850,15 +863,15 @@ MLSClientImpl::external_join(const ExternalJoinRequest* request, } auto encrypt = request->encrypt_handshake(); - auto leaf_secret = mls::random_bytes(suite.secret_size()); - auto [commit, state] = mls::State::external_join(leaf_secret, - sig_priv, - kp, - group_info_msg, - ratchet_tree, - { {}, encrypt, 0 }, - remove_prior, - psks); + auto leaf_secret = MLS_NAMESPACE::random_bytes(suite.secret_size()); + auto [commit, state] = MLS_NAMESPACE::State::external_join(leaf_secret, + sig_priv, + kp, + group_info_msg, + ratchet_tree, + { {}, encrypt, 0 }, + remove_prior, + psks); auto epoch_authenticator = state.epoch_authenticator(); auto state_id = store_state(std::move(state), encrypt); @@ -880,7 +893,8 @@ MLSClientImpl::group_info(CachedState& entry, response->set_group_info(marshal_message(group_info)); if (!inline_tree) { - auto ratchet_tree = bytes_to_string(tls::marshal(entry.state.tree())); + auto ratchet_tree = + bytes_to_string(MLS_NAMESPACE::tls::marshal(entry.state.tree())); response->set_ratchet_tree(ratchet_tree); } @@ -928,11 +942,12 @@ MLSClientImpl::unprotect(CachedState& entry, UnprotectResponse* response) { auto ct_data = string_to_bytes(request->ciphertext()); - auto ct = tls::get(ct_data); + auto ct = MLS_NAMESPACE::tls::get(ct_data); // Locate the right epoch to decrypt with const auto group_id = entry.state.group_id(); - const auto epoch = var::get(ct.message).get_epoch(); + const auto epoch = + var::get(ct.message).get_epoch(); auto* state = &entry.state; if (entry.state.epoch() != epoch) { @@ -958,7 +973,8 @@ MLSClientImpl::add_proposal(CachedState& entry, const AddProposalRequest* request, ProposalResponse* response) { - auto key_package = unmarshal_message(request->key_package()); + auto key_package = + unmarshal_message(request->key_package()); auto message = entry.state.add(key_package, entry.message_opts()); response->set_proposal(entry.marshal(message)); @@ -970,7 +986,8 @@ MLSClientImpl::update_proposal(CachedState& entry, const UpdateProposalRequest* /* request */, ProposalResponse* response) { - auto leaf_priv = mls::HPKEPrivateKey::generate(entry.state.cipher_suite()); + auto leaf_priv = + MLS_NAMESPACE::HPKEPrivateKey::generate(entry.state.cipher_suite()); auto message = entry.state.update(leaf_priv, {}, entry.message_opts()); response->set_proposal(entry.marshal(message)); @@ -1035,7 +1052,7 @@ MLSClientImpl::commit(CachedState& entry, } // Create by-value proposals - auto by_value = std::vector(); + auto by_value = std::vector(); for (int i = 0; i < request->by_value_size(); i++) { const auto desc = request->by_value(i); const auto proposal = proposal_from_description(entry.state.cipher_suite(), @@ -1049,14 +1066,15 @@ MLSClientImpl::commit(CachedState& entry, auto inline_tree = !request->external_tree(); auto leaf_secret = - mls::random_bytes(entry.state.cipher_suite().secret_size()); - auto [commit, welcome, next] = - entry.state.commit(leaf_secret, - mls::CommitOpts{ by_value, inline_tree, force_path, {} }, - entry.message_opts()); + MLS_NAMESPACE::random_bytes(entry.state.cipher_suite().secret_size()); + auto [commit, welcome, next] = entry.state.commit( + leaf_secret, + MLS_NAMESPACE::CommitOpts{ by_value, inline_tree, force_path, {} }, + entry.message_opts()); if (!inline_tree) { - auto ratchet_tree = bytes_to_string(tls::marshal(next.tree())); + auto ratchet_tree = + bytes_to_string(MLS_NAMESPACE::tls::marshal(next.tree())); response->set_ratchet_tree(ratchet_tree); } @@ -1143,8 +1161,9 @@ MLSClientImpl::new_member_add_proposal( NewMemberAddProposalResponse* response) { auto group_info_msg_data = string_to_bytes(request->group_info()); - auto group_info_msg = tls::get(group_info_msg_data); - auto group_info = var::get(group_info_msg.message); + auto group_info_msg = + MLS_NAMESPACE::tls::get(group_info_msg_data); + auto group_info = var::get(group_info_msg.message); auto cipher_suite = group_info.group_context.cipher_suite; auto group_id = group_info.group_context.group_id; @@ -1158,7 +1177,7 @@ MLSClientImpl::new_member_add_proposal( response->set_encryption_priv(bytes_to_string(kp_priv.encryption_priv.data)); response->set_signature_priv(bytes_to_string(kp_priv.signature_priv.data)); - auto proposal = mls::State::new_member_add( + auto proposal = MLS_NAMESPACE::State::new_member_add( group_id, epoch, kp_priv.key_package, kp_priv.signature_priv); response->set_proposal(marshal_message(std::move(proposal))); @@ -1176,12 +1195,14 @@ MLSClientImpl::create_external_signer( const auto cipher_suite = mls_suite(request->cipher_suite()); const auto identity = string_to_bytes(request->identity()); - auto signature_priv = mls::SignaturePrivateKey::generate(cipher_suite); - const auto cred = mls::Credential::basic(identity); + auto signature_priv = + MLS_NAMESPACE::SignaturePrivateKey::generate(cipher_suite); + const auto cred = MLS_NAMESPACE::Credential::basic(identity); const auto external_sender = - mls::ExternalSender{ signature_priv.public_key, cred }; - response->set_external_sender(bytes_to_string(tls::marshal(external_sender))); + MLS_NAMESPACE::ExternalSender{ signature_priv.public_key, cred }; + response->set_external_sender( + bytes_to_string(MLS_NAMESPACE::tls::marshal(external_sender))); const auto signer_id = store_signer(std::move(signature_priv)); response->set_signer_id(signer_id); @@ -1195,11 +1216,13 @@ MLSClientImpl::add_external_signer(CachedState& entry, ProposalResponse* response) { auto external_sender_data = string_to_bytes(request->external_sender()); - auto external_sender = tls::get(external_sender_data); + auto external_sender = MLS_NAMESPACE::tls::get( + external_sender_data); auto ext_list = entry.state.extensions(); - auto ext_senders = mls::ExternalSendersExtension{}; - auto curr_ext_senders = ext_list.find(); + auto ext_senders = MLS_NAMESPACE::ExternalSendersExtension{}; + auto curr_ext_senders = + ext_list.find(); if (curr_ext_senders) { ext_senders = opt::get(curr_ext_senders); } @@ -1219,15 +1242,17 @@ MLSClientImpl::external_signer_proposal( ProposalResponse* response) { auto group_info_msg_data = string_to_bytes(request->group_info()); - auto group_info_msg = tls::get(group_info_msg_data); - auto group_info = var::get(group_info_msg.message); + auto group_info_msg = + MLS_NAMESPACE::tls::get(group_info_msg_data); + auto group_info = var::get(group_info_msg.message); auto cipher_suite = group_info.group_context.cipher_suite; auto group_id = group_info.group_context.group_id; auto epoch = group_info.group_context.epoch; auto ratchet_tree_data = string_to_bytes(request->ratchet_tree()); - auto ratchet_tree = tls::get(ratchet_tree_data); + auto ratchet_tree = + MLS_NAMESPACE::tls::get(ratchet_tree_data); // Look up the signer auto* signer = load_signer(request->signer_id()); @@ -1237,7 +1262,8 @@ MLSClientImpl::external_signer_proposal( // Look up the signer index of this signer const auto maybe_ext_senders = - group_info.group_context.extensions.find(); + group_info.group_context.extensions + .find(); if (!maybe_ext_senders) { throw std::runtime_error("No external senders allowed"); } @@ -1256,12 +1282,13 @@ MLSClientImpl::external_signer_proposal( // Sign the proposal const auto proposal = proposal_from_description( cipher_suite, group_id, ratchet_tree, request->description()); - auto signed_proposal = mls::external_proposal(cipher_suite, - group_id, - epoch, - proposal, - signer_index, - signer->signature_priv); + auto signed_proposal = + MLS_NAMESPACE::external_proposal(cipher_suite, + group_id, + epoch, + proposal, + signer_index, + signer->signature_priv); response->set_proposal(marshal_message(std::move(signed_proposal))); return Status::OK; @@ -1273,13 +1300,14 @@ MLSClientImpl::reinit_proposal(CachedState& entry, ProposalResponse* response) { auto group_id = string_to_bytes(request->group_id()); - auto version = mls::ProtocolVersion::mls10; + auto version = MLS_NAMESPACE::ProtocolVersion::mls10; auto cipher_suite = mls_suite(request->cipher_suite()); - auto ext_list = mls::ExtensionList{}; + auto ext_list = MLS_NAMESPACE::ExtensionList{}; for (int i = 0; i < request->extensions_size(); i++) { auto ext = request->extensions(i); - auto ext_type = static_cast(ext.extension_type()); + auto ext_type = + static_cast(ext.extension_type()); auto ext_data = string_to_bytes(ext.extension_data()); ext_list.add(ext_type, ext_data); } @@ -1310,15 +1338,17 @@ MLSClientImpl::reinit_commit(CachedState& entry, } const auto leaf_secret = - mls::random_bytes(entry.state.cipher_suite().secret_size()); - const auto commit_opts = mls::CommitOpts{ {}, inline_tree, force_path, {} }; + MLS_NAMESPACE::random_bytes(entry.state.cipher_suite().secret_size()); + const auto commit_opts = + MLS_NAMESPACE::CommitOpts{ {}, inline_tree, force_path, {} }; auto [tombstone, commit] = entry.state.reinit_commit(leaf_secret, commit_opts, entry.message_opts()); // Cache the reinit const auto my_leaf = opt::get(entry.state.tree().leaf_node(entry.state.index())); - const auto identity = my_leaf.credential.get().identity; + const auto identity = + my_leaf.credential.get().identity; auto kp_priv = new_key_package(tombstone.reinit.cipher_suite, identity); const auto reinit_id = store_reinit( @@ -1380,7 +1410,8 @@ MLSClientImpl::handle_reinit_commit(CachedState& entry, // Cache the reinit const auto my_leaf = opt::get(entry.state.tree().leaf_node(entry.state.index())); - const auto identity = my_leaf.credential.get().identity; + const auto identity = + my_leaf.credential.get().identity; auto kp_priv = new_key_package(tombstone.reinit.cipher_suite, identity); const auto key_package = entry.marshal(kp_priv.key_package); @@ -1407,20 +1438,21 @@ MLSClientImpl::reinit_welcome(const ReInitWelcomeRequest* request, } // Import the KeyPackages - auto key_packages = std::vector{}; + auto key_packages = std::vector{}; for (int i = 0; i < request->key_package_size(); i++) { const auto key_package_msg_data = string_to_bytes(request->key_package(i)); const auto key_package_msg = - tls::get(key_package_msg_data); + MLS_NAMESPACE::tls::get(key_package_msg_data); key_packages.emplace_back( - var::get(key_package_msg.message)); + var::get(key_package_msg.message)); } // Create the Welcome const auto inline_tree = !request->external_tree(); const auto force_path = request->force_path(); const auto cipher_suite = reinit->tombstone.reinit.cipher_suite; - const auto leaf_secret = mls::random_bytes(cipher_suite.secret_size()); + const auto leaf_secret = + MLS_NAMESPACE::random_bytes(cipher_suite.secret_size()); auto [state, welcome] = reinit->tombstone.create_welcome(reinit->kp_priv.encryption_priv, reinit->kp_priv.signature_priv, @@ -1429,7 +1461,8 @@ MLSClientImpl::reinit_welcome(const ReInitWelcomeRequest* request, leaf_secret, { {}, inline_tree, force_path, {} }); - const auto welcome_data = tls::marshal(mls::MLSMessage{ welcome }); + const auto welcome_data = + MLS_NAMESPACE::tls::marshal(MLS_NAMESPACE::MLSMessage{ welcome }); // Store the resulting state auto epoch_authenticator = state.epoch_authenticator(); @@ -1440,7 +1473,8 @@ MLSClientImpl::reinit_welcome(const ReInitWelcomeRequest* request, response->set_welcome(bytes_to_string(welcome_data)); response->set_epoch_authenticator(bytes_to_string(epoch_authenticator)); if (!inline_tree) { - response->set_ratchet_tree(bytes_to_string(tls::marshal(tree))); + response->set_ratchet_tree( + bytes_to_string(MLS_NAMESPACE::tls::marshal(tree))); } return Status::OK; @@ -1458,13 +1492,15 @@ MLSClientImpl::handle_reinit_welcome(const HandleReInitWelcomeRequest* request, // Process the welcome const auto welcome_msg_data = string_to_bytes(request->welcome()); - const auto welcome_msg = tls::get(welcome_msg_data); - const auto welcome = var::get(welcome_msg.message); + const auto welcome_msg = + MLS_NAMESPACE::tls::get(welcome_msg_data); + const auto welcome = var::get(welcome_msg.message); - auto ratchet_tree = std::optional{}; + auto ratchet_tree = std::optional{}; auto ratchet_tree_data = string_to_bytes(request->ratchet_tree()); if (!ratchet_tree_data.empty()) { - ratchet_tree = tls::get(ratchet_tree_data); + ratchet_tree = MLS_NAMESPACE::tls::get( + ratchet_tree_data); } auto state = reinit->tombstone.handle_welcome(reinit->kp_priv.init_priv, @@ -1489,19 +1525,21 @@ MLSClientImpl::create_branch(CachedState& entry, CreateSubgroupResponse* response) { // Import KeyPackages - auto key_packages = std::vector{}; + auto key_packages = std::vector{}; for (int i = 0; i < request->key_packages_size(); i++) { const auto kp_msg_data = string_to_bytes(request->key_packages(i)); - const auto kp_msg = tls::get(kp_msg_data); - key_packages.emplace_back(var::get(kp_msg.message)); + const auto kp_msg = + MLS_NAMESPACE::tls::get(kp_msg_data); + key_packages.emplace_back( + var::get(kp_msg.message)); } // Import extensions - auto ext_list = mls::ExtensionList{}; + auto ext_list = MLS_NAMESPACE::ExtensionList{}; for (int i = 0; i < request->extensions_size(); i++) { const auto ext = request->extensions(i); const auto ext_type = - static_cast(ext.extension_type()); + static_cast(ext.extension_type()); const auto ext_data = string_to_bytes(ext.extension_data()); ext_list.add(ext_type, ext_data); } @@ -1509,14 +1547,16 @@ MLSClientImpl::create_branch(CachedState& entry, // Create the branch const auto my_leaf = opt::get(entry.state.tree().leaf_node(entry.state.index())); - const auto identity = my_leaf.credential.get().identity; + const auto identity = + my_leaf.credential.get().identity; const auto inline_tree = !request->external_tree(); const auto force_path = request->force_path(); const auto group_id = string_to_bytes(request->group_id()); const auto cipher_suite = entry.state.cipher_suite(); const auto kp_priv = new_key_package(cipher_suite, identity); - const auto leaf_secret = mls::random_bytes(cipher_suite.secret_size()); + const auto leaf_secret = + MLS_NAMESPACE::random_bytes(cipher_suite.secret_size()); auto [next, welcome] = entry.state.create_branch(group_id, kp_priv.encryption_priv, @@ -1536,7 +1576,8 @@ MLSClientImpl::create_branch(CachedState& entry, response->set_epoch_authenticator(bytes_to_string(epoch_authenticator)); if (!inline_tree) { - auto ratchet_tree = bytes_to_string(tls::marshal(next.tree())); + auto ratchet_tree = + bytes_to_string(MLS_NAMESPACE::tls::marshal(next.tree())); response->set_ratchet_tree(ratchet_tree); } @@ -1553,11 +1594,12 @@ MLSClientImpl::handle_branch(CachedState& entry, return Status(StatusCode::INVALID_ARGUMENT, "Unknown transaction ID"); } - auto welcome = unmarshal_message(request->welcome()); - auto ratchet_tree = std::optional{}; + auto welcome = unmarshal_message(request->welcome()); + auto ratchet_tree = std::optional{}; auto ratchet_tree_data = string_to_bytes(request->ratchet_tree()); if (!ratchet_tree_data.empty()) { - ratchet_tree = tls::get(ratchet_tree_data); + ratchet_tree = MLS_NAMESPACE::tls::get( + ratchet_tree_data); } auto state = entry.state.handle_branch(join->kp_priv.init_priv, diff --git a/cmd/interop/src/mls_client_impl.h b/cmd/interop/src/mls_client_impl.h index 30e373181..685195dc9 100644 --- a/cmd/interop/src/mls_client_impl.h +++ b/cmd/interop/src/mls_client_impl.h @@ -141,13 +141,13 @@ class MLSClientImpl final : public MLSClient::Service struct KeyPackageWithSecrets { - mls::HPKEPrivateKey init_priv; - mls::HPKEPrivateKey encryption_priv; - mls::SignaturePrivateKey signature_priv; - mls::KeyPackage key_package; + MLS_NAMESPACE::HPKEPrivateKey init_priv; + MLS_NAMESPACE::HPKEPrivateKey encryption_priv; + MLS_NAMESPACE::SignaturePrivateKey signature_priv; + MLS_NAMESPACE::KeyPackage key_package; }; - KeyPackageWithSecrets new_key_package(mls::CipherSuite cipher_suite, + KeyPackageWithSecrets new_key_package(MLS_NAMESPACE::CipherSuite cipher_suite, const bytes& identity); // Cached join transactions @@ -165,56 +165,59 @@ class MLSClientImpl final : public MLSClient::Service // Cached group state struct CachedState { - mls::State state; + MLS_NAMESPACE::State state; bool encrypt_handshake; - mls::MessageOpts message_opts() const; + MLS_NAMESPACE::MessageOpts message_opts() const; std::optional pending_commit; std::optional pending_state_id; void reset_pending(); // Marshal/unmarshal with encryption as required - std::string marshal(const mls::MLSMessage& msg); - mls::MLSMessage unmarshal(const std::string& wire); + std::string marshal(const MLS_NAMESPACE::MLSMessage& msg); + MLS_NAMESPACE::MLSMessage unmarshal(const std::string& wire); }; std::map state_cache; - uint32_t store_state(mls::State&& state, bool encrypt_handshake); + uint32_t store_state(MLS_NAMESPACE::State&& state, bool encrypt_handshake); CachedState* load_state(uint32_t state_id); - CachedState* find_state(const bytes& group_id, const mls::epoch_t epoch); + CachedState* find_state(const bytes& group_id, + const MLS_NAMESPACE::epoch_t epoch); void remove_state(uint32_t state_id); - mls::LeafIndex find_member(const mls::TreeKEMPublicKey& tree, - const std::string& identity); - mls::Proposal proposal_from_description(mls::CipherSuite suite, - const bytes& group_id, - const mls::TreeKEMPublicKey& tree, - const ProposalDescription& desc); + MLS_NAMESPACE::LeafIndex find_member( + const MLS_NAMESPACE::TreeKEMPublicKey& tree, + const std::string& identity); + MLS_NAMESPACE::Proposal proposal_from_description( + MLS_NAMESPACE::CipherSuite suite, + const bytes& group_id, + const MLS_NAMESPACE::TreeKEMPublicKey& tree, + const ProposalDescription& desc); // Cached external signers struct CachedSigner { - mls::SignaturePrivateKey signature_priv; + MLS_NAMESPACE::SignaturePrivateKey signature_priv; }; std::map signer_cache; - uint32_t store_signer(mls::SignaturePrivateKey&& signature_priv); + uint32_t store_signer(MLS_NAMESPACE::SignaturePrivateKey&& signature_priv); CachedSigner* load_signer(uint32_t signer_id); // Cached ReInit struct CachedReInit { KeyPackageWithSecrets kp_priv; - mls::State::Tombstone tombstone; + MLS_NAMESPACE::State::Tombstone tombstone; bool encrypt_handshake; }; std::map reinit_cache; uint32_t store_reinit(KeyPackageWithSecrets&& kp_priv, - mls::State::Tombstone&& tombstone, + MLS_NAMESPACE::State::Tombstone&& tombstone, bool encrypt_handshake); CachedReInit* load_reinit(uint32_t reinit_id); void remove_reinit(uint32_t reinit_id); diff --git a/include/mls/common.h b/include/mls/common.h index c89a6f3d9..ac9ea5165 100644 --- a/include/mls/common.h +++ b/include/mls/common.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -11,14 +12,14 @@ using namespace std::literals::string_literals; // Expose the bytes library globally #include -using namespace bytes_ns; +using namespace MLS_NAMESPACE::bytes_ns; // Expose the compatibility library globally #include -namespace var = tls::var; -namespace opt = tls::opt; +namespace var = MLS_NAMESPACE::tls::var; +namespace opt = MLS_NAMESPACE::tls::opt; -namespace mls { +namespace MLS_NAMESPACE { // Make variant equality work in the same way as optional equality, with // automatic unwrapping. In other words @@ -262,4 +263,4 @@ upper_bound(const Container& c, const Value& val) } // namespace stdx -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/include/mls/core_types.h b/include/mls/core_types.h index fda9f71fc..533c6a8cb 100644 --- a/include/mls/core_types.h +++ b/include/mls/core_types.h @@ -3,8 +3,9 @@ #include "mls/credential.h" #include "mls/crypto.h" #include "mls/tree_math.h" +#include -namespace mls { +namespace MLS_NAMESPACE { // enum { // reserved(0), @@ -112,6 +113,14 @@ struct Capabilities bool proposals_supported(const std::vector& required) const; bool credential_supported(const Credential& credential) const; + template + bool credentials_supported(const Container& required) const + { + return stdx::all_of(required, [&](CredentialType type) { + return stdx::contains(credentials, type); + }); + } + TLS_SERIALIZABLE(versions, cipher_suites, extensions, proposals, credentials) }; @@ -357,12 +366,16 @@ struct UpdatePath TLS_SERIALIZABLE(leaf_node, nodes) }; -} // namespace mls +} // namespace MLS_NAMESPACE -namespace tls { +namespace MLS_NAMESPACE::tls { -TLS_VARIANT_MAP(mls::LeafNodeSource, mls::Lifetime, key_package) -TLS_VARIANT_MAP(mls::LeafNodeSource, mls::Empty, update) -TLS_VARIANT_MAP(mls::LeafNodeSource, mls::ParentHash, commit) +TLS_VARIANT_MAP(MLS_NAMESPACE::LeafNodeSource, + MLS_NAMESPACE::Lifetime, + key_package) +TLS_VARIANT_MAP(MLS_NAMESPACE::LeafNodeSource, MLS_NAMESPACE::Empty, update) +TLS_VARIANT_MAP(MLS_NAMESPACE::LeafNodeSource, + MLS_NAMESPACE::ParentHash, + commit) -} // namespace tls +} // namespace MLS_NAMESPACE::tls diff --git a/include/mls/credential.h b/include/mls/credential.h index c7e251df3..be0c213e8 100644 --- a/include/mls/credential.h +++ b/include/mls/credential.h @@ -2,8 +2,13 @@ #include #include +#include -namespace mls { +namespace MLS_NAMESPACE { + +namespace hpke { +struct UserInfoVC; +} // struct { // opaque identity<0..2^16-1>; @@ -53,6 +58,28 @@ operator<<(tls::ostream& str, const X509Credential& obj); tls::istream& operator>>(tls::istream& str, X509Credential& obj); +struct UserInfoVCCredential +{ + UserInfoVCCredential() = default; + explicit UserInfoVCCredential(std::string userinfo_vc_jwt_in); + + std::string userinfo_vc_jwt; + + bool valid_for(const SignaturePublicKey& pub) const; + bool valid_from(const PublicJWK& pub) const; + + friend tls::ostream operator<<(tls::ostream& str, + const UserInfoVCCredential& obj); + friend tls::istream operator>>(tls::istream& str, UserInfoVCCredential& obj); + friend bool operator==(const UserInfoVCCredential& lhs, + const UserInfoVCCredential& rhs); + friend bool operator!=(const UserInfoVCCredential& lhs, + const UserInfoVCCredential& rhs); + +private: + std::shared_ptr _vc; +}; + bool operator==(const X509Credential& lhs, const X509Credential& rhs); @@ -62,6 +89,9 @@ enum struct CredentialType : uint16_t basic = 1, x509 = 2, + userinfo_vc_draft_00 = 0xFE00, + multi_draft_00 = 0xFF00, + // GREASE values, included here mainly so that debugger output looks nice GREASE_0 = 0x0A0A, GREASE_1 = 0x1A1A, @@ -80,6 +110,31 @@ enum struct CredentialType : uint16_t GREASE_E = 0xEAEA, }; +// struct { +// Credential credential; +// SignaturePublicKey credential_key; +// opaque signature; +// } CredentialBinding +// +// struct { +// CredentialBinding bindings; +// } MultiCredential; +struct CredentialBinding; +struct CredentialBindingInput; + +struct MultiCredential +{ + MultiCredential() = default; + MultiCredential(const std::vector& binding_inputs, + const SignaturePublicKey& signature_key); + + std::vector bindings; + + bool valid_for(const SignaturePublicKey& pub) const; + + TLS_SERIALIZABLE(bindings) +}; + // struct { // CredentialType credential_type; // select (credential_type) { @@ -90,9 +145,10 @@ enum struct CredentialType : uint16_t // opaque cert_data<1..2^24-1>; // }; // } Credential; -class Credential +struct Credential { -public: + Credential() = default; + CredentialType type() const; template @@ -103,6 +159,10 @@ class Credential static Credential basic(const bytes& identity); static Credential x509(const std::vector& der_chain); + static Credential userinfo_vc(const std::string& userinfo_vc_jwt); + static Credential multi( + const std::vector& binding_inputs, + const SignaturePublicKey& signature_key); bool valid_for(const SignaturePublicKey& pub) const; @@ -110,14 +170,60 @@ class Credential TLS_TRAITS(tls::variant) private: - var::variant _cred; + using SpecificCredential = var::variant; + + Credential(SpecificCredential specific); + SpecificCredential _cred; +}; + +// XXX(RLB): This struct needs to appear below Credential so that all types are +// concrete at the appropriate points. +struct CredentialBindingInput +{ + CipherSuite cipher_suite; + Credential credential; + const SignaturePrivateKey& credential_priv; +}; + +struct CredentialBinding +{ + CipherSuite cipher_suite; + Credential credential; + SignaturePublicKey credential_key; + bytes signature; + + CredentialBinding() = default; + CredentialBinding(CipherSuite suite_in, + Credential credential_in, + const SignaturePrivateKey& credential_priv, + const SignaturePublicKey& signature_key); + + bool valid_for(const SignaturePublicKey& signature_key) const; + + TLS_SERIALIZABLE(cipher_suite, credential, credential_key, signature) + +private: + bytes to_be_signed(const SignaturePublicKey& signature_key) const; }; -} // namespace mls +} // namespace MLS_NAMESPACE -namespace tls { +namespace MLS_NAMESPACE::tls { -TLS_VARIANT_MAP(mls::CredentialType, mls::BasicCredential, basic) -TLS_VARIANT_MAP(mls::CredentialType, mls::X509Credential, x509) +TLS_VARIANT_MAP(MLS_NAMESPACE::CredentialType, + MLS_NAMESPACE::BasicCredential, + basic) +TLS_VARIANT_MAP(MLS_NAMESPACE::CredentialType, + MLS_NAMESPACE::X509Credential, + x509) +TLS_VARIANT_MAP(MLS_NAMESPACE::CredentialType, + MLS_NAMESPACE::UserInfoVCCredential, + userinfo_vc_draft_00) +TLS_VARIANT_MAP(MLS_NAMESPACE::CredentialType, + MLS_NAMESPACE::MultiCredential, + multi_draft_00) -} // namespace TLS +} // namespace MLS_NAMESPACE::tls diff --git a/include/mls/crypto.h b/include/mls/crypto.h index 838f456a8..16aefaabe 100644 --- a/include/mls/crypto.h +++ b/include/mls/crypto.h @@ -5,11 +5,11 @@ #include #include #include +#include #include - #include -namespace mls { +namespace MLS_NAMESPACE { /// Signature Code points, borrowed from RFC 8446 enum struct SignatureScheme : uint16_t @@ -140,7 +140,7 @@ extern const std::array all_supported_suites; #endif // Utilities -using hpke::random_bytes; +using MLS_NAMESPACE::hpke::random_bytes; // HPKE Keys namespace encrypt_label { @@ -209,10 +209,14 @@ extern const std::string mls_content; extern const std::string leaf_node; extern const std::string key_package; extern const std::string group_info; +extern const std::string multi_credential; } // namespace sign_label struct SignaturePublicKey { + static SignaturePublicKey from_jwk(CipherSuite suite, + const std::string& json_str); + bytes data; bool verify(const CipherSuite& suite, @@ -220,14 +224,27 @@ struct SignaturePublicKey const bytes& message, const bytes& signature) const; + std::string to_jwk(CipherSuite suite) const; + TLS_SERIALIZABLE(data) }; +struct PublicJWK +{ + SignatureScheme signature_scheme; + std::optional key_id; + SignaturePublicKey public_key; + + static PublicJWK parse(const std::string& jwk_json); +}; + struct SignaturePrivateKey { static SignaturePrivateKey generate(CipherSuite suite); static SignaturePrivateKey parse(CipherSuite suite, const bytes& data); static SignaturePrivateKey derive(CipherSuite suite, const bytes& secret); + static SignaturePrivateKey from_jwk(CipherSuite suite, + const std::string& json_str); SignaturePrivateKey() = default; @@ -239,6 +256,7 @@ struct SignaturePrivateKey const bytes& message) const; void set_public_key(CipherSuite suite); + std::string to_jwk(CipherSuite suite) const; TLS_SERIALIZABLE(data) @@ -246,4 +264,4 @@ struct SignaturePrivateKey SignaturePrivateKey(bytes priv_data, bytes pub_data); }; -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/include/mls/key_schedule.h b/include/mls/key_schedule.h index e1014de7e..6cf5be2e6 100644 --- a/include/mls/key_schedule.h +++ b/include/mls/key_schedule.h @@ -5,8 +5,9 @@ #include #include #include +#include -namespace mls { +namespace MLS_NAMESPACE { struct HashRatchet { @@ -202,4 +203,4 @@ struct TranscriptHash bool operator==(const TranscriptHash& lhs, const TranscriptHash& rhs); -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/include/mls/messages.h b/include/mls/messages.h index e53e2764c..a660aeb0e 100644 --- a/include/mls/messages.h +++ b/include/mls/messages.h @@ -5,10 +5,11 @@ #include "mls/credential.h" #include "mls/crypto.h" #include "mls/treekem.h" +#include #include #include -namespace mls { +namespace MLS_NAMESPACE { struct ExternalPubExtension { @@ -418,8 +419,8 @@ struct GroupContext; enum struct WireFormat : uint16_t { reserved = 0, - mls_plaintext = 1, - mls_ciphertext = 2, + mls_public_message = 1, + mls_private_message = 2, mls_welcome = 3, mls_group_info = 4, mls_key_package = 5, @@ -591,6 +592,8 @@ struct PublicMessage friend tls::ostream& operator<<(tls::ostream& str, const PublicMessage& obj); friend tls::istream& operator>>(tls::istream& str, PublicMessage& obj); + friend bool operator==(const PublicMessage& lhs, const PublicMessage& rhs); + friend bool operator!=(const PublicMessage& lhs, const PublicMessage& rhs); private: GroupContent content; @@ -650,8 +653,8 @@ struct MLSMessage WireFormat wire_format() const; MLSMessage() = default; - MLSMessage(PublicMessage mls_plaintext); - MLSMessage(PrivateMessage mls_ciphertext); + MLSMessage(PublicMessage public_message); + MLSMessage(PrivateMessage private_message); MLSMessage(Welcome welcome); MLSMessage(GroupInfo group_info); MLSMessage(KeyPackage key_package); @@ -668,41 +671,63 @@ external_proposal(CipherSuite suite, uint32_t signer_index, const SignaturePrivateKey& sig_priv); -} // namespace mls - -namespace tls { - -TLS_VARIANT_MAP(mls::PSKType, mls::ExternalPSK, external) -TLS_VARIANT_MAP(mls::PSKType, mls::ResumptionPSK, resumption) - -TLS_VARIANT_MAP(mls::ProposalOrRefType, mls::Proposal, value) -TLS_VARIANT_MAP(mls::ProposalOrRefType, mls::ProposalRef, reference) - -TLS_VARIANT_MAP(mls::ProposalType, mls::Add, add) -TLS_VARIANT_MAP(mls::ProposalType, mls::Update, update) -TLS_VARIANT_MAP(mls::ProposalType, mls::Remove, remove) -TLS_VARIANT_MAP(mls::ProposalType, mls::PreSharedKey, psk) -TLS_VARIANT_MAP(mls::ProposalType, mls::ReInit, reinit) -TLS_VARIANT_MAP(mls::ProposalType, mls::ExternalInit, external_init) -TLS_VARIANT_MAP(mls::ProposalType, - mls::GroupContextExtensions, +} // namespace MLS_NAMESPACE + +namespace MLS_NAMESPACE::tls { + +TLS_VARIANT_MAP(MLS_NAMESPACE::PSKType, MLS_NAMESPACE::ExternalPSK, external) +TLS_VARIANT_MAP(MLS_NAMESPACE::PSKType, + MLS_NAMESPACE::ResumptionPSK, + resumption) + +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalOrRefType, + MLS_NAMESPACE::Proposal, + value) +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalOrRefType, + MLS_NAMESPACE::ProposalRef, + reference) + +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalType, MLS_NAMESPACE::Add, add) +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalType, MLS_NAMESPACE::Update, update) +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalType, MLS_NAMESPACE::Remove, remove) +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalType, MLS_NAMESPACE::PreSharedKey, psk) +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalType, MLS_NAMESPACE::ReInit, reinit) +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalType, + MLS_NAMESPACE::ExternalInit, + external_init) +TLS_VARIANT_MAP(MLS_NAMESPACE::ProposalType, + MLS_NAMESPACE::GroupContextExtensions, group_context_extensions) -TLS_VARIANT_MAP(mls::ContentType, mls::ApplicationData, application) -TLS_VARIANT_MAP(mls::ContentType, mls::Proposal, proposal) -TLS_VARIANT_MAP(mls::ContentType, mls::Commit, commit) - -TLS_VARIANT_MAP(mls::SenderType, mls::MemberSender, member) -TLS_VARIANT_MAP(mls::SenderType, mls::ExternalSenderIndex, external) -TLS_VARIANT_MAP(mls::SenderType, - mls::NewMemberProposalSender, +TLS_VARIANT_MAP(MLS_NAMESPACE::ContentType, + MLS_NAMESPACE::ApplicationData, + application) +TLS_VARIANT_MAP(MLS_NAMESPACE::ContentType, MLS_NAMESPACE::Proposal, proposal) +TLS_VARIANT_MAP(MLS_NAMESPACE::ContentType, MLS_NAMESPACE::Commit, commit) + +TLS_VARIANT_MAP(MLS_NAMESPACE::SenderType, MLS_NAMESPACE::MemberSender, member) +TLS_VARIANT_MAP(MLS_NAMESPACE::SenderType, + MLS_NAMESPACE::ExternalSenderIndex, + external) +TLS_VARIANT_MAP(MLS_NAMESPACE::SenderType, + MLS_NAMESPACE::NewMemberProposalSender, new_member_proposal) -TLS_VARIANT_MAP(mls::SenderType, mls::NewMemberCommitSender, new_member_commit) - -TLS_VARIANT_MAP(mls::WireFormat, mls::PublicMessage, mls_plaintext) -TLS_VARIANT_MAP(mls::WireFormat, mls::PrivateMessage, mls_ciphertext) -TLS_VARIANT_MAP(mls::WireFormat, mls::Welcome, mls_welcome) -TLS_VARIANT_MAP(mls::WireFormat, mls::GroupInfo, mls_group_info) -TLS_VARIANT_MAP(mls::WireFormat, mls::KeyPackage, mls_key_package) - -} // namespace tls +TLS_VARIANT_MAP(MLS_NAMESPACE::SenderType, + MLS_NAMESPACE::NewMemberCommitSender, + new_member_commit) + +TLS_VARIANT_MAP(MLS_NAMESPACE::WireFormat, + MLS_NAMESPACE::PublicMessage, + mls_public_message) +TLS_VARIANT_MAP(MLS_NAMESPACE::WireFormat, + MLS_NAMESPACE::PrivateMessage, + mls_private_message) +TLS_VARIANT_MAP(MLS_NAMESPACE::WireFormat, MLS_NAMESPACE::Welcome, mls_welcome) +TLS_VARIANT_MAP(MLS_NAMESPACE::WireFormat, + MLS_NAMESPACE::GroupInfo, + mls_group_info) +TLS_VARIANT_MAP(MLS_NAMESPACE::WireFormat, + MLS_NAMESPACE::KeyPackage, + mls_key_package) + +} // namespace MLS_NAMESPACE::tls diff --git a/include/mls/session.h b/include/mls/session.h index eb9d0b65e..987b54df1 100644 --- a/include/mls/session.h +++ b/include/mls/session.h @@ -5,8 +5,9 @@ #include #include #include +#include -namespace mls { +namespace MLS_NAMESPACE { class PendingJoin; class Session; @@ -95,4 +96,4 @@ class Session friend bool operator!=(const Session& lhs, const Session& rhs); }; -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/include/mls/state.h b/include/mls/state.h index 2d7b448f6..329aadb07 100644 --- a/include/mls/state.h +++ b/include/mls/state.h @@ -5,10 +5,11 @@ #include "mls/messages.h" #include "mls/treekem.h" #include +#include #include #include -namespace mls { +namespace MLS_NAMESPACE { // Index into the session roster struct RosterIndex : public UInt32 @@ -276,6 +277,7 @@ class State TreeKEMPublicKey import_tree(const bytes& tree_hash, const std::optional& external, const ExtensionList& extensions); + bool validate_tree() const; // Form a commit, covering all the cases with slightly different validation // rules: @@ -417,4 +419,4 @@ class State State successor() const; }; -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/include/mls/tree_math.h b/include/mls/tree_math.h index ffcade836..19e12d7b9 100644 --- a/include/mls/tree_math.h +++ b/include/mls/tree_math.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -27,7 +28,7 @@ // // 01x = <00x, 10x> -namespace mls { +namespace MLS_NAMESPACE { // Index types go in the overall namespace // XXX(rlb@ipv.sx): Seems like this stuff can probably get @@ -104,4 +105,4 @@ struct NodeIndex : public UInt32 uint32_t level() const; }; -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/include/mls/treekem.h b/include/mls/treekem.h index b5c91b29c..75a9550f4 100644 --- a/include/mls/treekem.h +++ b/include/mls/treekem.h @@ -4,11 +4,12 @@ #include "mls/core_types.h" #include "mls/crypto.h" #include "mls/tree_math.h" +#include #include #define ENABLE_TREE_DUMP 1 -namespace mls { +namespace MLS_NAMESPACE { enum struct NodeType : uint8_t { @@ -122,6 +123,7 @@ struct TreeKEMPublicKey TreeKEMPublicKey& operator=(const TreeKEMPublicKey& other) = default; TreeKEMPublicKey& operator=(TreeKEMPublicKey&& other) = default; + LeafIndex allocate_leaf(); LeafIndex add_leaf(const LeafNode& leaf); void update_leaf(LeafIndex index, const LeafNode& leaf); void blank_path(LeafIndex index); @@ -148,6 +150,40 @@ struct TreeKEMPublicKey std::optional leaf_node(LeafIndex index) const; std::vector resolve(NodeIndex index) const; + template + bool all_leaves(const UnaryPredicate& pred) const + { + for (LeafIndex i{ 0 }; i < size; i.val++) { + const auto& node = node_at(i); + if (node.blank()) { + continue; + } + + if (!pred(i, node.leaf_node())) { + return false; + } + } + + return true; + } + + template + bool any_leaf(const UnaryPredicate& pred) const + { + for (LeafIndex i{ 0 }; i < size; i.val++) { + const auto& node = node_at(i); + if (node.blank()) { + continue; + } + + if (pred(i, node.leaf_node())) { + return true; + } + } + + return false; + } + using FilteredDirectPath = std::vector>>; FilteredDirectPath filtered_direct_path(NodeIndex index) const; @@ -187,6 +223,11 @@ struct TreeKEMPublicKey NodeIndex parent, NodeIndex sibling) const; + bool exists_in_tree(const HPKEPublicKey& key, + std::optional except) const; + bool exists_in_tree(const SignaturePublicKey& key, + std::optional except) const; + OptionalNode blank_node; friend struct TreeKEMPrivateKey; @@ -200,14 +241,16 @@ operator>>(tls::istream& str, TreeKEMPublicKey& obj); struct LeafNodeHashInput; struct ParentNodeHashInput; -} // namespace mls +} // namespace MLS_NAMESPACE -namespace tls { +namespace MLS_NAMESPACE::tls { -TLS_VARIANT_MAP(mls::NodeType, mls::LeafNodeHashInput, leaf) -TLS_VARIANT_MAP(mls::NodeType, mls::ParentNodeHashInput, parent) +TLS_VARIANT_MAP(MLS_NAMESPACE::NodeType, MLS_NAMESPACE::LeafNodeHashInput, leaf) +TLS_VARIANT_MAP(MLS_NAMESPACE::NodeType, + MLS_NAMESPACE::ParentNodeHashInput, + parent) -TLS_VARIANT_MAP(mls::NodeType, mls::LeafNode, leaf) -TLS_VARIANT_MAP(mls::NodeType, mls::ParentNode, parent) +TLS_VARIANT_MAP(MLS_NAMESPACE::NodeType, MLS_NAMESPACE::LeafNode, leaf) +TLS_VARIANT_MAP(MLS_NAMESPACE::NodeType, MLS_NAMESPACE::ParentNode, parent) -} // namespace tls +} // namespace MLS_NAMESPACE::tls diff --git a/lib/bytes/CMakeLists.txt b/lib/bytes/CMakeLists.txt index 41d9c1154..7cfb6e31e 100644 --- a/lib/bytes/CMakeLists.txt +++ b/lib/bytes/CMakeLists.txt @@ -11,7 +11,22 @@ add_library(${CURRENT_LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES}) add_dependencies(${CURRENT_LIB_NAME} tls_syntax) target_link_libraries(${CURRENT_LIB_NAME} tls_syntax) target_include_directories(${CURRENT_LIB_NAME} - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + PUBLIC + $ + $ + $ +) + +### +### Install +### + +install(TARGETS ${CURRENT_LIB_NAME} EXPORT mlspp-targets) +install( + DIRECTORY + include/ + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} ) ### diff --git a/lib/bytes/include/bytes/bytes.h b/lib/bytes/include/bytes/bytes.h index ddb5dacf9..582701a26 100644 --- a/lib/bytes/include/bytes/bytes.h +++ b/lib/bytes/include/bytes/bytes.h @@ -1,10 +1,11 @@ #pragma once +#include #include #include #include -namespace bytes_ns { +namespace MLS_NAMESPACE::bytes_ns { struct bytes { @@ -78,6 +79,8 @@ struct bytes auto& at(size_t pos) { return _data.at(pos); } void resize(size_t count) { _data.resize(count); } + void reserve(size_t len) { _data.reserve(len); } + void push_back(uint8_t byte) { _data.push_back(byte); } // Equality operators bool operator==(const bytes& other) const; @@ -106,6 +109,9 @@ struct bytes std::vector _data; }; +std::string +to_ascii(const bytes& data); + bytes from_ascii(const std::string& ascii); @@ -115,4 +121,4 @@ to_hex(const bytes& data); bytes from_hex(const std::string& hex); -} // namespace bytes_ns +} // namespace MLS_NAMESPACE::bytes_ns diff --git a/lib/bytes/src/bytes.cpp b/lib/bytes/src/bytes.cpp index 509e25323..2bb2e93e2 100644 --- a/lib/bytes/src/bytes.cpp +++ b/lib/bytes/src/bytes.cpp @@ -2,10 +2,11 @@ #include #include +#include #include #include -namespace bytes_ns { +namespace MLS_NAMESPACE::bytes_ns { bool bytes::operator==(const bytes& other) const @@ -79,6 +80,12 @@ bytes::operator^(const bytes& rhs) const return out; } +std::string +to_ascii(const bytes& data) +{ + return { data.begin(), data.end() }; +} + bytes from_ascii(const std::string& ascii) { @@ -137,4 +144,4 @@ operator!=(const std::vector& lhs, const bytes_ns::bytes& rhs) return rhs != lhs; } -} // namespace bytes_ns +} // namespace MLS_NAMESPACE::bytes_ns diff --git a/lib/bytes/test/bytes.cpp b/lib/bytes/test/bytes.cpp index a28dbf9f6..3241f1ec7 100644 --- a/lib/bytes/test/bytes.cpp +++ b/lib/bytes/test/bytes.cpp @@ -3,7 +3,7 @@ #include #include -using namespace bytes_ns; +using namespace MLS_NAMESPACE::bytes_ns; using namespace std::literals::string_literals; // To check that memory is safely zeroized on destroy, we have to deliberately diff --git a/lib/hpke/CMakeLists.txt b/lib/hpke/CMakeLists.txt index b1f493c28..ecc0409fa 100644 --- a/lib/hpke/CMakeLists.txt +++ b/lib/hpke/CMakeLists.txt @@ -3,6 +3,7 @@ set(CURRENT_LIB_NAME hpke) ### ### Dependencies ### +find_package(nlohmann_json REQUIRED) ### ### Library Config @@ -13,14 +14,32 @@ file(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src add_library(${CURRENT_LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES}) add_dependencies(${CURRENT_LIB_NAME} bytes tls_syntax) -target_link_libraries(${CURRENT_LIB_NAME} PRIVATE bytes tls_syntax) +target_link_libraries(${CURRENT_LIB_NAME} + PRIVATE + OpenSSL::Crypto + PUBLIC + nlohmann_json::nlohmann_json bytes tls_syntax) target_include_directories(${CURRENT_LIB_NAME} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - PRIVATE + $ + $ + $ + PRIVATE ${OPENSSL_INCLUDE_DIR} ) +### +### Install +### + +install(TARGETS ${CURRENT_LIB_NAME} EXPORT mlspp-targets) +install( + DIRECTORY + include/ + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} +) + ### ### Tests ### diff --git a/lib/hpke/include/hpke/base64.h b/lib/hpke/include/hpke/base64.h new file mode 100644 index 000000000..0be0d4057 --- /dev/null +++ b/lib/hpke/include/hpke/base64.h @@ -0,0 +1,20 @@ +#pragma once + +#include +using namespace MLS_NAMESPACE::bytes_ns; + +namespace MLS_NAMESPACE::hpke { + +std::string +to_base64(const bytes& data); + +std::string +to_base64url(const bytes& data); + +bytes +from_base64(const std::string& enc); + +bytes +from_base64url(const std::string& enc); + +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/include/hpke/certificate.h b/lib/hpke/include/hpke/certificate.h index 3267f134f..785594cc5 100644 --- a/lib/hpke/include/hpke/certificate.h +++ b/lib/hpke/include/hpke/certificate.h @@ -6,10 +6,11 @@ #include #include #include +#include -using namespace bytes_ns; +using namespace MLS_NAMESPACE::bytes_ns; -namespace hpke { +namespace MLS_NAMESPACE::hpke { struct Certificate { @@ -72,4 +73,4 @@ struct Certificate bool operator==(const Certificate& lhs, const Certificate& rhs); -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/include/hpke/digest.h b/lib/hpke/include/hpke/digest.h index 9523a6f14..022bc9429 100644 --- a/lib/hpke/include/hpke/digest.h +++ b/lib/hpke/include/hpke/digest.h @@ -3,9 +3,11 @@ #include #include -using namespace bytes_ns; +#include -namespace hpke { +using namespace MLS_NAMESPACE::bytes_ns; + +namespace MLS_NAMESPACE::hpke { struct Digest { @@ -33,4 +35,4 @@ struct Digest friend struct HKDF; }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/include/hpke/hpke.h b/lib/hpke/include/hpke/hpke.h index 5ff10682e..0c64fb91d 100644 --- a/lib/hpke/include/hpke/hpke.h +++ b/lib/hpke/include/hpke/hpke.h @@ -4,9 +4,10 @@ #include #include -using namespace bytes_ns; +#include +using namespace MLS_NAMESPACE::bytes_ns; -namespace hpke { +namespace MLS_NAMESPACE::hpke { struct KEM { @@ -250,4 +251,4 @@ struct HPKE const bytes& psk_id) const; }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/include/hpke/random.h b/lib/hpke/include/hpke/random.h index 8245efac8..b43503a5c 100644 --- a/lib/hpke/include/hpke/random.h +++ b/lib/hpke/include/hpke/random.h @@ -1,11 +1,12 @@ #pragma once #include -using namespace bytes_ns; +#include +using namespace MLS_NAMESPACE::bytes_ns; -namespace hpke { +namespace MLS_NAMESPACE::hpke { bytes random_bytes(size_t size); -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/include/hpke/signature.h b/lib/hpke/include/hpke/signature.h index aa1fe7708..19e66b064 100644 --- a/lib/hpke/include/hpke/signature.h +++ b/lib/hpke/include/hpke/signature.h @@ -3,9 +3,10 @@ #include #include -using namespace bytes_ns; +#include +using namespace MLS_NAMESPACE::bytes_ns; -namespace hpke { +namespace MLS_NAMESPACE::hpke { struct Signature { @@ -48,9 +49,32 @@ struct Signature virtual bytes serialize(const PublicKey& pk) const = 0; virtual std::unique_ptr deserialize(const bytes& enc) const = 0; - virtual bytes serialize_private(const PrivateKey& sk) const; + virtual bytes serialize_private(const PrivateKey& sk) const = 0; virtual std::unique_ptr deserialize_private( - const bytes& skm) const; + const bytes& skm) const = 0; + + struct PrivateJWK + { + const Signature& sig; + std::optional key_id; + std::unique_ptr key; + }; + static PrivateJWK parse_jwk_private(const std::string& jwk_json); + + struct PublicJWK + { + const Signature& sig; + std::optional key_id; + std::unique_ptr key; + }; + static PublicJWK parse_jwk(const std::string& jwk_json); + + virtual std::unique_ptr import_jwk_private( + const std::string& jwk_json) const = 0; + virtual std::unique_ptr import_jwk( + const std::string& jwk_json) const = 0; + virtual std::string export_jwk_private(const PrivateKey& env) const = 0; + virtual std::string export_jwk(const PublicKey& env) const = 0; virtual bytes sign(const bytes& data, const PrivateKey& sk) const = 0; virtual bool verify(const bytes& data, @@ -63,4 +87,4 @@ struct Signature Signature(ID id_in); }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/include/hpke/userinfo_vc.h b/lib/hpke/include/hpke/userinfo_vc.h new file mode 100644 index 000000000..902fed53b --- /dev/null +++ b/lib/hpke/include/hpke/userinfo_vc.h @@ -0,0 +1,85 @@ +#pragma once +#include +#include + +#include +#include +#include +#include +#include + +using namespace MLS_NAMESPACE::bytes_ns; + +namespace MLS_NAMESPACE::hpke { + +struct UserInfoClaimsAddress +{ + std::optional formatted; + std::optional street_address; + std::optional locality; + std::optional region; + std::optional postal_code; + std::optional country; + + static UserInfoClaimsAddress from_json(const nlohmann::json& address_json); +}; + +struct UserInfoClaims +{ + + std::optional sub; + std::optional name; + std::optional given_name; + std::optional family_name; + std::optional middle_name; + std::optional nickname; + std::optional preferred_username; + std::optional profile; + std::optional picture; + std::optional website; + std::optional email; + std::optional email_verified; + std::optional gender; + std::optional birthdate; + std::optional zoneinfo; + std::optional locale; + std::optional phone_number; + std::optional phone_number_verified; + std::optional address; + std::optional updated_at; + + static UserInfoClaims from_json(const nlohmann::json& cred_subject_json); +}; + +struct UserInfoVC +{ +private: + struct ParsedCredential; + std::shared_ptr parsed_cred; + +public: + explicit UserInfoVC(std::string jwt); + UserInfoVC() = default; + UserInfoVC(const UserInfoVC& other) = default; + ~UserInfoVC() = default; + UserInfoVC& operator=(const UserInfoVC& other) = default; + UserInfoVC& operator=(UserInfoVC&& other) = default; + + const Signature& signature_algorithm() const; + std::string issuer() const; + std::optional key_id() const; + std::chrono::system_clock::time_point not_before() const; + std::chrono::system_clock::time_point not_after() const; + const std::string& raw_credential() const; + const UserInfoClaims& subject() const; + const Signature::PublicJWK& public_key() const; + + bool valid_from(const Signature::PublicKey& issuer_key) const; + + std::string raw; +}; + +bool +operator==(const UserInfoVC& lhs, const UserInfoVC& rhs); + +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/aead_cipher.cpp b/lib/hpke/src/aead_cipher.cpp index 496f37c30..8f04d7334 100644 --- a/lib/hpke/src/aead_cipher.cpp +++ b/lib/hpke/src/aead_cipher.cpp @@ -1,13 +1,14 @@ #include "aead_cipher.h" #include "openssl_common.h" +#include #include #if WITH_BORINGSSL #include #endif -namespace hpke { +namespace MLS_NAMESPACE::hpke { /// /// ExportOnlyCipher @@ -318,4 +319,4 @@ AEADCipher::open(const bytes& key, #endif // WITH_BORINGSSL } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/aead_cipher.h b/lib/hpke/src/aead_cipher.h index aef28245c..a1461cc31 100644 --- a/lib/hpke/src/aead_cipher.h +++ b/lib/hpke/src/aead_cipher.h @@ -1,8 +1,9 @@ #pragma once #include +#include -namespace hpke { +namespace MLS_NAMESPACE::hpke { struct ExportOnlyCipher : public AEAD { @@ -42,4 +43,4 @@ struct AEADCipher : public AEAD friend AEADCipher make_aead(AEAD::ID cipher_in); }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/base64.cpp b/lib/hpke/src/base64.cpp new file mode 100644 index 000000000..1b28cd026 --- /dev/null +++ b/lib/hpke/src/base64.cpp @@ -0,0 +1,105 @@ +#include + +#include "openssl_common.h" + +#include +#include +#include + +namespace MLS_NAMESPACE::hpke { + +std::string +to_base64(const bytes& data) +{ + if (data.empty()) { + return ""; + } + +#if WITH_BORINGSSL + const auto data_size = data.size(); +#else + const auto data_size = static_cast(data.size()); +#endif + + // base64 encoding produces 4 characters for every 3 input bytes (rounded up) + const auto out_size = (data_size + 2) / 3 * 4; + auto out = bytes(out_size + 1); // NUL terminator + + const auto result = EVP_EncodeBlock(out.data(), data.data(), data_size); + if (result != out_size) { + throw openssl_error(); + } + + out.resize(out.size() - 1); // strip NUL terminator + return to_ascii(out); +} + +std::string +to_base64url(const bytes& data) +{ + if (data.empty()) { + return ""; + } + + auto encoded = to_base64(data); + + auto pad_start = encoded.find_first_of('='); + if (pad_start != std::string::npos) { + encoded = encoded.substr(0, pad_start); + } + + std::replace(encoded.begin(), encoded.end(), '+', '-'); + std::replace(encoded.begin(), encoded.end(), '/', '_'); + + return encoded; +} + +bytes +from_base64(const std::string& enc) +{ + if (enc.length() == 0) { + return {}; + } + + if (enc.length() % 4 != 0) { + throw std::runtime_error("Base64 length is not divisible by 4"); + } + + const auto in = from_ascii(enc); + const auto in_size = static_cast(in.size()); + const auto out_size = in_size / 4 * 3; + auto out = bytes(out_size); + + const auto result = EVP_DecodeBlock(out.data(), in.data(), in_size); + if (result != out_size) { + throw openssl_error(); + } + + if (enc.substr(enc.length() - 2, enc.length()) == "==") { + out.resize(out.size() - 2); + } else if (enc.substr(enc.length() - 1, enc.length()) == "=") { + out.resize(out.size() - 1); + } + + return out; +} + +bytes +from_base64url(const std::string& enc) +{ + if (enc.empty()) { + return {}; + } + + auto enc_copy = enc; + std::replace(enc_copy.begin(), enc_copy.end(), '-', '+'); + std::replace(enc_copy.begin(), enc_copy.end(), '_', '/'); + + while (enc_copy.length() % 4 != 0) { + enc_copy += "="; + } + + return from_base64(enc_copy); +} + +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/certificate.cpp b/lib/hpke/src/certificate.cpp index f1c61e592..14254c69d 100644 --- a/lib/hpke/src/certificate.cpp +++ b/lib/hpke/src/certificate.cpp @@ -4,13 +4,14 @@ #include #include #include +#include #include #include #include #include #include -namespace hpke { +namespace MLS_NAMESPACE::hpke { /// /// Utility functions /// @@ -529,4 +530,4 @@ operator==(const Certificate& lhs, const Certificate& rhs) return lhs.raw == rhs.raw; } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/common.cpp b/lib/hpke/src/common.cpp index 1666e71b5..2848768cf 100644 --- a/lib/hpke/src/common.cpp +++ b/lib/hpke/src/common.cpp @@ -1,6 +1,7 @@ #include "common.h" +#include -namespace hpke { +namespace MLS_NAMESPACE::hpke { bytes i2osp(uint64_t val, size_t size) @@ -17,4 +18,4 @@ i2osp(uint64_t val, size_t size) return out; } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/common.h b/lib/hpke/src/common.h index 4f122f1da..c58ab0732 100644 --- a/lib/hpke/src/common.h +++ b/lib/hpke/src/common.h @@ -1,10 +1,11 @@ #pragma once #include +#include -namespace hpke { +namespace MLS_NAMESPACE::hpke { bytes i2osp(uint64_t val, size_t size); -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/dhkem.cpp b/lib/hpke/src/dhkem.cpp index 434d929bb..28562f64a 100644 --- a/lib/hpke/src/dhkem.cpp +++ b/lib/hpke/src/dhkem.cpp @@ -1,8 +1,9 @@ #include "dhkem.h" #include "common.h" +#include -namespace hpke { +namespace MLS_NAMESPACE::hpke { DHKEM::PrivateKey::PrivateKey(Group::PrivateKey* group_priv_in) : group_priv(group_priv_in) @@ -213,4 +214,4 @@ DHKEM::extract_and_expand(const bytes& dh, const bytes& kem_context) const suite_id, eae_prk, label_shared_secret, kem_context, secret_size); } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/dhkem.h b/lib/hpke/src/dhkem.h index c6c4fa48a..fc540065f 100644 --- a/lib/hpke/src/dhkem.h +++ b/lib/hpke/src/dhkem.h @@ -1,10 +1,11 @@ #pragma once #include +#include #include "group.h" -namespace hpke { +namespace MLS_NAMESPACE::hpke { struct DHKEM : public KEM { @@ -54,4 +55,4 @@ struct DHKEM : public KEM const KDF& kdf_in); }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/digest.cpp b/lib/hpke/src/digest.cpp index daf7dc7c3..053fd2cd7 100644 --- a/lib/hpke/src/digest.cpp +++ b/lib/hpke/src/digest.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -8,7 +9,7 @@ #include "openssl_common.h" -namespace hpke { +namespace MLS_NAMESPACE::hpke { static const EVP_MD* openssl_digest_type(Digest::ID digest) @@ -184,4 +185,4 @@ Digest::hmac_for_hkdf_extract(const bytes& key, const bytes& data) const return md; } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/group.cpp b/lib/hpke/src/group.cpp index 404ccf4a8..2d60dd337 100644 --- a/lib/hpke/src/group.cpp +++ b/lib/hpke/src/group.cpp @@ -1,6 +1,7 @@ #include "group.h" #include +#include #include "common.h" #include "openssl_common.h" @@ -14,7 +15,7 @@ #include "openssl/param_build.h" #endif -namespace hpke { +namespace MLS_NAMESPACE::hpke { static inline size_t group_dh_size(Group::ID group_id); @@ -322,7 +323,7 @@ struct ECKeyGroup : public EVPGroup auto* group = EC_GROUP_new_by_curve_name_ex(nullptr, nullptr, curve_nid); auto group_ptr = make_typed_unique(group); #else - auto eckey = make_typed_unique(new_ec_key()); + auto eckey = new_ec_key(); const auto* group = EC_KEY_get0_group(eckey.get()); #endif @@ -359,7 +360,8 @@ struct ECKeyGroup : public EVPGroup EC_KEY_set_private_key(eckey.get(), sk.get()); EC_KEY_set_public_key(eckey.get(), pt.get()); - return std::make_unique(to_pkey(eckey.release())); + auto pkey = to_pkey(eckey.release()); + return std::make_unique(pkey.release()); #endif } @@ -450,7 +452,7 @@ struct ECKeyGroup : public EVPGroup } return std::make_unique(key.release()); #else - auto eckey = make_typed_unique(new_ec_key()); + auto eckey = new_ec_key(); auto* eckey_ptr = eckey.get(); const auto* data_ptr = enc.data(); if (nullptr == @@ -461,7 +463,8 @@ struct ECKeyGroup : public EVPGroup throw openssl_error(); } - return std::make_unique(to_pkey(eckey.release())); + auto pkey = to_pkey(eckey.release()); + return std::make_unique(pkey.release()); #endif } @@ -520,7 +523,7 @@ struct ECKeyGroup : public EVPGroup auto key = keypair_evp_key(priv); return std::make_unique(key.release()); #else - auto eckey = make_typed_unique(new_ec_key()); + auto eckey = new_ec_key(); const auto* group = EC_KEY_get0_group(eckey.get()); const auto d = make_typed_unique( BN_bin2bn(skm.data(), static_cast(skm.size()), nullptr)); @@ -530,7 +533,180 @@ struct ECKeyGroup : public EVPGroup EC_KEY_set_private_key(eckey.get(), d.get()); EC_KEY_set_public_key(eckey.get(), pt.get()); - return std::make_unique(to_pkey(eckey.release())); + auto pkey = to_pkey(eckey.release()); + return std::make_unique(pkey.release()); +#endif + } + + // EC Key + std::tuple coordinates( + const Group::PublicKey& pk) const override + { + auto bn_x = make_typed_unique(BN_new()); + auto bn_y = make_typed_unique(BN_new()); + const auto& rpk = dynamic_cast(pk); + +#if defined(WITH_OPENSSL3) + // Raw pointer OK here because it becomes managed as soon as possible + OSSL_PARAM* param_ptr = nullptr; + if (1 != EVP_PKEY_todata(rpk.pkey.get(), EVP_PKEY_PUBLIC_KEY, ¶m_ptr)) { + throw openssl_error(); + } + + auto param = make_typed_unique(param_ptr); + + // Raw pointer OK here because it is non-owning + const auto* pk_param = + OSSL_PARAM_locate_const(param.get(), OSSL_PKEY_PARAM_PUB_KEY); + if (pk_param == nullptr) { + throw std::runtime_error("Failed to locate OSSL_PKEY_PARAM_PUB_KEY"); + } + + // Copy the octet string representation of the key into a buffer + auto len = size_t(0); + if (1 != OSSL_PARAM_get_octet_string(pk_param, nullptr, 0, &len)) { + throw std::runtime_error("Failed to get OSSL_PKEY_PARAM_PUB_KEY len"); + } + + auto buf = bytes(len); + auto* buf_ptr = buf.data(); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + auto* buf_ptr_void = reinterpret_cast(buf_ptr); + if (1 != + OSSL_PARAM_get_octet_string(pk_param, &buf_ptr_void, len, nullptr)) { + throw std::runtime_error("Failed to get OSSL_PKEY_PARAM_PUB_KEY data"); + } + + // Parse the octet string representation into an EC_POINT + auto group = make_typed_unique( + EC_GROUP_new_by_curve_name_ex(nullptr, nullptr, curve_nid)); + if (group == nullptr) { + throw openssl_error(); + } + + auto point = make_typed_unique(EC_POINT_new(group.get())); + if (point == nullptr) { + throw openssl_error(); + } + + if (1 != + EC_POINT_oct2point(group.get(), point.get(), buf_ptr, len, nullptr)) { + throw openssl_error(); + } + + // Retrieve the affine coordinates of the point + if (1 != EC_POINT_get_affine_coordinates( + group.get(), point.get(), bn_x.get(), bn_y.get(), nullptr)) { + throw openssl_error(); + } +#else + // Raw pointers are non-owning + auto* pub = EVP_PKEY_get0_EC_KEY(rpk.pkey.get()); + const auto* point = EC_KEY_get0_public_key(pub); + const auto* group = EC_KEY_get0_group(pub); + + if (1 != EC_POINT_get_affine_coordinates_GFp( + group, point, bn_x.get(), bn_y.get(), nullptr)) { + throw openssl_error(); + } +#endif + const auto x_size = BN_num_bytes(bn_x.get()); + auto x = bytes(x_size); + if (BN_bn2bin(bn_x.get(), x.data()) != x_size) { + throw openssl_error(); + } + + const auto y_size = BN_num_bytes(bn_y.get()); + auto y = bytes(y_size); + if (BN_bn2bin(bn_y.get(), y.data()) != y_size) { + throw openssl_error(); + } + + const auto zeros_needed_x = dh_size - x.size(); + const auto zeros_needed_y = dh_size - y.size(); + auto leading_zeros_x = bytes(zeros_needed_x, 0); + auto leading_zeros_y = bytes(zeros_needed_y, 0); + + return { leading_zeros_x + x, leading_zeros_y + y }; + } + + // EC Key + std::unique_ptr public_key_from_coordinates( + const bytes& x, + const bytes& y) const override + { + auto bn_x = make_typed_unique( + BN_bin2bn(x.data(), static_cast(x.size()), nullptr)); + auto bn_y = make_typed_unique( + BN_bin2bn(y.data(), static_cast(y.size()), nullptr)); + + if (bn_x == nullptr || bn_y == nullptr) { + throw std::runtime_error("Failed to convert bn_x or bn_y"); + } + +#if defined(WITH_OPENSSL3) + const auto group = make_typed_unique( + EC_GROUP_new_by_curve_name_ex(nullptr, nullptr, curve_nid)); + if (group == nullptr) { + throw std::runtime_error("Failed to create EC_GROUP"); + } + + // Construct a point with the given coordinates + auto point = make_typed_unique(EC_POINT_new(group.get())); + if (group == nullptr) { + throw std::runtime_error("Failed to create EC_POINT"); + } + + if (1 != EC_POINT_set_affine_coordinates( + group.get(), point.get(), bn_x.get(), bn_y.get(), nullptr)) { + throw openssl_error(); + } + + // Serialize the point + const auto point_size = EC_POINT_point2oct(group.get(), + point.get(), + POINT_CONVERSION_UNCOMPRESSED, + nullptr, + 0, + nullptr); + if (0 == point_size) { + throw openssl_error(); + } + + auto pub = bytes(point_size); + if (EC_POINT_point2oct(group.get(), + point.get(), + POINT_CONVERSION_UNCOMPRESSED, + pub.data(), + point_size, + nullptr) != point_size) { + throw openssl_error(); + } + + // Initialize a public key from the serialized point + auto key = public_evp_key(pub); + return std::make_unique(key.release()); +#else + auto eckey = new_ec_key(); + if (eckey == nullptr) { + throw std::runtime_error("Failed to create EC_KEY"); + } + + // Group pointer is non-owning + const auto* group = EC_KEY_get0_group(eckey.get()); + auto point = make_typed_unique(EC_POINT_new(group)); + + if (1 != EC_POINT_set_affine_coordinates_GFp( + group, point.get(), bn_x.get(), bn_y.get(), nullptr)) { + throw openssl_error(); + } + + if (1 != EC_KEY_set_public_key(eckey.get(), point.get())) { + throw openssl_error(); + } + + auto pkey = to_pkey(eckey.release()); + return std::make_unique(pkey.release()); #endif } @@ -538,17 +714,17 @@ struct ECKeyGroup : public EVPGroup int curve_nid; #if !defined(WITH_OPENSSL3) - EC_KEY* new_ec_key() const + typed_unique_ptr new_ec_key() const { - return EC_KEY_new_by_curve_name(curve_nid); + return make_typed_unique(EC_KEY_new_by_curve_name(curve_nid)); } - static EVP_PKEY* to_pkey(EC_KEY* eckey) + static typed_unique_ptr to_pkey(EC_KEY* eckey) { auto* pkey = EVP_PKEY_new(); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) EVP_PKEY_assign_EC_KEY(pkey, eckey); - return pkey; + return make_typed_unique(pkey); } #endif @@ -659,6 +835,30 @@ struct RawKeyGroup : public EVPGroup return std::make_unique(pkey); } + // Raw Key + std::tuple coordinates( + const Group::PublicKey& pk) const override + { + const auto& rpk = dynamic_cast(pk); + auto raw = bytes(pk_size); + auto* data_ptr = raw.data(); + auto data_len = raw.size(); + + if (1 != EVP_PKEY_get_raw_public_key(rpk.pkey.get(), data_ptr, &data_len)) { + throw openssl_error(); + } + + return { raw, {} }; + } + + // Raw Key + std::unique_ptr public_key_from_coordinates( + const bytes& x, + const bytes& /* y */) const override + { + return deserialize(x); + } + private: const int evp_type; @@ -823,13 +1023,56 @@ group_sk_size(Group::ID group_id) } } +static inline std::string +group_jwk_curve_name(Group::ID group_id) +{ + switch (group_id) { + case Group::ID::P256: + return "P-256"; + case Group::ID::P384: + return "P-384"; + case Group::ID::P521: + return "P-521"; + case Group::ID::Ed25519: + return "Ed25519"; + case Group::ID::Ed448: + return "Ed448"; + case Group::ID::X25519: + return "X25519"; + case Group::ID::X448: + return "X448"; + default: + throw std::runtime_error("Unknown group"); + } +} + +static inline std::string +group_jwk_key_type(Group::ID group_id) +{ + switch (group_id) { + case Group::ID::P256: + case Group::ID::P384: + case Group::ID::P521: + return "EC"; + case Group::ID::Ed25519: + case Group::ID::Ed448: + case Group::ID::X25519: + case Group::ID::X448: + return "OKP"; + default: + throw std::runtime_error("Unknown group"); + } +} + Group::Group(ID group_id_in, const KDF& kdf_in) : id(group_id_in) , dh_size(group_dh_size(group_id_in)) , pk_size(group_pk_size(group_id_in)) , sk_size(group_sk_size(group_id_in)) + , jwk_key_type(group_jwk_key_type(group_id_in)) + , jwk_curve_name(group_jwk_curve_name(group_id_in)) , kdf(kdf_in) { } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/group.h b/lib/hpke/src/group.h index ace8d7a92..d871e1ed8 100644 --- a/lib/hpke/src/group.h +++ b/lib/hpke/src/group.h @@ -2,11 +2,12 @@ #include #include +#include #include "openssl_common.h" #include -namespace hpke { +namespace MLS_NAMESPACE::hpke { struct Group { @@ -43,6 +44,8 @@ struct Group const size_t dh_size; const size_t pk_size; const size_t sk_size; + const std::string jwk_key_type; + const std::string jwk_curve_name; virtual std::unique_ptr generate_key_pair() const = 0; virtual std::unique_ptr derive_key_pair( @@ -63,6 +66,11 @@ struct Group const bytes& sig, const PublicKey& pk) const = 0; + virtual std::tuple coordinates(const PublicKey& pk) const = 0; + virtual std::unique_ptr public_key_from_coordinates( + const bytes& x, + const bytes& y) const = 0; + protected: const KDF& kdf; @@ -106,4 +114,4 @@ struct EVPGroup : public Group const Group::PublicKey& pk) const override; }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/hkdf.cpp b/lib/hpke/src/hkdf.cpp index 382d5bc1c..2961e63ed 100644 --- a/lib/hpke/src/hkdf.cpp +++ b/lib/hpke/src/hkdf.cpp @@ -1,11 +1,12 @@ #include "hkdf.h" #include "openssl_common.h" +#include #include #include #include -namespace hpke { +namespace MLS_NAMESPACE::hpke { template<> const HKDF& @@ -76,4 +77,4 @@ HKDF::expand(const bytes& prk, const bytes& info, size_t size) const return okm; } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/hkdf.h b/lib/hpke/src/hkdf.h index c89a00695..004b55c34 100644 --- a/lib/hpke/src/hkdf.h +++ b/lib/hpke/src/hkdf.h @@ -2,8 +2,9 @@ #include #include +#include -namespace hpke { +namespace MLS_NAMESPACE::hpke { struct HKDF : public KDF { @@ -21,4 +22,4 @@ struct HKDF : public KDF explicit HKDF(const Digest& digest_in); }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/hpke.cpp b/lib/hpke/src/hpke.cpp index dc64b03b8..51100c084 100644 --- a/lib/hpke/src/hpke.cpp +++ b/lib/hpke/src/hpke.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "aead_cipher.h" #include "common.h" @@ -10,7 +11,7 @@ #include #include -namespace hpke { +namespace MLS_NAMESPACE::hpke { /// /// Helper functions and constants @@ -537,4 +538,4 @@ HPKE::key_schedule(Mode mode, return { suite, key, nonce, exporter_secret, kdf, aead }; } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/openssl_common.cpp b/lib/hpke/src/openssl_common.cpp index b120f7a2c..33f62f2d3 100644 --- a/lib/hpke/src/openssl_common.cpp +++ b/lib/hpke/src/openssl_common.cpp @@ -1,4 +1,5 @@ #include "openssl_common.h" +#include #include #include @@ -10,7 +11,7 @@ #include #endif -namespace hpke { +namespace MLS_NAMESPACE::hpke { template<> void @@ -157,4 +158,4 @@ openssl_error() return std::runtime_error(ERR_error_string(code, nullptr)); } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/openssl_common.h b/lib/hpke/src/openssl_common.h index a9413544b..60d75e036 100644 --- a/lib/hpke/src/openssl_common.h +++ b/lib/hpke/src/openssl_common.h @@ -2,9 +2,10 @@ #include #include +#include #include -namespace hpke { +namespace MLS_NAMESPACE::hpke { template void @@ -23,4 +24,4 @@ make_typed_unique(T* ptr) std::runtime_error openssl_error(); -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/random.cpp b/lib/hpke/src/random.cpp index 363653592..d88be35a2 100644 --- a/lib/hpke/src/random.cpp +++ b/lib/hpke/src/random.cpp @@ -1,10 +1,11 @@ #include +#include #include "openssl_common.h" #include -namespace hpke { +namespace MLS_NAMESPACE::hpke { bytes random_bytes(size_t size) @@ -16,4 +17,4 @@ random_bytes(size_t size) return rand; } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/rsa.cpp b/lib/hpke/src/rsa.cpp index 5bb6b5219..2eea958de 100644 --- a/lib/hpke/src/rsa.cpp +++ b/lib/hpke/src/rsa.cpp @@ -3,8 +3,9 @@ #include "common.h" #include "openssl/rsa.h" #include "openssl_common.h" +#include -namespace hpke { +namespace MLS_NAMESPACE::hpke { std::unique_ptr RSASignature::generate_key_pair() const @@ -146,6 +147,32 @@ RSASignature::verify(const bytes& data, return rv == 1; } +// TODO(RLB) Implement these methods. No concrete need, but might be nice for +// completeness. +std::unique_ptr +RSASignature::import_jwk_private(const std::string& /* json_str */) const +{ + throw std::runtime_error("not implemented"); +} + +std::unique_ptr +RSASignature::import_jwk(const std::string& /* json_str */) const +{ + throw std::runtime_error("not implemented"); +} + +std::string +RSASignature::export_jwk_private(const Signature::PrivateKey& /* sk */) const +{ + throw std::runtime_error("not implemented"); +} + +std::string +RSASignature::export_jwk(const Signature::PublicKey& /* pk */) const +{ + throw std::runtime_error("not implemented"); +} + const EVP_MD* RSASignature::digest_to_md(Digest::ID digest) { @@ -177,4 +204,5 @@ RSASignature::digest_to_sig(Digest::ID digest) throw std::runtime_error("Unsupported digest"); } } -} // namespace hpke + +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/rsa.h b/lib/hpke/src/rsa.h index 684435ae3..44eaf382c 100644 --- a/lib/hpke/src/rsa.h +++ b/lib/hpke/src/rsa.h @@ -3,12 +3,13 @@ #include #include #include +#include #include "openssl_common.h" #include #include -namespace hpke { +namespace MLS_NAMESPACE::hpke { // XXX(RLB): There is a lot of code in RSASignature that is duplicated in // EVPGroup. I have allowed this duplication rather than factoring it out @@ -78,6 +79,14 @@ struct RSASignature : public Signature const bytes& sig, const Signature::PublicKey& pk) const override; + std::unique_ptr import_jwk_private( + const std::string& json_str) const override; + std::unique_ptr import_jwk( + const std::string& json_str) const override; + std::string export_jwk_private( + const Signature::PrivateKey& sk) const override; + std::string export_jwk(const Signature::PublicKey& pk) const override; + private: const EVP_MD* md; @@ -86,4 +95,4 @@ struct RSASignature : public Signature static Signature::ID digest_to_sig(Digest::ID digest); }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/signature.cpp b/lib/hpke/src/signature.cpp index 6b71a0920..73384f3f7 100644 --- a/lib/hpke/src/signature.cpp +++ b/lib/hpke/src/signature.cpp @@ -1,15 +1,22 @@ +#include #include #include +#include +#include #include "dhkem.h" - -#include "common.h" -#include "group.h" #include "rsa.h" + +#include +#include +#include +#include #include #include -namespace hpke { +using nlohmann::json; + +namespace MLS_NAMESPACE::hpke { struct GroupSignature : public Signature { @@ -105,8 +112,96 @@ struct GroupSignature : public Signature return group.verify(data, sig, rpk); } + std::unique_ptr import_jwk_private( + const std::string& jwk_json) const override + { + const auto jwk = validate_jwk_json(jwk_json, true); + + const auto d = from_base64url(jwk.at("d")); + auto gsk = group.deserialize_private(d); + + return std::make_unique(gsk.release()); + } + + std::unique_ptr import_jwk( + const std::string& jwk_json) const override + { + const auto jwk = validate_jwk_json(jwk_json, false); + + const auto x = from_base64url(jwk.at("x")); + auto y = bytes{}; + if (jwk.contains("y")) { + y = from_base64url(jwk.at("y")); + } + + return group.public_key_from_coordinates(x, y); + } + + std::string export_jwk(const Signature::PublicKey& pk) const override + { + const auto& gpk = dynamic_cast(pk); + const auto jwk_json = export_jwk_json(gpk); + return jwk_json.dump(); + } + + std::string export_jwk_private(const Signature::PrivateKey& sk) const override + { + const auto& gssk = dynamic_cast(sk); + const auto& gsk = gssk.group_priv; + const auto gpk = gsk->public_key(); + + auto jwk_json = export_jwk_json(*gpk); + + // encode the private key + const auto enc = serialize_private(sk); + jwk_json.emplace("d", to_base64url(enc)); + + return jwk_json.dump(); + } + private: const Group& group; + + json validate_jwk_json(const std::string& jwk_json, bool private_key) const + { + json jwk = json::parse(jwk_json); + + if (jwk.empty() || !jwk.contains("kty") || !jwk.contains("crv") || + !jwk.contains("x") || (private_key && !jwk.contains("d"))) { + throw std::runtime_error("malformed JWK"); + } + + if (jwk.at("kty") != group.jwk_key_type) { + throw std::runtime_error("invalid JWK key type"); + } + + if (jwk.at("crv") != group.jwk_curve_name) { + throw std::runtime_error("invalid JWK curve"); + } + + return jwk; + } + + json export_jwk_json(const Group::PublicKey& pk) const + { + const auto [x, y] = group.coordinates(pk); + + json jwk = json::object({ + { "crv", group.jwk_curve_name }, + { "kty", group.jwk_key_type }, + }); + + if (group.jwk_key_type == "EC") { + jwk.emplace("x", to_base64url(x)); + jwk.emplace("y", to_base64url(y)); + } else if (group.jwk_key_type == "OKP") { + jwk.emplace("x", to_base64url(x)); + } else { + throw std::runtime_error("unknown key type"); + } + + return jwk; + } }; template<> @@ -180,22 +275,70 @@ Signature::Signature(Signature::ID id_in) { } -bytes -Signature::serialize_private(const PrivateKey& /* unused */) const +std::unique_ptr +Signature::generate_rsa(size_t bits) { - throw std::runtime_error("Not implemented"); + return RSASignature::generate_key_pair(bits); } -std::unique_ptr -Signature::deserialize_private(const bytes& /* unused */) const +static const Signature& +sig_from_jwk(const std::string& jwk_json) { - throw std::runtime_error("Not implemented"); + using KeyTypeAndCurve = std::tuple; + static const auto alg_sig_map = std::map{ + { { "EC", "P-256" }, Signature::get() }, + { { "EC", "P-384" }, Signature::get() }, + { { "EC", "P-512" }, Signature::get() }, + { { "OKP", "Ed25519" }, Signature::get() }, +#if !defined(WITH_BORINGSSL) + { { "OKP", "Ed448" }, Signature::get() }, +#endif + // TODO(RLB): RSA + }; + + const auto jwk = json::parse(jwk_json); + const auto& kty = jwk.at("kty"); + + auto crv = std::string(""); + if (jwk.contains("crv")) { + crv = jwk.at("crv"); + } + + const auto key = KeyTypeAndCurve{ kty, crv }; + return alg_sig_map.at(key); } -std::unique_ptr -Signature::generate_rsa(size_t bits) +Signature::PrivateJWK +Signature::parse_jwk_private(const std::string& jwk_json) { - return RSASignature::generate_key_pair(bits); + // XXX(RLB): This JSON-parses the JWK twice. I'm assuming that this is a less + // bad cost than changing the import_jwk method signature to take `json`. + const auto& sig = sig_from_jwk(jwk_json); + const auto jwk = json::parse(jwk_json); + auto priv = sig.import_jwk_private(jwk_json); + + auto kid = std::optional{}; + if (jwk.contains("kid")) { + kid = jwk.at("kid").get(); + } + + return { sig, kid, std::move(priv) }; +} + +Signature::PublicJWK +Signature::parse_jwk(const std::string& jwk_json) +{ + // XXX(RLB): Same double-parsing comment as with `parse_jwk_private` + const auto& sig = sig_from_jwk(jwk_json); + const auto jwk = json::parse(jwk_json); + auto pub = sig.import_jwk(jwk_json); + + auto kid = std::optional{}; + if (jwk.contains("kid")) { + kid = jwk.at("kid").get(); + } + + return { sig, kid, std::move(pub) }; } -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/src/userinfo_vc.cpp b/lib/hpke/src/userinfo_vc.cpp new file mode 100644 index 000000000..cbba475a3 --- /dev/null +++ b/lib/hpke/src/userinfo_vc.cpp @@ -0,0 +1,406 @@ +#include +#include +#include +#include +#include + +using nlohmann::json; + +namespace MLS_NAMESPACE::hpke { + +static const std::string name_attr = "name"; +static const std::string sub_attr = "sub"; +static const std::string given_name_attr = "given_name"; +static const std::string family_name_attr = "family_name"; +static const std::string middle_name_attr = "middle_name"; +static const std::string nickname_attr = "nickname"; +static const std::string preferred_username_attr = "preferred_username"; +static const std::string profile_attr = "profile"; +static const std::string picture_attr = "picture"; +static const std::string website_attr = "website"; +static const std::string email_attr = "email"; +static const std::string email_verified_attr = "email_verified"; +static const std::string gender_attr = "gender"; +static const std::string birthdate_attr = "birthdate"; +static const std::string zoneinfo_attr = "zoneinfo"; +static const std::string locale_attr = "locale"; +static const std::string phone_number_attr = "phone_number"; +static const std::string phone_number_verified_attr = "phone_number_verified"; +static const std::string address_attr = "address"; +static const std::string address_formatted_attr = "formatted"; +static const std::string address_street_address_attr = "street_address"; +static const std::string address_locality_attr = "locality"; +static const std::string address_region_attr = "region"; +static const std::string address_postal_code_attr = "postal_code"; +static const std::string address_country_attr = "country"; +static const std::string updated_at_attr = "updated_at"; + +template +static std::optional +get_optional(const json& json_object, const std::string& field_name) +{ + if (!json_object.contains(field_name)) { + return std::nullopt; + } + + return { json_object.at(field_name).get() }; +} + +/// +/// ParsedCredential +/// +static const Signature& +signature_from_alg(const std::string& alg) +{ + static const auto alg_sig_map = std::map{ + { "ES256", Signature::get() }, + { "ES384", Signature::get() }, + { "ES512", Signature::get() }, + { "Ed25519", Signature::get() }, +#if !defined(WITH_BORINGSSL) + { "Ed448", Signature::get() }, +#endif + { "RS256", Signature::get() }, + { "RS384", Signature::get() }, + { "RS512", Signature::get() }, + }; + + return alg_sig_map.at(alg); +} + +static std::chrono::system_clock::time_point +epoch_time(int64_t seconds_since_epoch) +{ + const auto delta = std::chrono::seconds(seconds_since_epoch); + return std::chrono::system_clock::time_point(delta); +} + +static bool +is_ecdsa(const Signature& sig) +{ + return sig.id == Signature::ID::P256_SHA256 || + sig.id == Signature::ID::P384_SHA384 || + sig.id == Signature::ID::P521_SHA512; +} + +// OpenSSL expects ECDSA signatures to be in DER form. JWS provides the +// signature in raw R||S form. So we need to do some manual DER encoding. +static bytes +jws_to_der_sig(const bytes& jws_sig) +{ + // Inputs that are too large will result in invalid DER encodings with this + // code. At this size, the combination of the DER integer headers and the + // integer data will overflow the one-byte DER struct length. + static const auto max_sig_size = size_t(250); + if (jws_sig.size() > max_sig_size) { + throw std::runtime_error("JWS signature too large"); + } + + if (jws_sig.size() % 2 != 0) { + throw std::runtime_error("Malformed JWS signature"); + } + + const auto int_size = jws_sig.size() / 2; + const auto jws_sig_cut = + jws_sig.begin() + static_cast(int_size); + + // Compute the encoded size of R and S integer data, adding a zero byte if + // needed to clear the sign bit + const auto r_big = (jws_sig.at(0) >= 0x80); + const auto s_big = (jws_sig.at(int_size) >= 0x80); + + const auto r_size = int_size + (r_big ? 1 : 0); + const auto s_size = int_size + (s_big ? 1 : 0); + + // Compute the size of the DER-encoded signature + static const auto int_header_size = 2; + const auto r_int_size = int_header_size + r_size; + const auto s_int_size = int_header_size + s_size; + + const auto content_size = r_int_size + s_int_size; + const auto content_big = (content_size > 0x80); + + auto der_header_size = 2 + (content_big ? 1 : 0); + const auto der_size = der_header_size + content_size; + + // Allocate the DER buffer + auto der = bytes(der_size, 0); + + // Write the header + der.at(0) = 0x30; + if (content_big) { + der.at(1) = 0x81; + der.at(2) = static_cast(content_size); + } else { + der.at(1) = static_cast(content_size); + } + + // Write R, virtually padding with a zero byte if needed + const auto r_start = der_header_size; + const auto r_data_start = r_start + int_header_size + (r_big ? 1 : 0); + const auto r_data_begin = + der.begin() + static_cast(r_data_start); + + der.at(r_start) = 0x02; + der.at(r_start + 1) = static_cast(r_size); + std::copy(jws_sig.begin(), jws_sig_cut, r_data_begin); + + // Write S, virtually padding with a zero byte if needed + const auto s_start = der_header_size + r_int_size; + const auto s_data_start = s_start + int_header_size + (s_big ? 1 : 0); + const auto s_data_begin = + der.begin() + static_cast(s_data_start); + + der.at(s_start) = 0x02; + der.at(s_start + 1) = static_cast(s_size); + std::copy(jws_sig_cut, jws_sig.end(), s_data_begin); + + return der; +} + +struct UserInfoVC::ParsedCredential +{ + // Header fields + const Signature& signature_algorithm; // `alg` + std::optional key_id; // `kid` + + // Top-level Payload fields + std::string issuer; // `iss` + std::chrono::system_clock::time_point not_before; // `nbf` + std::chrono::system_clock::time_point not_after; // `exp` + + // Credential subject fields + UserInfoClaims credential_subject; + Signature::PublicJWK public_key; + + // Signature verification information + bytes to_be_signed; + bytes signature; + + ParsedCredential(const Signature& signature_algorithm_in, + std::optional key_id_in, + std::string issuer_in, + std::chrono::system_clock::time_point not_before_in, + std::chrono::system_clock::time_point not_after_in, + UserInfoClaims credential_subject_in, + Signature::PublicJWK&& public_key_in, + bytes to_be_signed_in, + bytes signature_in) + : signature_algorithm(signature_algorithm_in) + , key_id(std::move(key_id_in)) + , issuer(std::move(issuer_in)) + , not_before(not_before_in) + , not_after(not_after_in) + , credential_subject(std::move(credential_subject_in)) + , public_key(std::move(public_key_in)) + , to_be_signed(std::move(to_be_signed_in)) + , signature(std::move(signature_in)) + { + } + + static std::shared_ptr parse(const std::string& jwt) + { + // Split the JWT into its header, payload, and signature + const auto first_dot = jwt.find_first_of('.'); + const auto last_dot = jwt.find_last_of('.'); + if (first_dot == std::string::npos || last_dot == std::string::npos || + first_dot == last_dot || last_dot > jwt.length() - 2) { + throw std::runtime_error("malformed JWT; not enough '.' characters"); + } + + const auto header_b64 = jwt.substr(0, first_dot); + const auto payload_b64 = + jwt.substr(first_dot + 1, last_dot - first_dot - 1); + const auto signature_b64 = jwt.substr(last_dot + 1); + + // Parse the components + const auto header = json::parse(from_base64url(header_b64)); + const auto payload = json::parse(from_base64url(payload_b64)); + + // Prepare the validation inputs + const auto& sig = signature_from_alg(header.at("alg")); + const auto to_be_signed = from_ascii(header_b64 + "." + payload_b64); + auto signature = from_base64url(signature_b64); + if (is_ecdsa(sig)) { + signature = jws_to_der_sig(signature); + } + + auto kid = std::optional{}; + if (header.contains("kid")) { + kid = header.at("kid").get(); + } + + // Verify the VC parts + const auto& vc = payload.at("vc"); + + static const auto context = + std::vector{ { "https://www.w3.org/2018/credentials/v1" } }; + const auto vc_context = vc.at("@context").get>(); + if (vc_context != context) { + throw std::runtime_error("malformed VC: incorrect context value"); + } + + static const auto type = std::vector{ + "VerifiableCredential", + "UserInfoCredential", + }; + if (vc.at("type") != type) { + throw std::runtime_error("malformed VC: incorrect type value"); + } + + // Parse the subject public key + static const std::string did_jwk_prefix = "did:jwk:"; + const auto id = vc.at("credentialSubject").at("id").get(); + if (id.find(did_jwk_prefix) != 0) { + throw std::runtime_error("malformed UserInfo VC: ID is not did:jwk"); + } + + const auto jwk = to_ascii(from_base64url(id.substr(did_jwk_prefix.size()))); + auto public_key = Signature::parse_jwk(jwk); + + // Extract the salient parts + return std::make_shared( + sig, + kid, + + payload.at("iss"), + epoch_time(payload.at("nbf").get()), + epoch_time(payload.at("exp").get()), + + UserInfoClaims::from_json(vc.at("credentialSubject")), + std::move(public_key), + + to_be_signed, + signature); + } + + bool verify(const Signature::PublicKey& issuer_key) + { + return signature_algorithm.verify(to_be_signed, signature, issuer_key); + } +}; + +/// +/// UserInfoClaimsAddress +/// +UserInfoClaimsAddress +UserInfoClaimsAddress::from_json(const nlohmann::json& address_json) +{ + return { + get_optional(address_json, address_formatted_attr), + get_optional(address_json, address_street_address_attr), + get_optional(address_json, address_locality_attr), + get_optional(address_json, address_region_attr), + get_optional(address_json, address_postal_code_attr), + get_optional(address_json, address_country_attr), + }; +} + +/// +/// UserInfoClaims +/// +UserInfoClaims +UserInfoClaims::from_json(const nlohmann::json& cred_subject_json) +{ + std::optional address_opt = {}; + + if (cred_subject_json.contains(address_attr)) { + address_opt = + UserInfoClaimsAddress::from_json(cred_subject_json.at(address_attr)); + } + + return { + get_optional(cred_subject_json, sub_attr), + get_optional(cred_subject_json, name_attr), + get_optional(cred_subject_json, given_name_attr), + get_optional(cred_subject_json, family_name_attr), + get_optional(cred_subject_json, middle_name_attr), + get_optional(cred_subject_json, nickname_attr), + get_optional(cred_subject_json, preferred_username_attr), + get_optional(cred_subject_json, profile_attr), + get_optional(cred_subject_json, picture_attr), + get_optional(cred_subject_json, website_attr), + get_optional(cred_subject_json, email_attr), + get_optional(cred_subject_json, email_verified_attr), + get_optional(cred_subject_json, gender_attr), + get_optional(cred_subject_json, birthdate_attr), + get_optional(cred_subject_json, zoneinfo_attr), + get_optional(cred_subject_json, locale_attr), + get_optional(cred_subject_json, phone_number_attr), + get_optional(cred_subject_json, phone_number_verified_attr), + address_opt, + get_optional(cred_subject_json, updated_at_attr), + }; +} + +/// +/// UserInfoVC +/// + +UserInfoVC::UserInfoVC(std::string jwt) + : parsed_cred(ParsedCredential::parse(jwt)) + , raw(std::move(jwt)) +{ +} + +const Signature& +UserInfoVC::signature_algorithm() const +{ + return parsed_cred->signature_algorithm; +} + +std::string +UserInfoVC::issuer() const +{ + return parsed_cred->issuer; +} + +std::optional +UserInfoVC::key_id() const +{ + return parsed_cred->key_id; +} + +bool +UserInfoVC::valid_from(const Signature::PublicKey& issuer_key) const +{ + return parsed_cred->verify(issuer_key); +} + +const std::string& +UserInfoVC::raw_credential() const +{ + return raw; +} + +const UserInfoClaims& +UserInfoVC::subject() const +{ + return parsed_cred->credential_subject; +} + +std::chrono::system_clock::time_point +UserInfoVC::not_before() const +{ + return parsed_cred->not_before; +} + +std::chrono::system_clock::time_point +UserInfoVC::not_after() const +{ + return parsed_cred->not_after; +} + +const Signature::PublicJWK& +UserInfoVC::public_key() const +{ + return parsed_cred->public_key; +} + +bool +operator==(const UserInfoVC& lhs, const UserInfoVC& rhs) +{ + return lhs.raw == rhs.raw; +} + +} // namespace MLS_NAMESPACE::hpke diff --git a/lib/hpke/test/base64.cpp b/lib/hpke/test/base64.cpp new file mode 100644 index 000000000..6ac06a70e --- /dev/null +++ b/lib/hpke/test/base64.cpp @@ -0,0 +1,32 @@ +#include +#include + +using namespace MLS_NAMESPACE::hpke; +using namespace MLS_NAMESPACE::bytes_ns; + +TEST_CASE("To Base64 / To Base64Url") +{ + struct KnownAnswerTest + { + bytes data; + std::string base64; + std::string base64u; + }; + + const std::vector cases{ + { from_ascii("hello there"), "aGVsbG8gdGhlcmU=", "aGVsbG8gdGhlcmU" }, + { from_ascii("A B C D E F "), "QSBCIEMgRCBFIEYg", "QSBCIEMgRCBFIEYg" }, + { from_ascii("hello\xfethere"), "aGVsbG/+dGhlcmU=", "aGVsbG_-dGhlcmU" }, + { from_ascii("\xfe"), "/g==", "_g" }, + { from_ascii("\x01\x02"), "AQI=", "AQI" }, + { from_ascii("\x01"), "AQ==", "AQ" }, + { from_ascii(""), "", "" }, + }; + + for (const auto& tc : cases) { + REQUIRE(to_base64(tc.data) == tc.base64); + REQUIRE(to_base64url(tc.data) == tc.base64u); + REQUIRE(from_base64(tc.base64) == tc.data); + REQUIRE(from_base64url(tc.base64u) == tc.data); + } +} diff --git a/lib/hpke/test/certificate.cpp b/lib/hpke/test/certificate.cpp index 5df2bd8c5..d3ab39775 100644 --- a/lib/hpke/test/certificate.cpp +++ b/lib/hpke/test/certificate.cpp @@ -8,7 +8,7 @@ #include #include -namespace opt = tls::opt; +namespace opt = MLS_NAMESPACE::tls::opt; TEST_CASE("Certificate Known-Answer depth 2") { diff --git a/lib/hpke/test/common.h b/lib/hpke/test/common.h index 3b6487c76..b26f0bfdd 100644 --- a/lib/hpke/test/common.h +++ b/lib/hpke/test/common.h @@ -1,9 +1,9 @@ #include #include -using namespace hpke; +using namespace MLS_NAMESPACE::hpke; #include -using namespace bytes_ns; +using namespace MLS_NAMESPACE::bytes_ns; void ensure_fips_if_required(); diff --git a/lib/hpke/test/hpke.cpp b/lib/hpke/test/hpke.cpp index 42d595cde..93c63645b 100644 --- a/lib/hpke/test/hpke.cpp +++ b/lib/hpke/test/hpke.cpp @@ -1,10 +1,11 @@ #include #include +#include #include "common.h" #include "test_vectors.h" -namespace hpke { +namespace MLS_NAMESPACE::hpke { struct HPKETest { @@ -16,18 +17,18 @@ struct HPKETest } }; -} // namespace hpke +} // namespace MLS_NAMESPACE::hpke static void test_context(ReceiverContext& ctxR, const HPKETestVector& tv) { - auto key = hpke::HPKETest::key(ctxR); + auto key = MLS_NAMESPACE::hpke::HPKETest::key(ctxR); REQUIRE(key == tv.key); - auto nonce = hpke::HPKETest::nonce(ctxR); + auto nonce = MLS_NAMESPACE::hpke::HPKETest::nonce(ctxR); REQUIRE(nonce == tv.nonce); - auto exporter_secret = hpke::HPKETest::exporter_secret(ctxR); + auto exporter_secret = MLS_NAMESPACE::hpke::HPKETest::exporter_secret(ctxR); REQUIRE(exporter_secret == tv.exporter_secret); for (const auto& enc : tv.encryptions) { @@ -151,7 +152,8 @@ TEST_CASE("HPKE Test Vectors") ensure_fips_if_required(); auto test_vector_bytes = bytes(test_vector_data); - auto test_vectors = tls::get(test_vector_bytes); + auto test_vectors = + MLS_NAMESPACE::tls::get(test_vector_bytes); for (const auto& tv : test_vectors.vectors) { if (fips() && fips_disable(tv.aead_id)) { diff --git a/lib/hpke/test/random.cpp b/lib/hpke/test/random.cpp index d7cd0b42a..48b869e12 100644 --- a/lib/hpke/test/random.cpp +++ b/lib/hpke/test/random.cpp @@ -8,6 +8,6 @@ TEST_CASE("Random bytes") ensure_fips_if_required(); auto size = size_t(128); - auto test_val = hpke::random_bytes(size); + auto test_val = MLS_NAMESPACE::hpke::random_bytes(size); CHECK(test_val.size() == size); } diff --git a/lib/hpke/test/signature.cpp b/lib/hpke/test/signature.cpp index d7d854c15..eee3c361f 100644 --- a/lib/hpke/test/signature.cpp +++ b/lib/hpke/test/signature.cpp @@ -1,9 +1,13 @@ #include #include +#include + +#include #include "common.h" -#include +using nlohmann::json; + TEST_CASE("Signature Known-Answer") { ensure_fips_if_required(); @@ -265,3 +269,141 @@ TEST_CASE("Signature Round-Trip") CHECK(sig.verify(data, signature2, *pub3)); } } + +TEST_CASE("Signature Key JWK Round-Trip") +{ + ensure_fips_if_required(); + const std::vector ids{ + Signature::ID::P256_SHA256, Signature::ID::P384_SHA384, + Signature::ID::P521_SHA512, Signature::ID::Ed25519, +#if !defined(WITH_BORINGSSL) + Signature::ID::Ed448, +#endif + }; + + for (const auto& id : ids) { + if (fips() && fips_disable(id)) { + continue; + } + + const auto& sig = select_signature(id); + + const auto priv = sig.generate_key_pair(); + const auto encoded_priv = sig.export_jwk_private(*priv); + const auto decoded_priv = sig.import_jwk_private(encoded_priv); + CHECK(sig.serialize_private(*priv) == sig.serialize_private(*decoded_priv)); + + const auto pub = priv->public_key(); + const auto encoded_pub = sig.export_jwk(*pub); + const auto decoded_pub = sig.import_jwk(encoded_pub); + CHECK(sig.serialize(*pub) == sig.serialize(*decoded_pub)); + } +} + +TEST_CASE("Signature Key JWK Known-Answer") +{ + ensure_fips_if_required(); + + struct KnownAnswerTest + { + Signature::ID id; + std::string jwk_priv; + std::string jwk_pub; + }; + + // XXX(RLB) clang-format wants to indent this really far to the right. + // clang-format off + const std::vector cases{ + { + Signature::ID::P256_SHA256, + R"({ + "crv": "P-256", + "kty": "EC", + "d": "1J2hVVHFHYyeyfPmcXMG0uHLBn3i742jbRyUBGe2Jmg", + "x": "Bo8xjILpecBFkEwDa0H8p0_xCv7UPE2XMj8KcEe_LGE", + "y":"LbiO9VbkY1eRrR6UgdL7W4jhqej6Sfu6n6HvTRm5ySs" + })", + R"({ + "crv": "P-256", + "kty": "EC", + "x": "Bo8xjILpecBFkEwDa0H8p0_xCv7UPE2XMj8KcEe_LGE", + "y":"LbiO9VbkY1eRrR6UgdL7W4jhqej6Sfu6n6HvTRm5ySs" + })", + }, + { + Signature::ID::P384_SHA384, + R"({ + "crv": "P-384", + "kty": "EC", + "d": "uLHpTwv2Pucmo4ZVgO-0IYJQEyKQlJdukO1M6vZp0EYyvgCJhzoYWsBTbIUEOG8e", + "x": "apvkQ2jdEkTr5MkLePjAt2vW-Sk_djA7WjcPN_Waw_MS327r3WFCrchYEFvrMLru", + "y":"In9wbO3tnH9CQn0NMJm3DvgLtBF3OmcfOnW9sAy1I1-yHVs_N8Bph2Z46YbJGbdS" + })", + R"({ + "crv": "P-384", + "kty": "EC", + "x": "apvkQ2jdEkTr5MkLePjAt2vW-Sk_djA7WjcPN_Waw_MS327r3WFCrchYEFvrMLru", + "y":"In9wbO3tnH9CQn0NMJm3DvgLtBF3OmcfOnW9sAy1I1-yHVs_N8Bph2Z46YbJGbdS" + })", + }, + { + Signature::ID::P521_SHA512, + R"({ + "crv": "P-521", + "kty": "EC", + "d": "AckbSdZaMoA4ylPYStWyAUU20Wo5Yu8hsiocCmhqNniJ42ZXYzZ4EzQElB_sU6RGbPgwQmiKWTZ0jbi5NmZTC6mv", + "x": "AH9tp56TOvXzN_JWFGEAmk0HdkpDSPZBZOwX-xuT3RV0JjFVq3VWzMtMrf_x_Okt5QVNLBi-41no47VDDTK5UQiM", + "y":"AJtFy8ogaNhBxLx6CtjoOG5ptR3-z7CEz9I9ioIOJ9Q2a0vtixuNGa4ILQUY8vz-5_AuqHEWkIaK3sqrryYGPqdH" + })", + R"({ + "crv": "P-521", + "kty": "EC", + "x": "AH9tp56TOvXzN_JWFGEAmk0HdkpDSPZBZOwX-xuT3RV0JjFVq3VWzMtMrf_x_Okt5QVNLBi-41no47VDDTK5UQiM", + "y":"AJtFy8ogaNhBxLx6CtjoOG5ptR3-z7CEz9I9ioIOJ9Q2a0vtixuNGa4ILQUY8vz-5_AuqHEWkIaK3sqrryYGPqdH" + })", + }, + { + Signature::ID::Ed25519, + R"({ + "crv": "Ed25519", + "kty": "OKP", + "d": "kH7Q5NRfGgX1GSTTSz7ofQuQZNnLE5jWKP_RKT76Um8", + "x":"Rv-rXr1oUaa29TdaXzIhJEOV3eJYzMqy_luvV0T80no" + })", + R"({ + "crv": "Ed25519", + "kty": "OKP", + "x": "Rv-rXr1oUaa29TdaXzIhJEOV3eJYzMqy_luvV0T80no" + })", + }, +#if !defined(WITH_BORINGSSL) + { + Signature::ID::Ed448, + R"({ + "crv": "Ed448", + "kty": "OKP", + "d": "NSnqhxrVgCDdREhKNkRsegr5o4WGIMUIdG9enGCBb413B4KgJ8URiZVPaVn0-AslPgHdkF--oFGV", + "x":"0P_035gQbBr8mHe_4uLu8wUI22JiSBYWq9Yzb3Tr3C4ksfv85Xo5OIRrWzE-L0QFRez78nrNA4wA" + })", + R"({ + "crv": "Ed448", + "kty": "OKP", + "x":"0P_035gQbBr8mHe_4uLu8wUI22JiSBYWq9Yzb3Tr3C4ksfv85Xo5OIRrWzE-L0QFRez78nrNA4wA" + })", + }, +#endif + }; + // clang-format on + + for (const auto& tc : cases) { + const auto& sig = select_signature(tc.id); + + auto imported_priv = sig.import_jwk_private(tc.jwk_priv); + auto exported_priv = sig.export_jwk_private(*imported_priv); + CHECK(json::parse(tc.jwk_priv) == json::parse(exported_priv)); + + auto imported_pub = sig.import_jwk(tc.jwk_pub); + auto exported_pub = sig.export_jwk(*imported_pub); + CHECK(json::parse(tc.jwk_pub) == json::parse(exported_pub)); + } +} diff --git a/lib/hpke/test/userinfo_vc.cpp b/lib/hpke/test/userinfo_vc.cpp new file mode 100644 index 000000000..f083f5730 --- /dev/null +++ b/lib/hpke/test/userinfo_vc.cpp @@ -0,0 +1,227 @@ +#include +#include +#include + +#include +namespace opt = MLS_NAMESPACE::tls::opt; + +using namespace MLS_NAMESPACE::hpke; +using namespace std::string_literals; + +static bool +operator==(const Signature::PublicJWK& lhs, const Signature::PublicJWK& rhs) +{ + const auto sig = (&lhs.sig == &rhs.sig); + const auto kid = ((!lhs.key_id && !rhs.key_id) || lhs.key_id == rhs.key_id); + + const auto pkL = lhs.sig.serialize(*lhs.key); + const auto pkR = rhs.sig.serialize(*rhs.key); + const auto key = (pkL == pkR); + + return sig && kid && key; +} + +TEST_CASE("UserInfoVC Parsing and Validation") +{ + // Parsed contents: + // + // Protected header: + // { + // "alg": "ES256", + // "typ": "JWT", + // "kid": "gyAKXvQA8X-m9JxDBgv9rULPxlU7fjB9O7D_gmIrDXs" + // } + // + // Payload: + // { + // "vc": { + // "@context": [ + // "https://www.w3.org/2018/credentials/v1" + // ], + // "type": [ + // "VerifiableCredential", + // "UserInfoCredential" + // ], + // "credentialSubject": { + // "id": + // "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InAxOXJZemVDYnZ5" + // "VHpyWGtqTGIyVkRGYllEc20yVFpxSURselQyQnEzQUEiLCJ5IjoiVVVnRmdwWjZ3" + // "WndHZkstWE4tVWtJSlVnTHlwZ3o2MW5xVWY4M1Nza2poRSJ9", + // "sub": "248289761001", + // "name": "Jane Doe", + // "given_name": "Jane", + // "family_name": "Doe", + // "preferred_username": "j.doe", + // "email": "janedoe@example.com", + // "picture": "http://example.com/janedoe/me.jpg" + // } + // }, + // "nbf": 1693420220, + // "exp": 1694025020, + // "iss": "https://localhost:3000", + // "aud": "client_id", + // "iat": 1693506620 + // } + const auto userinfo_vc_raw = + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imd5QUtYdlFBOFgtbTlK" + "eERCZ3Y5clVMUHhsVTdmakI5TzdEX2dtSXJEWHMifQ" + "." + "eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVk" + "ZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVXNl" + "ckluZm9DcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlk" + "Omp3azpleUpyZEhraU9pSkZReUlzSW1OeWRpSTZJbEF0TWpVMklpd2llQ0k2SW5B" + "eE9YSlplbVZEWW5aNVZIcHlXR3RxVEdJeVZrUkdZbGxFYzIweVZGcHhTVVJzZWxR" + "eVFuRXpRVUVpTENKNUlqb2lWVlZuUm1kd1dqWjNXbmRIWmtzdFdFNHRWV3RKU2xW" + "blRIbHdaM28yTVc1eFZXWTRNMU56YTJwb1JTSjkiLCJzdWIiOiIyNDgyODk3NjEw" + "MDEiLCJuYW1lIjoiSmFuZSBEb2UiLCJnaXZlbl9uYW1lIjoiSmFuZSIsImZhbWls" + "eV9uYW1lIjoiRG9lIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiai5kb2UiLCJlbWFp" + "bCI6ImphbmVkb2VAZXhhbXBsZS5jb20iLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1w" + "bGUuY29tL2phbmVkb2UvbWUuanBnIn19LCJuYmYiOjE2OTM0MjAyMjAsImV4cCI6" + "MTY5NDAyNTAyMCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6MzAwMCIsImF1ZCI6" + "ImNsaWVudF9pZCIsImlhdCI6MTY5MzUwNjYyMH0" + "." + "lSU3pbjPCcBpQID6w1WeAYO_ZyYRDZ3rsJiPD1uWPOILWzeUIHTwjjyqaL9sko9k" + "FV0Xch-16qwdOlpTgzaHrw"s; + + const auto known_issuer = "https://localhost:3000"s; + const auto known_key_id = "gyAKXvQA8X-m9JxDBgv9rULPxlU7fjB9O7D_gmIrDXs"s; + const auto known_not_before = std::chrono::seconds(1693420220); + const auto known_not_after = std::chrono::seconds(1694025020); + const auto known_subject = std::map{ + { { "id", + "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6InAxOXJZemVDYnZ5" + "VHpyWGtqTGIyVkRGYllEc20yVFpxSURselQyQnEzQUEiLCJ5IjoiVVVnRmdwWjZ3" + "WndHZkstWE4tVWtJSlVnTHlwZ3o2MW5xVWY4M1Nza2poRSJ9" }, + { "sub", "248289761001" }, + { "name", "Jane Doe" }, + { "given_name", "Jane" }, + { "family_name", "Doe" }, + { "preferred_username", "j.doe" }, + { "email", "janedoe@example.com" }, + { "picture", "http://example.com/janedoe/me.jpg" } } + }; + const auto known_subject_jwk_raw = R"({ + "kty": "EC", + "crv": "P-256", + "x": "p19rYzeCbvyTzrXkjLb2VDFbYDsm2TZqIDlzT2Bq3AA", + "y": "UUgFgpZ6wZwGfK-XN-UkIJUgLypgz61nqUf83SskjhE" + })"s; + + const auto issuer_jwk_raw = R"({ + "kty": "EC", + "use": "sig", + "kid": "gyAKXvQA8X-m9JxDBgv9rULPxlU7fjB9O7D_gmIrDXs", + "alg": "ES256", + "crv": "P-256", + "x": "2-MG_vi7KtZNzbwrbT2JX4kJTw7iJcnVXj7ucBZHUCg", + "y":"ZwQq_CgT-1vfeE77uoWGM9Pm-8DyH7p-SIi1RKHEB8E" + })"s; + + const auto vc = UserInfoVC(userinfo_vc_raw); + const auto known_subject_jwk = Signature::parse_jwk(known_subject_jwk_raw); + const auto issuer_jwk = Signature::parse_jwk(issuer_jwk_raw); + + CHECK(vc.valid_from(*issuer_jwk.key)); + + CHECK(vc.issuer() == known_issuer); + CHECK(vc.key_id() == known_key_id); + CHECK(vc.key_id() == opt::get(issuer_jwk.key_id)); + CHECK(vc.not_before().time_since_epoch() == known_not_before); + CHECK(vc.not_after().time_since_epoch() == known_not_after); + CHECK(vc.public_key() == known_subject_jwk); + + const auto& subject = vc.subject(); + CHECK(subject.sub.value_or("") == known_subject.at("sub")); + + CHECK(vc.subject().name.value_or("") == known_subject.at("name")); + CHECK(vc.subject().given_name.value_or("") == known_subject.at("given_name")); + CHECK(vc.subject().family_name.value_or("") == + known_subject.at("family_name")); + CHECK(vc.subject().preferred_username.value_or("") == + known_subject.at("preferred_username")); + CHECK(vc.subject().email.value_or("") == known_subject.at("email")); + CHECK(vc.subject().picture.value_or("") == known_subject.at("picture")); +} + +TEST_CASE("UserInfoClaims Field Parsing") +{ + nlohmann::json credentialSubject = { + { "test", "test" }, + { "sub", "sub" }, + { "name", "name" }, + { "given_name", "given_name" }, + { "family_name", "family_name" }, + { "middle_name", "middle_name" }, + { "nickname", "nickname" }, + { "preferred_username", "preferred_username" }, + { "profile", "profile" }, + { "picture", "picture" }, + { "website", "website" }, + { "email", "email" }, + { "email_verified", true }, + { "gender", "gender" }, + { "birthdate", "birthdate" }, + { "zoneinfo", "zoneinfo" }, + { "locale", "locale" }, + { "phone_number", "phone_number" }, + { "phone_number_verified", true }, + { "address", + { { "formatted", "formatted" }, + { "street_address", "street_address" }, + { "locality", "locality" }, + { "region", "region" }, + { "postal_code", "postal_code" }, + { "country", "country" } } }, + { "updated_at", 42 } + }; + + const auto userinfo_claims = UserInfoClaims::from_json(credentialSubject); + + CHECK(userinfo_claims.sub == credentialSubject.at("sub")); + CHECK(userinfo_claims.name == credentialSubject.at("name")); + CHECK(userinfo_claims.given_name == credentialSubject.at("given_name")); + CHECK(userinfo_claims.family_name == credentialSubject.at("family_name")); + CHECK(userinfo_claims.middle_name == credentialSubject.at("middle_name")); + CHECK(userinfo_claims.nickname == credentialSubject.at("nickname")); + CHECK(userinfo_claims.preferred_username == + credentialSubject.at("preferred_username")); + CHECK(userinfo_claims.profile == credentialSubject.at("profile")); + CHECK(userinfo_claims.picture == credentialSubject.at("picture")); + CHECK(userinfo_claims.website == credentialSubject.at("website")); + CHECK(userinfo_claims.email == credentialSubject.at("email")); + CHECK(userinfo_claims.email_verified == + credentialSubject.at("email_verified")); + CHECK(userinfo_claims.gender == credentialSubject.at("gender")); + CHECK(userinfo_claims.birthdate == credentialSubject.at("birthdate")); + CHECK(userinfo_claims.zoneinfo == credentialSubject.at("zoneinfo")); + CHECK(userinfo_claims.locale == credentialSubject.at("locale")); + CHECK(userinfo_claims.phone_number == credentialSubject.at("phone_number")); + CHECK(userinfo_claims.phone_number_verified == + credentialSubject.at("phone_number_verified")); + CHECK(userinfo_claims.updated_at == credentialSubject.at("updated_at")); + + auto address = userinfo_claims.address.value_or(UserInfoClaimsAddress()); + CHECK(address.formatted == credentialSubject.at("address").at("formatted")); + CHECK(address.street_address == + credentialSubject.at("address").at("street_address")); + CHECK(address.locality == credentialSubject.at("address").at("locality")); + CHECK(address.region == credentialSubject.at("address").at("region")); + CHECK(address.postal_code == + credentialSubject.at("address").at("postal_code")); + CHECK(address.country == credentialSubject.at("address").at("country")); +} + +TEST_CASE("UserInfoClaims Edge Cases") +{ + CHECK_THROWS_WITH( + UserInfoClaims::from_json({ { "updated_at", "42" } }), + "[json.exception.type_error.302] type must be number, but is string"); + + CHECK_THROWS_WITH( + UserInfoClaims::from_json({ { "name", true } }), + "[json.exception.type_error.302] type must be string, but is boolean"); + + CHECK_THROWS_WITH( + UserInfoClaims::from_json({ { "email_verified", "true" } }), + "[json.exception.type_error.302] type must be boolean, but is string"); +} diff --git a/lib/mls_vectors/CMakeLists.txt b/lib/mls_vectors/CMakeLists.txt index 66aef3e8a..83b30daaf 100644 --- a/lib/mls_vectors/CMakeLists.txt +++ b/lib/mls_vectors/CMakeLists.txt @@ -11,7 +11,22 @@ add_library(${CURRENT_LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES}) add_dependencies(${CURRENT_LIB_NAME} mlspp) target_link_libraries(${CURRENT_LIB_NAME} mlspp bytes tls_syntax) target_include_directories(${CURRENT_LIB_NAME} - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + PUBLIC + $ + $ + $ +) + +### +### Install +### + +install(TARGETS ${CURRENT_LIB_NAME} EXPORT mlspp-targets) +install( + DIRECTORY + include/ + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} ) ### diff --git a/lib/mls_vectors/include/mls_vectors/mls_vectors.h b/lib/mls_vectors/include/mls_vectors/mls_vectors.h index 6eef59660..e808083a5 100644 --- a/lib/mls_vectors/include/mls_vectors/mls_vectors.h +++ b/lib/mls_vectors/include/mls_vectors/mls_vectors.h @@ -16,7 +16,7 @@ struct PseudoRandom struct Generator { Generator() = default; - Generator(mls::CipherSuite suite_in, const std::string& label); + Generator(MLS_NAMESPACE::CipherSuite suite_in, const std::string& label); Generator sub(const std::string& label) const; bytes secret(const std::string& label) const; @@ -26,38 +26,40 @@ struct PseudoRandom uint32_t uint32(const std::string& label) const; uint64_t uint64(const std::string& label) const; - mls::SignaturePrivateKey signature_key(const std::string& label) const; - mls::HPKEPrivateKey hpke_key(const std::string& label) const; + MLS_NAMESPACE::SignaturePrivateKey signature_key( + const std::string& label) const; + MLS_NAMESPACE::HPKEPrivateKey hpke_key(const std::string& label) const; size_t output_length() const; private: - mls::CipherSuite suite; + MLS_NAMESPACE::CipherSuite suite; bytes seed; - Generator(mls::CipherSuite suite_in, bytes&& seed_in); + Generator(MLS_NAMESPACE::CipherSuite suite_in, bytes&& seed_in); }; PseudoRandom() = default; - PseudoRandom(mls::CipherSuite suite, const std::string& label); + PseudoRandom(MLS_NAMESPACE::CipherSuite suite, const std::string& label); Generator prg; }; struct TreeMathTestVector { - using OptionalNode = std::optional; + using OptionalNode = std::optional; - mls::LeafCount n_leaves; - mls::NodeCount n_nodes; - mls::NodeIndex root; + MLS_NAMESPACE::LeafCount n_leaves; + MLS_NAMESPACE::NodeCount n_nodes; + MLS_NAMESPACE::NodeIndex root; std::vector left; std::vector right; std::vector parent; std::vector sibling; - std::optional null_if_invalid(mls::NodeIndex input, - mls::NodeIndex answer) const; + std::optional null_if_invalid( + MLS_NAMESPACE::NodeIndex input, + MLS_NAMESPACE::NodeIndex answer) const; TreeMathTestVector() = default; TreeMathTestVector(uint32_t n_leaves); @@ -73,8 +75,8 @@ struct CryptoBasicsTestVector : PseudoRandom bytes out; RefHash() = default; - RefHash(mls::CipherSuite suite, PseudoRandom::Generator&& prg); - std::optional verify(mls::CipherSuite suite) const; + RefHash(MLS_NAMESPACE::CipherSuite suite, PseudoRandom::Generator&& prg); + std::optional verify(MLS_NAMESPACE::CipherSuite suite) const; }; struct ExpandWithLabel @@ -86,8 +88,9 @@ struct CryptoBasicsTestVector : PseudoRandom bytes out; ExpandWithLabel() = default; - ExpandWithLabel(mls::CipherSuite suite, PseudoRandom::Generator&& prg); - std::optional verify(mls::CipherSuite suite) const; + ExpandWithLabel(MLS_NAMESPACE::CipherSuite suite, + PseudoRandom::Generator&& prg); + std::optional verify(MLS_NAMESPACE::CipherSuite suite) const; }; struct DeriveSecret @@ -97,8 +100,9 @@ struct CryptoBasicsTestVector : PseudoRandom bytes out; DeriveSecret() = default; - DeriveSecret(mls::CipherSuite suite, PseudoRandom::Generator&& prg); - std::optional verify(mls::CipherSuite suite) const; + DeriveSecret(MLS_NAMESPACE::CipherSuite suite, + PseudoRandom::Generator&& prg); + std::optional verify(MLS_NAMESPACE::CipherSuite suite) const; }; struct DeriveTreeSecret @@ -110,27 +114,29 @@ struct CryptoBasicsTestVector : PseudoRandom bytes out; DeriveTreeSecret() = default; - DeriveTreeSecret(mls::CipherSuite suite, PseudoRandom::Generator&& prg); - std::optional verify(mls::CipherSuite suite) const; + DeriveTreeSecret(MLS_NAMESPACE::CipherSuite suite, + PseudoRandom::Generator&& prg); + std::optional verify(MLS_NAMESPACE::CipherSuite suite) const; }; struct SignWithLabel { - mls::SignaturePrivateKey priv; - mls::SignaturePublicKey pub; + MLS_NAMESPACE::SignaturePrivateKey priv; + MLS_NAMESPACE::SignaturePublicKey pub; bytes content; std::string label; bytes signature; SignWithLabel() = default; - SignWithLabel(mls::CipherSuite suite, PseudoRandom::Generator&& prg); - std::optional verify(mls::CipherSuite suite) const; + SignWithLabel(MLS_NAMESPACE::CipherSuite suite, + PseudoRandom::Generator&& prg); + std::optional verify(MLS_NAMESPACE::CipherSuite suite) const; }; struct EncryptWithLabel { - mls::HPKEPrivateKey priv; - mls::HPKEPublicKey pub; + MLS_NAMESPACE::HPKEPrivateKey priv; + MLS_NAMESPACE::HPKEPublicKey pub; std::string label; bytes context; bytes plaintext; @@ -138,11 +144,12 @@ struct CryptoBasicsTestVector : PseudoRandom bytes ciphertext; EncryptWithLabel() = default; - EncryptWithLabel(mls::CipherSuite suite, PseudoRandom::Generator&& prg); - std::optional verify(mls::CipherSuite suite) const; + EncryptWithLabel(MLS_NAMESPACE::CipherSuite suite, + PseudoRandom::Generator&& prg); + std::optional verify(MLS_NAMESPACE::CipherSuite suite) const; }; - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; RefHash ref_hash; ExpandWithLabel expand_with_label; @@ -152,7 +159,7 @@ struct CryptoBasicsTestVector : PseudoRandom EncryptWithLabel encrypt_with_label; CryptoBasicsTestVector() = default; - CryptoBasicsTestVector(mls::CipherSuite suite); + CryptoBasicsTestVector(MLS_NAMESPACE::CipherSuite suite); std::optional verify() const; }; @@ -166,8 +173,8 @@ struct SecretTreeTestVector : PseudoRandom bytes nonce; SenderData() = default; - SenderData(mls::CipherSuite suite, PseudoRandom::Generator&& prg); - std::optional verify(mls::CipherSuite suite) const; + SenderData(MLS_NAMESPACE::CipherSuite suite, PseudoRandom::Generator&& prg); + std::optional verify(MLS_NAMESPACE::CipherSuite suite) const; }; struct RatchetStep @@ -179,7 +186,7 @@ struct SecretTreeTestVector : PseudoRandom bytes application_nonce; }; - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; SenderData sender_data; @@ -187,7 +194,7 @@ struct SecretTreeTestVector : PseudoRandom std::vector> leaves; SecretTreeTestVector() = default; - SecretTreeTestVector(mls::CipherSuite suite, + SecretTreeTestVector(MLS_NAMESPACE::CipherSuite suite, uint32_t n_leaves, const std::vector& generations); std::optional verify() const; @@ -227,11 +234,11 @@ struct KeyScheduleTestVector : PseudoRandom bytes membership_key; bytes resumption_psk; - mls::HPKEPublicKey external_pub; + MLS_NAMESPACE::HPKEPublicKey external_pub; Export exporter; }; - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; bytes group_id; bytes initial_init_secret; @@ -239,50 +246,51 @@ struct KeyScheduleTestVector : PseudoRandom std::vector epochs; KeyScheduleTestVector() = default; - KeyScheduleTestVector(mls::CipherSuite suite, uint32_t n_epochs); + KeyScheduleTestVector(MLS_NAMESPACE::CipherSuite suite, uint32_t n_epochs); std::optional verify() const; }; struct MessageProtectionTestVector : PseudoRandom { - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; bytes group_id; - mls::epoch_t epoch; + MLS_NAMESPACE::epoch_t epoch; bytes tree_hash; bytes confirmed_transcript_hash; - mls::SignaturePrivateKey signature_priv; - mls::SignaturePublicKey signature_pub; + MLS_NAMESPACE::SignaturePrivateKey signature_priv; + MLS_NAMESPACE::SignaturePublicKey signature_pub; bytes encryption_secret; bytes sender_data_secret; bytes membership_key; - mls::Proposal proposal; - mls::MLSMessage proposal_pub; - mls::MLSMessage proposal_priv; + MLS_NAMESPACE::Proposal proposal; + MLS_NAMESPACE::MLSMessage proposal_pub; + MLS_NAMESPACE::MLSMessage proposal_priv; - mls::Commit commit; - mls::MLSMessage commit_pub; - mls::MLSMessage commit_priv; + MLS_NAMESPACE::Commit commit; + MLS_NAMESPACE::MLSMessage commit_pub; + MLS_NAMESPACE::MLSMessage commit_priv; bytes application; - mls::MLSMessage application_priv; + MLS_NAMESPACE::MLSMessage application_priv; MessageProtectionTestVector() = default; - MessageProtectionTestVector(mls::CipherSuite suite); + MessageProtectionTestVector(MLS_NAMESPACE::CipherSuite suite); std::optional verify(); private: - mls::GroupKeySource group_keys() const; - mls::GroupContext group_context() const; - - mls::MLSMessage protect_pub( - const mls::GroupContent::RawContent& raw_content) const; - mls::MLSMessage protect_priv( - const mls::GroupContent::RawContent& raw_content); - std::optional unprotect(const mls::MLSMessage& message); + MLS_NAMESPACE::GroupKeySource group_keys() const; + MLS_NAMESPACE::GroupContext group_context() const; + + MLS_NAMESPACE::MLSMessage protect_pub( + const MLS_NAMESPACE::GroupContent::RawContent& raw_content) const; + MLS_NAMESPACE::MLSMessage protect_priv( + const MLS_NAMESPACE::GroupContent::RawContent& raw_content); + std::optional unprotect( + const MLS_NAMESPACE::MLSMessage& message); }; struct PSKSecretTestVector : PseudoRandom @@ -294,44 +302,44 @@ struct PSKSecretTestVector : PseudoRandom bytes psk; }; - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; std::vector psks; bytes psk_secret; PSKSecretTestVector() = default; - PSKSecretTestVector(mls::CipherSuite suite, size_t n_psks); + PSKSecretTestVector(MLS_NAMESPACE::CipherSuite suite, size_t n_psks); std::optional verify() const; }; struct TranscriptTestVector : PseudoRandom { - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; bytes confirmation_key; bytes interim_transcript_hash_before; - mls::AuthenticatedContent authenticated_content; + MLS_NAMESPACE::AuthenticatedContent authenticated_content; bytes confirmed_transcript_hash_after; bytes interim_transcript_hash_after; TranscriptTestVector() = default; - TranscriptTestVector(mls::CipherSuite suite); + TranscriptTestVector(MLS_NAMESPACE::CipherSuite suite); std::optional verify() const; }; struct WelcomeTestVector : PseudoRandom { - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; - mls::HPKEPrivateKey init_priv; - mls::SignaturePublicKey signer_pub; + MLS_NAMESPACE::HPKEPrivateKey init_priv; + MLS_NAMESPACE::SignaturePublicKey signer_pub; - mls::MLSMessage key_package; - mls::MLSMessage welcome; + MLS_NAMESPACE::MLSMessage key_package; + MLS_NAMESPACE::MLSMessage welcome; WelcomeTestVector() = default; - WelcomeTestVector(mls::CipherSuite suite); + WelcomeTestVector(MLS_NAMESPACE::CipherSuite suite); std::optional verify() const; }; @@ -419,15 +427,16 @@ extern std::array treekem_test_tree_structures; struct TreeHashTestVector : PseudoRandom { - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; bytes group_id; - mls::TreeKEMPublicKey tree; + MLS_NAMESPACE::TreeKEMPublicKey tree; std::vector tree_hashes; - std::vector> resolutions; + std::vector> resolutions; TreeHashTestVector() = default; - TreeHashTestVector(mls::CipherSuite suite, TreeStructure tree_structure); + TreeHashTestVector(MLS_NAMESPACE::CipherSuite suite, + TreeStructure tree_structure); std::optional verify(); }; @@ -444,55 +453,61 @@ struct TreeOperationsTestVector : PseudoRandom static const std::vector all_scenarios; - mls::TreeKEMPublicKey tree_before; - mls::Proposal proposal; - mls::LeafIndex proposal_sender; + MLS_NAMESPACE::CipherSuite cipher_suite; - mls::TreeKEMPublicKey tree_after; + MLS_NAMESPACE::TreeKEMPublicKey tree_before; + bytes tree_hash_before; + + MLS_NAMESPACE::Proposal proposal; + MLS_NAMESPACE::LeafIndex proposal_sender; + + MLS_NAMESPACE::TreeKEMPublicKey tree_after; + bytes tree_hash_after; TreeOperationsTestVector() = default; - TreeOperationsTestVector(mls::CipherSuite suite, Scenario scenario); - std::optional verify() const; + TreeOperationsTestVector(MLS_NAMESPACE::CipherSuite suite, Scenario scenario); + std::optional verify(); }; struct TreeKEMTestVector : PseudoRandom { struct PathSecret { - mls::NodeIndex node; + MLS_NAMESPACE::NodeIndex node; bytes path_secret; }; struct LeafPrivateInfo { - mls::LeafIndex index; - mls::HPKEPrivateKey encryption_priv; - mls::SignaturePrivateKey signature_priv; + MLS_NAMESPACE::LeafIndex index; + MLS_NAMESPACE::HPKEPrivateKey encryption_priv; + MLS_NAMESPACE::SignaturePrivateKey signature_priv; std::vector path_secrets; }; struct UpdatePathInfo { - mls::LeafIndex sender; - mls::UpdatePath update_path; + MLS_NAMESPACE::LeafIndex sender; + MLS_NAMESPACE::UpdatePath update_path; std::vector> path_secrets; bytes commit_secret; bytes tree_hash_after; }; - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; bytes group_id; - mls::epoch_t epoch; + MLS_NAMESPACE::epoch_t epoch; bytes confirmed_transcript_hash; - mls::TreeKEMPublicKey ratchet_tree; + MLS_NAMESPACE::TreeKEMPublicKey ratchet_tree; std::vector leaves_private; std::vector update_paths; TreeKEMTestVector() = default; - TreeKEMTestVector(mls::CipherSuite suite, TreeStructure tree_structure); + TreeKEMTestVector(MLS_NAMESPACE::CipherSuite suite, + TreeStructure tree_structure); std::optional verify(); }; @@ -533,22 +548,22 @@ struct PassiveClientTestVector : PseudoRandom struct Epoch { - std::vector proposals; - mls::MLSMessage commit; + std::vector proposals; + MLS_NAMESPACE::MLSMessage commit; bytes epoch_authenticator; }; - mls::CipherSuite cipher_suite; + MLS_NAMESPACE::CipherSuite cipher_suite; - mls::MLSMessage key_package; - mls::SignaturePrivateKey signature_priv; - mls::HPKEPrivateKey encryption_priv; - mls::HPKEPrivateKey init_priv; + MLS_NAMESPACE::MLSMessage key_package; + MLS_NAMESPACE::SignaturePrivateKey signature_priv; + MLS_NAMESPACE::HPKEPrivateKey encryption_priv; + MLS_NAMESPACE::HPKEPrivateKey init_priv; std::vector external_psks; - mls::MLSMessage welcome; - std::optional ratchet_tree; + MLS_NAMESPACE::MLSMessage welcome; + std::optional ratchet_tree; bytes initial_epoch_authenticator; std::vector epochs; diff --git a/lib/mls_vectors/src/mls_vectors.cpp b/lib/mls_vectors/src/mls_vectors.cpp index ec5690647..11bd8c8df 100644 --- a/lib/mls_vectors/src/mls_vectors.cpp +++ b/lib/mls_vectors/src/mls_vectors.cpp @@ -7,7 +7,7 @@ namespace mls_vectors { -using namespace mls; +using namespace MLS_NAMESPACE; /// /// Assertions for verifying test vectors @@ -248,7 +248,7 @@ PseudoRandom::PseudoRandom(CipherSuite suite, const std::string& label) // XXX(RLB): This is a hack to get the tests working in the right format. In // reality, the tree math functions should be updated to be fallible. -std::optional +std::optional TreeMathTestVector::null_if_invalid(NodeIndex input, NodeIndex answer) const { // For some invalid cases (e.g., leaf.left()), we currently return the node @@ -476,7 +476,7 @@ CryptoBasicsTestVector::verify() const /// SecretTreeTestVector /// -SecretTreeTestVector::SenderData::SenderData(mls::CipherSuite suite, +SecretTreeTestVector::SenderData::SenderData(MLS_NAMESPACE::CipherSuite suite, PseudoRandom::Generator&& prg) : sender_data_secret(prg.secret("sender_data_secret")) , ciphertext(prg.secret("ciphertext")) @@ -488,7 +488,7 @@ SecretTreeTestVector::SenderData::SenderData(mls::CipherSuite suite, } std::optional -SecretTreeTestVector::SenderData::verify(mls::CipherSuite suite) const +SecretTreeTestVector::SenderData::verify(MLS_NAMESPACE::CipherSuite suite) const { auto key_and_nonce = KeyScheduleEpoch::sender_data_keys(suite, sender_data_secret, ciphertext); @@ -498,7 +498,7 @@ SecretTreeTestVector::SenderData::verify(mls::CipherSuite suite) const } SecretTreeTestVector::SecretTreeTestVector( - mls::CipherSuite suite, + MLS_NAMESPACE::CipherSuite suite, uint32_t n_leaves, const std::vector& generations) : PseudoRandom(suite, "secret-tree") @@ -820,7 +820,7 @@ MessageProtectionTestVector::group_context() const MLSMessage MessageProtectionTestVector::protect_pub( - const mls::GroupContent::RawContent& raw_content) const + const MLS_NAMESPACE::GroupContent::RawContent& raw_content) const { auto sender = Sender{ MemberSender{ LeafIndex{ 1 } } }; auto authenticated_data = bytes{}; @@ -828,7 +828,7 @@ MessageProtectionTestVector::protect_pub( auto content = GroupContent{ group_id, epoch, sender, authenticated_data, raw_content }; - auto auth_content = AuthenticatedContent::sign(WireFormat::mls_plaintext, + auto auth_content = AuthenticatedContent::sign(WireFormat::mls_public_message, content, cipher_suite, signature_priv, @@ -844,7 +844,7 @@ MessageProtectionTestVector::protect_pub( MLSMessage MessageProtectionTestVector::protect_priv( - const mls::GroupContent::RawContent& raw_content) + const MLS_NAMESPACE::GroupContent::RawContent& raw_content) { auto sender = Sender{ MemberSender{ LeafIndex{ 1 } } }; auto authenticated_data = bytes{}; @@ -853,11 +853,12 @@ MessageProtectionTestVector::protect_priv( auto content = GroupContent{ group_id, epoch, sender, authenticated_data, raw_content }; - auto auth_content = AuthenticatedContent::sign(WireFormat::mls_ciphertext, - content, - cipher_suite, - signature_priv, - group_context()); + auto auth_content = + AuthenticatedContent::sign(WireFormat::mls_private_message, + content, + cipher_suite, + signature_priv, + group_context()); if (content.content_type() == ContentType::commit) { auto confirmation_tag = prg.secret("confirmation_tag"); auth_content.set_confirmation_tag(confirmation_tag); @@ -914,7 +915,8 @@ to_psk_w_secret(const std::vector& psks) return pskws; } -PSKSecretTestVector::PSKSecretTestVector(mls::CipherSuite suite, size_t n_psks) +PSKSecretTestVector::PSKSecretTestVector(MLS_NAMESPACE::CipherSuite suite, + size_t n_psks) : PseudoRandom(suite, "psk_secret") , cipher_suite(suite) , psks(n_psks) @@ -972,7 +974,7 @@ TranscriptTestVector::TranscriptTestVector(CipherSuite suite) auto leaf_index = LeafIndex{ 0 }; authenticated_content = AuthenticatedContent::sign( - WireFormat::mls_plaintext, + WireFormat::mls_public_message, GroupContent{ group_id, epoch, { MemberSender{ leaf_index } }, {}, Commit{} }, suite, @@ -1229,7 +1231,13 @@ struct TreeTestCase auto path = pub.encap(sender_priv, context, joiner); // Process the UpdatePath at all the members - for (auto& [leaf, priv_state] : privs) { + for (auto& pair : privs) { + // XXX(RLB): It might seem like this could be done with a simple + // destructuring assignment, either here or in the `for` clause above. + // However, either of these options cause clang-tidy to segfault when + // evaulating the "bugprone-unchecked-optional-access" lint. + const auto& leaf = pair.first; + auto& priv_state = pair.second; if (leaf == from) { priv_state = PrivateState{ priv_state.sig_priv, sender_priv, { from } }; @@ -1379,7 +1387,7 @@ struct TreeTestCase /// /// TreeHashTestVector /// -TreeHashTestVector::TreeHashTestVector(mls::CipherSuite suite, +TreeHashTestVector::TreeHashTestVector(MLS_NAMESPACE::CipherSuite suite, TreeStructure tree_structure) : PseudoRandom(suite, "tree-hashes") , cipher_suite(suite) @@ -1442,9 +1450,11 @@ const std::vector Scenario::remove_right_edge, Scenario::remove_internal, }; -TreeOperationsTestVector::TreeOperationsTestVector(mls::CipherSuite suite, - Scenario scenario) +TreeOperationsTestVector::TreeOperationsTestVector( + MLS_NAMESPACE::CipherSuite suite, + Scenario scenario) : PseudoRandom(suite, "tree-operations") + , cipher_suite(suite) , proposal_sender(0) { auto init_priv = prg.hpke_key("init_key"); @@ -1474,6 +1484,7 @@ TreeOperationsTestVector::TreeOperationsTestVector(mls::CipherSuite suite, proposal = Proposal{ Add{ key_package } }; tree_before = tc.pub; + tree_hash_before = tree_before.root_hash(); tree_after = tree_before; tree_after.add_leaf(key_package.leaf_node); @@ -1487,6 +1498,8 @@ TreeOperationsTestVector::TreeOperationsTestVector(mls::CipherSuite suite, tree_before = tc.pub; tree_before.blank_path(LeafIndex{ 4 }); + tree_before.set_hash_all(); + tree_hash_before = tree_before.root_hash(); tree_after = tree_before; tree_after.add_leaf(key_package.leaf_node); @@ -1500,6 +1513,7 @@ TreeOperationsTestVector::TreeOperationsTestVector(mls::CipherSuite suite, proposal = Proposal{ Update{ key_package.leaf_node } }; tree_before = tc.pub; + tree_hash_before = tree_before.root_hash(); tree_after = tree_before; tree_after.update_leaf(proposal_sender, key_package.leaf_node); @@ -1513,6 +1527,7 @@ TreeOperationsTestVector::TreeOperationsTestVector(mls::CipherSuite suite, proposal = Proposal{ Remove{ removed } }; tree_before = tc.pub; + tree_hash_before = tree_before.root_hash(); tree_after = tree_before; tree_after.blank_path(removed); @@ -1527,6 +1542,7 @@ TreeOperationsTestVector::TreeOperationsTestVector(mls::CipherSuite suite, proposal = Proposal{ Remove{ removed } }; tree_before = tc.pub; + tree_hash_before = tree_before.root_hash(); tree_after = tree_before; tree_after.blank_path(removed); @@ -1534,12 +1550,20 @@ TreeOperationsTestVector::TreeOperationsTestVector(mls::CipherSuite suite, break; } } + + tree_after.set_hash_all(); + tree_hash_after = tree_after.root_hash(); } std::optional -TreeOperationsTestVector::verify() const +TreeOperationsTestVector::verify() { + tree_before.suite = cipher_suite; + tree_before.set_hash_all(); + auto tree = tree_before; + VERIFY_EQUAL("tree hash before", tree.root_hash(), tree_hash_before); + auto apply = overloaded{ [&](const Add& add) { tree.add_leaf(add.key_package.leaf_node); }, @@ -1560,6 +1584,9 @@ TreeOperationsTestVector::verify() const var::visit(apply, proposal.content); VERIFY_EQUAL("tree after", tree, tree_after); + tree.set_hash_all(); + VERIFY_EQUAL("tree hash after", tree.root_hash(), tree_hash_after); + return std::nullopt; } @@ -1567,7 +1594,7 @@ TreeOperationsTestVector::verify() const /// TreeKEMTestVector /// -TreeKEMTestVector::TreeKEMTestVector(mls::CipherSuite suite, +TreeKEMTestVector::TreeKEMTestVector(MLS_NAMESPACE::CipherSuite suite, TreeStructure tree_structure) : PseudoRandom(suite, "treekem") , cipher_suite(suite) @@ -1786,11 +1813,15 @@ MessagesTestVector::MessagesTestVector() auto version = ProtocolVersion::mls10; auto hpke_priv = prg.hpke_key("hpke_priv"); + auto hpke_priv_2 = prg.hpke_key("hpke_priv_2"); auto hpke_pub = hpke_priv.public_key; + auto hpke_pub_2 = hpke_priv_2.public_key; auto hpke_ct = HPKECiphertext{ prg.secret("kem_output"), prg.secret("ciphertext") }; auto sig_priv = prg.signature_key("signature_priv"); + auto sig_priv_2 = prg.signature_key("signature_priv_2"); auto sig_pub = sig_priv.public_key; + auto sig_pub_2 = sig_priv_2.public_key; // KeyPackage and extensions auto cred = Credential::basic(user_id); @@ -1802,6 +1833,14 @@ MessagesTestVector::MessagesTestVector() Lifetime::create_default(), ext_list, sig_priv }; + auto leaf_node_2 = LeafNode{ suite, + hpke_pub_2, + sig_pub_2, + cred, + Capabilities::create_default(), + Lifetime::create_default(), + ext_list, + sig_priv_2 }; auto key_package_obj = KeyPackage{ suite, hpke_pub, leaf_node, {}, sig_priv }; auto leaf_node_update = @@ -1813,7 +1852,7 @@ MessagesTestVector::MessagesTestVector() auto tree = TreeKEMPublicKey{ suite }; tree.add_leaf(leaf_node); - tree.add_leaf(leaf_node); + tree.add_leaf(leaf_node_2); auto ratchet_tree_obj = RatchetTreeExtension{ tree }; // Welcome and its substituents @@ -1860,7 +1899,7 @@ MessagesTestVector::MessagesTestVector() auto membership_key = prg.secret("membership_key"); auto content_auth_proposal = AuthenticatedContent::sign( - WireFormat::mls_plaintext, + WireFormat::mls_public_message, { group_id, epoch, sender, {}, Proposal{ remove } }, suite, sig_priv, @@ -1869,7 +1908,7 @@ MessagesTestVector::MessagesTestVector() content_auth_proposal, suite, membership_key, group_context); auto content_auth_commit = - AuthenticatedContent::sign(WireFormat::mls_plaintext, + AuthenticatedContent::sign(WireFormat::mls_public_message, { group_id, epoch, sender, {}, commit_obj }, suite, sig_priv, @@ -1880,7 +1919,7 @@ MessagesTestVector::MessagesTestVector() // PrivateMessage auto content_auth_application_obj = AuthenticatedContent::sign( - WireFormat::mls_ciphertext, + WireFormat::mls_private_message, { group_id, epoch, sender, {}, ApplicationData{} }, suite, sig_priv, @@ -1956,15 +1995,15 @@ MessagesTestVector::verify() const VERIFY_TLS_RTT_VAL("Public(Proposal)", MLSMessage, public_message_proposal, - require_format(WireFormat::mls_plaintext)); + require_format(WireFormat::mls_public_message)); VERIFY_TLS_RTT_VAL("Public(Commit)", MLSMessage, public_message_commit, - require_format(WireFormat::mls_plaintext)); + require_format(WireFormat::mls_public_message)); VERIFY_TLS_RTT_VAL("PrivateMessage", MLSMessage, private_message, - require_format(WireFormat::mls_ciphertext)); + require_format(WireFormat::mls_private_message)); return std::nullopt; } diff --git a/lib/mls_vectors/test/mls_vectors.cpp b/lib/mls_vectors/test/mls_vectors.cpp index 4c55e16c5..827635c9e 100644 --- a/lib/mls_vectors/test/mls_vectors.cpp +++ b/lib/mls_vectors/test/mls_vectors.cpp @@ -4,14 +4,14 @@ using namespace mls_vectors; -static const std::vector supported_suites{ - { mls::CipherSuite::ID::X25519_AES128GCM_SHA256_Ed25519 }, - { mls::CipherSuite::ID::P256_AES128GCM_SHA256_P256 }, - { mls::CipherSuite::ID::X25519_CHACHA20POLY1305_SHA256_Ed25519 }, - { mls::CipherSuite::ID::P521_AES256GCM_SHA512_P521 }, +static const std::vector supported_suites{ + { MLS_NAMESPACE::CipherSuite::ID::X25519_AES128GCM_SHA256_Ed25519 }, + { MLS_NAMESPACE::CipherSuite::ID::P256_AES128GCM_SHA256_P256 }, + { MLS_NAMESPACE::CipherSuite::ID::X25519_CHACHA20POLY1305_SHA256_Ed25519 }, + { MLS_NAMESPACE::CipherSuite::ID::P521_AES256GCM_SHA512_P521 }, #if !defined(WITH_BORINGSSL) - { mls::CipherSuite::ID::X448_AES256GCM_SHA512_Ed448 }, - { mls::CipherSuite::ID::X448_CHACHA20POLY1305_SHA512_Ed448 }, + { MLS_NAMESPACE::CipherSuite::ID::X448_AES256GCM_SHA512_Ed448 }, + { MLS_NAMESPACE::CipherSuite::ID::X448_CHACHA20POLY1305_SHA512_Ed448 }, #endif }; @@ -89,7 +89,7 @@ TEST_CASE("Tree Hashes") TEST_CASE("Tree Operations") { - auto suite = mls::CipherSuite::ID::X25519_AES128GCM_SHA256_Ed25519; + auto suite = MLS_NAMESPACE::CipherSuite::ID::X25519_AES128GCM_SHA256_Ed25519; for (auto scenario : TreeOperationsTestVector::all_scenarios) { auto tv = TreeOperationsTestVector{ suite, scenario }; REQUIRE(tv.verify() == std::nullopt); diff --git a/lib/tls_syntax/CMakeLists.txt b/lib/tls_syntax/CMakeLists.txt index 27f9148d1..4d98912ed 100644 --- a/lib/tls_syntax/CMakeLists.txt +++ b/lib/tls_syntax/CMakeLists.txt @@ -9,9 +9,24 @@ file(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src add_library(${CURRENT_LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES}) add_dependencies(${CURRENT_LIB_NAME} third_party) -target_link_libraries(${CURRENT_LIB_NAME} third_party) +target_link_libraries(${CURRENT_LIB_NAME} PUBLIC third_party) target_include_directories(${CURRENT_LIB_NAME} - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + PUBLIC + $ + $ + $ +) + +### +### Install +### + +install(TARGETS ${CURRENT_LIB_NAME} EXPORT mlspp-targets) +install( + DIRECTORY + include/ + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} ) ### diff --git a/lib/tls_syntax/include/tls/compat.h b/lib/tls_syntax/include/tls/compat.h index be8c20d98..6c78683a6 100644 --- a/lib/tls_syntax/include/tls/compat.h +++ b/lib/tls_syntax/include/tls/compat.h @@ -8,8 +8,9 @@ #else #include #endif // VARIANT_COMPAT +#include -namespace tls { +namespace MLS_NAMESPACE::tls { // To balance backward-compatibility with macOS 10.11 with forward-compatibility // with future versions of C++, we use `mpark::variant` or `std::variant` as @@ -65,4 +66,4 @@ get(const std::optional&& opt) } } // namespace opt -} // namespace tls +} // namespace MLS_NAMESPACE::tls diff --git a/lib/tls_syntax/include/tls/tls_syntax.h b/lib/tls_syntax/include/tls/tls_syntax.h index 19806f0c4..28427aa21 100644 --- a/lib/tls_syntax/include/tls/tls_syntax.h +++ b/lib/tls_syntax/include/tls/tls_syntax.h @@ -4,13 +4,14 @@ #include #include #include +#include #include #include #include #include -namespace tls { +namespace MLS_NAMESPACE::tls { // For indicating no min or max in vector definitions const size_t none = std::numeric_limits::max(); @@ -565,4 +566,4 @@ inline return str; } -} // namespace tls +} // namespace MLS_NAMESPACE::tls diff --git a/lib/tls_syntax/src/tls_syntax.cpp b/lib/tls_syntax/src/tls_syntax.cpp index 717b6b1f7..dccfe16fa 100644 --- a/lib/tls_syntax/src/tls_syntax.cpp +++ b/lib/tls_syntax/src/tls_syntax.cpp @@ -1,7 +1,8 @@ +#include #include // NOLINTNEXTLINE(llvmlibc-implementation-in-namespace) -namespace tls { +namespace MLS_NAMESPACE::tls { void ostream::write_raw(const std::vector& bytes) @@ -175,4 +176,4 @@ varint::decode(istream& str, uint64_t& val) return str; } -} // namespace tls +} // namespace MLS_NAMESPACE::tls diff --git a/lib/tls_syntax/test/tls_syntax.cpp b/lib/tls_syntax/test/tls_syntax.cpp index 8fdbd2706..3a6e68d65 100644 --- a/lib/tls_syntax/test/tls_syntax.cpp +++ b/lib/tls_syntax/test/tls_syntax.cpp @@ -1,8 +1,9 @@ #include #include +#include #include -using namespace bytes_ns; +using namespace MLS_NAMESPACE::bytes_ns; // An enum to test enum encoding, and as a type for variants enum struct IntType : uint16_t @@ -11,12 +12,12 @@ enum struct IntType : uint16_t uint16 = 0xBBBB, }; -namespace tls { +namespace MLS_NAMESPACE::tls { TLS_VARIANT_MAP(IntType, uint8_t, uint8) TLS_VARIANT_MAP(IntType, uint16_t, uint16) -} // namespace tls +} // namespace MLS_NAMESPACE::tls // A struct to test struct encoding and traits struct ExampleStruct @@ -25,20 +26,20 @@ struct ExampleStruct std::array b{ 0, 0, 0, 0 }; std::optional c; std::vector d; - tls::var::variant e; + MLS_NAMESPACE::tls::var::variant e; uint64_t f{ 0 }; uint64_t g{ 0 }; uint64_t h{ 0 }; TLS_SERIALIZABLE(a, b, c, d, e, f, g, h) - TLS_TRAITS(tls::pass, - tls::pass, - tls::pass, - tls::pass, - tls::variant, - tls::varint, - tls::varint, - tls::varint) + TLS_TRAITS(MLS_NAMESPACE::tls::pass, + MLS_NAMESPACE::tls::pass, + MLS_NAMESPACE::tls::pass, + MLS_NAMESPACE::tls::pass, + MLS_NAMESPACE::tls::variant, + MLS_NAMESPACE::tls::varint, + MLS_NAMESPACE::tls::varint, + MLS_NAMESPACE::tls::varint) }; static bool @@ -99,7 +100,7 @@ template void ostream_test(T val, const std::vector& enc) { - tls::ostream w; // NOLINT(misc-const-correctness) + MLS_NAMESPACE::tls::ostream w; // NOLINT(misc-const-correctness) w << val; REQUIRE(w.bytes() == enc); REQUIRE(w.size() == enc.size()); @@ -108,7 +109,7 @@ ostream_test(T val, const std::vector& enc) TEST_CASE_FIXTURE(TLSSyntaxTest, "TLS ostream") { bytes answer{ 1, 2, 3, 4 }; - tls::ostream w; + MLS_NAMESPACE::tls::ostream w; w.write_raw(answer); REQUIRE(w.bytes() == answer); @@ -129,7 +130,7 @@ template void istream_test(T val, T& data, const std::vector& enc) { - tls::istream r(enc); // NOLINT(misc-const-correctness) + MLS_NAMESPACE::tls::istream r(enc); // NOLINT(misc-const-correctness) r >> data; REQUIRE(data == val); REQUIRE(r.empty()); @@ -175,26 +176,26 @@ TEST_CASE_FIXTURE(TLSSyntaxTest, "TLS abbreviations") { ExampleStruct val_in = val_struct; - tls::ostream w; + MLS_NAMESPACE::tls::ostream w; w << val_struct; auto streamed = w.bytes(); - auto marshaled = tls::marshal(val_struct); + auto marshaled = MLS_NAMESPACE::tls::marshal(val_struct); REQUIRE(streamed == marshaled); ExampleStruct val_out1; - tls::unmarshal(marshaled, val_out1); + MLS_NAMESPACE::tls::unmarshal(marshaled, val_out1); REQUIRE(val_in == val_out1); - auto val_out2 = tls::get(marshaled); + auto val_out2 = MLS_NAMESPACE::tls::get(marshaled); REQUIRE(val_in == val_out2); } TEST_CASE("TLS varint failure cases") { // Encoding a value that is to large - tls::ostream w; + MLS_NAMESPACE::tls::ostream w; // NOLINTNEXTLINE(llvm-else-after-return, readability-else-after-return) - REQUIRE_THROWS(tls::varint::encode(w, uint64_t(0xffffffff))); + REQUIRE_THROWS(MLS_NAMESPACE::tls::varint::encode(w, uint64_t(0xffffffff))); // Too large and non-minimal values auto decode_failure_cases = std::vector{ @@ -204,9 +205,9 @@ TEST_CASE("TLS varint failure cases") }; for (const auto& enc : decode_failure_cases) { auto val = uint64_t(0); - auto r = tls::istream(enc); + auto r = MLS_NAMESPACE::tls::istream(enc); // NOLINTNEXTLINE(llvm-else-after-return, readability-else-after-return) - REQUIRE_THROWS(tls::varint::decode(r, val)); + REQUIRE_THROWS(MLS_NAMESPACE::tls::varint::decode(r, val)); } } diff --git a/src/common.cpp b/src/common.cpp index 1091d4c60..c0185aa54 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -1,6 +1,7 @@ #include "mls/common.h" +#include -namespace mls { +namespace MLS_NAMESPACE { uint64_t seconds_since_epoch() @@ -10,4 +11,4 @@ seconds_since_epoch() return std::time(nullptr); } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/core_types.cpp b/src/core_types.cpp index 6bfdc08fa..dc763b233 100644 --- a/src/core_types.cpp +++ b/src/core_types.cpp @@ -1,11 +1,12 @@ #include "mls/core_types.h" #include "mls/messages.h" +#include #include "grease.h" #include -namespace mls { +namespace MLS_NAMESPACE { /// /// Extensions @@ -45,9 +46,11 @@ const std::array all_supported_ciphersuites = { CipherSuite::ID::X448_CHACHA20POLY1305_SHA512_Ed448, }; -const std::array all_supported_credentials = { +const std::array all_supported_credentials = { CredentialType::basic, CredentialType::x509, + CredentialType::userinfo_vc_draft_00, + CredentialType::multi_draft_00 }; Capabilities @@ -435,4 +438,4 @@ KeyPackage::to_be_signed() const return out.bytes(); } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/credential.cpp b/src/credential.cpp index 301af4fb9..cc5178c54 100644 --- a/src/credential.cpp +++ b/src/credential.cpp @@ -1,15 +1,18 @@ -#include "mls/credential.h" -#include "hpke/certificate.h" +#include +#include +#include +#include #include -namespace mls { +namespace MLS_NAMESPACE { /// /// X509Credential /// -using hpke::Certificate; // NOLINT(misc-unused-using-decls) -using hpke::Signature; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::Certificate; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::Signature; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::UserInfoVC; // NOLINT(misc-unused-using-decls) static const Signature& find_signature(Signature::ID id) @@ -111,6 +114,135 @@ operator==(const X509Credential& lhs, const X509Credential& rhs) return lhs.der_chain == rhs.der_chain; } +/// +/// UserInfoVCCredential +/// +UserInfoVCCredential::UserInfoVCCredential(std::string userinfo_vc_jwt_in) + : userinfo_vc_jwt(std::move(userinfo_vc_jwt_in)) + , _vc(std::make_shared(userinfo_vc_jwt)) +{ +} + +bool +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +UserInfoVCCredential::valid_for(const SignaturePublicKey& pub) const +{ + const auto& vc_pub = _vc->public_key(); + return pub.data == vc_pub.sig.serialize(*vc_pub.key); +} + +bool +UserInfoVCCredential::valid_from(const PublicJWK& pub) const +{ + const auto& sig = _vc->signature_algorithm(); + if (pub.signature_scheme != tls_signature_scheme(sig.id)) { + return false; + } + + const auto sig_pub = sig.deserialize(pub.public_key.data); + return _vc->valid_from(*sig_pub); +} + +tls::ostream +operator<<(tls::ostream& str, const UserInfoVCCredential& obj) +{ + return str << from_ascii(obj.userinfo_vc_jwt); +} + +tls::istream +operator>>(tls::istream& str, UserInfoVCCredential& obj) +{ + auto jwt = bytes{}; + str >> jwt; + obj = UserInfoVCCredential(to_ascii(jwt)); + return str; +} + +bool +operator==(const UserInfoVCCredential& lhs, const UserInfoVCCredential& rhs) +{ + return lhs.userinfo_vc_jwt == rhs.userinfo_vc_jwt; +} + +bool +operator!=(const UserInfoVCCredential& lhs, const UserInfoVCCredential& rhs) +{ + return !(lhs == rhs); +} + +/// +/// CredentialBinding and MultiCredential +/// + +struct CredentialBindingTBS +{ + const CipherSuite& cipher_suite; + const Credential& credential; + const SignaturePublicKey& credential_key; + const SignaturePublicKey& signature_key; + + TLS_SERIALIZABLE(cipher_suite, credential, credential_key, signature_key) +}; + +CredentialBinding::CredentialBinding(CipherSuite cipher_suite_in, + Credential credential_in, + const SignaturePrivateKey& credential_priv, + const SignaturePublicKey& signature_key) + : cipher_suite(cipher_suite_in) + , credential(std::move(credential_in)) + , credential_key(credential_priv.public_key) +{ + if (credential.type() == CredentialType::multi_draft_00) { + throw InvalidParameterError("Multi-credentials cannot be nested"); + } + + if (!credential.valid_for(credential_key)) { + throw InvalidParameterError("Credential key does not match credential"); + } + + signature = credential_priv.sign( + cipher_suite, sign_label::multi_credential, to_be_signed(signature_key)); +} + +bytes +CredentialBinding::to_be_signed(const SignaturePublicKey& signature_key) const +{ + return tls::marshal(CredentialBindingTBS{ + cipher_suite, credential, credential_key, signature_key }); +} + +bool +CredentialBinding::valid_for(const SignaturePublicKey& signature_key) const +{ + auto valid_self = credential.valid_for(credential_key); + auto valid_other = credential_key.verify(cipher_suite, + sign_label::multi_credential, + to_be_signed(signature_key), + signature); + + return valid_self && valid_other; +} + +MultiCredential::MultiCredential( + const std::vector& binding_inputs, + const SignaturePublicKey& signature_key) +{ + bindings = + stdx::transform(binding_inputs, [&](auto&& input) { + return CredentialBinding(input.cipher_suite, + input.credential, + input.credential_priv, + signature_key); + }); +} + +bool +MultiCredential::valid_for(const SignaturePublicKey& pub) const +{ + return stdx::all_of( + bindings, [&](const auto& binding) { return binding.valid_for(pub); }); +} + /// /// Credential /// @@ -124,17 +256,26 @@ Credential::type() const Credential Credential::basic(const bytes& identity) { - Credential cred; - cred._cred = BasicCredential{ identity }; - return cred; + return { BasicCredential{ identity } }; } Credential Credential::x509(const std::vector& der_chain) { - Credential cred; - cred._cred = X509Credential{ der_chain }; - return cred; + return { X509Credential{ der_chain } }; +} + +Credential +Credential::multi(const std::vector& binding_inputs, + const SignaturePublicKey& signature_key) +{ + return { MultiCredential{ binding_inputs, signature_key } }; +} + +Credential +Credential::userinfo_vc(const std::string& userinfo_vc_jwt) +{ + return { UserInfoVCCredential{ userinfo_vc_jwt } }; } bool @@ -142,11 +283,17 @@ Credential::valid_for(const SignaturePublicKey& pub) const { const auto pub_key_match = overloaded{ [&](const X509Credential& x509) { return x509.valid_for(pub); }, - [](const BasicCredential& /* basic */) { return true; }, + [&](const UserInfoVCCredential& vc) { return vc.valid_for(pub); }, + [&](const MultiCredential& multi) { return multi.valid_for(pub); }, }; return var::visit(pub_key_match, _cred); } -} // namespace mls +Credential::Credential(SpecificCredential specific) + : _cred(std::move(specific)) +{ +} + +} // namespace MLS_NAMESPACE diff --git a/src/crypto.cpp b/src/crypto.cpp index dedf0442d..8d859c0ee 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -1,17 +1,18 @@ #include #include #include +#include #include -using hpke::AEAD; // NOLINT(misc-unused-using-decls) -using hpke::Digest; // NOLINT(misc-unused-using-decls) -using hpke::HPKE; // NOLINT(misc-unused-using-decls) -using hpke::KDF; // NOLINT(misc-unused-using-decls) -using hpke::KEM; // NOLINT(misc-unused-using-decls) -using hpke::Signature; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::AEAD; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::Digest; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::HPKE; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::KDF; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::KEM; // NOLINT(misc-unused-using-decls) +using MLS_NAMESPACE::hpke::Signature; // NOLINT(misc-unused-using-decls) -namespace mls { +namespace MLS_NAMESPACE { SignatureScheme tls_signature_scheme(Signature::ID id) @@ -378,6 +379,7 @@ const std::string mls_content = "FramedContentTBS"; const std::string leaf_node = "LeafNodeTBS"; const std::string key_package = "KeyPackageTBS"; const std::string group_info = "GroupInfoTBS"; +const std::string multi_credential = "MultiCredential"; } // namespace sign_label struct SignContent @@ -399,6 +401,30 @@ SignaturePublicKey::verify(const CipherSuite& suite, return suite.sig().verify(content, signature, *pub); } +SignaturePublicKey +SignaturePublicKey::from_jwk(CipherSuite suite, const std::string& json_str) +{ + auto pub = suite.sig().import_jwk(json_str); + auto pub_data = suite.sig().serialize(*pub); + return SignaturePublicKey{ pub_data }; +} + +std::string +SignaturePublicKey::to_jwk(CipherSuite suite) const +{ + auto pub = suite.sig().deserialize(data); + return suite.sig().export_jwk(*pub); +} + +PublicJWK +PublicJWK::parse(const std::string& jwk_json) +{ + const auto parsed = Signature::parse_jwk(jwk_json); + const auto scheme = tls_signature_scheme(parsed.sig.id); + const auto pub_data = parsed.sig.serialize(*parsed.key); + return { scheme, parsed.key_id, { pub_data } }; +} + SignaturePrivateKey SignaturePrivateKey::generate(CipherSuite suite) { @@ -453,4 +479,21 @@ SignaturePrivateKey::set_public_key(CipherSuite suite) public_key.data = suite.sig().serialize(*pub); } -} // namespace mls +SignaturePrivateKey +SignaturePrivateKey::from_jwk(CipherSuite suite, const std::string& json_str) +{ + auto priv = suite.sig().import_jwk_private(json_str); + auto priv_data = suite.sig().serialize_private(*priv); + auto pub = priv->public_key(); + auto pub_data = suite.sig().serialize(*pub); + return { priv_data, pub_data }; +} + +std::string +SignaturePrivateKey::to_jwk(CipherSuite suite) const +{ + const auto priv = suite.sig().deserialize_private(data); + return suite.sig().export_jwk_private(*priv); +} + +} // namespace MLS_NAMESPACE diff --git a/src/grease.cpp b/src/grease.cpp index e0de4c62e..fc210648c 100644 --- a/src/grease.cpp +++ b/src/grease.cpp @@ -1,9 +1,10 @@ #include "grease.h" +#include #include #include -namespace mls { +namespace MLS_NAMESPACE { // Randomness parmeters: // * Given a list of N items, insert max(1, rand(p_grease * N)) GREASE values @@ -117,4 +118,4 @@ grease(ExtensionList&& extensions) return { ext }; } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/grease.h b/src/grease.h index 1a59f108c..c57f10a64 100644 --- a/src/grease.h +++ b/src/grease.h @@ -1,8 +1,9 @@ #pragma once #include "mls/core_types.h" +#include -namespace mls { +namespace MLS_NAMESPACE { Capabilities grease(Capabilities&& capabilities, const ExtensionList& extensions); @@ -10,4 +11,4 @@ grease(Capabilities&& capabilities, const ExtensionList& extensions); ExtensionList grease(ExtensionList&& extensions); -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/key_schedule.cpp b/src/key_schedule.cpp index 3882955a0..a5587ca2d 100644 --- a/src/key_schedule.cpp +++ b/src/key_schedule.cpp @@ -1,6 +1,7 @@ #include +#include -namespace mls { +namespace MLS_NAMESPACE { /// /// Key Derivation Functions @@ -576,4 +577,4 @@ operator==(const TranscriptHash& lhs, const TranscriptHash& rhs) return confirmed && interim; } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/messages.cpp b/src/messages.cpp index 17590b3d1..8daa652da 100644 --- a/src/messages.cpp +++ b/src/messages.cpp @@ -2,10 +2,11 @@ #include #include #include +#include #include "grease.h" -namespace mls { +namespace MLS_NAMESPACE { // Extensions @@ -350,7 +351,7 @@ AuthenticatedContent::sign(WireFormat wire_format, const SignaturePrivateKey& sig_priv, const std::optional& context) { - if (wire_format == WireFormat::mls_plaintext && + if (wire_format == WireFormat::mls_public_message && content.content_type() == ContentType::application) { throw InvalidParameterError( "Application data cannot be sent as PublicMessage"); @@ -368,7 +369,7 @@ AuthenticatedContent::verify(CipherSuite suite, const SignaturePublicKey& sig_pub, const std::optional& context) const { - if (wire_format == WireFormat::mls_plaintext && + if (wire_format == WireFormat::mls_public_message && content.content_type() == ContentType::application) { return false; } @@ -544,7 +545,7 @@ PublicMessage::unprotect(CipherSuite suite, } return AuthenticatedContent{ - WireFormat::mls_plaintext, + WireFormat::mls_public_message, content, auth, }; @@ -560,7 +561,7 @@ AuthenticatedContent PublicMessage::authenticated_content() const { auto auth_content = AuthenticatedContent{}; - auth_content.wire_format = WireFormat::mls_plaintext; + auth_content.wire_format = WireFormat::mls_public_message; auth_content.content = content; auth_content.auth = auth; return auth_content; @@ -570,7 +571,7 @@ PublicMessage::PublicMessage(AuthenticatedContent content_auth) : content(std::move(content_auth.content)) , auth(std::move(content_auth.auth)) { - if (content_auth.wire_format != WireFormat::mls_plaintext) { + if (content_auth.wire_format != WireFormat::mls_public_message) { throw InvalidParameterError("Wire format mismatch (not mls_plaintext)"); } } @@ -589,7 +590,7 @@ PublicMessage::membership_mac(CipherSuite suite, const std::optional& context) const { auto tbm = tls::marshal(GroupContentTBM{ - { WireFormat::mls_plaintext, content, context }, + { WireFormat::mls_public_message, content, context }, auth, }); @@ -629,6 +630,19 @@ operator>>(tls::istream& str, PublicMessage& obj) return str; } +bool +operator==(const PublicMessage& lhs, const PublicMessage& rhs) +{ + return lhs.content == rhs.content && lhs.auth == rhs.auth && + lhs.membership_tag == rhs.membership_tag; +} + +bool +operator!=(const PublicMessage& lhs, const PublicMessage& rhs) +{ + return !(lhs == rhs); +} + static bytes marshal_ciphertext_content(const GroupContent& content, const GroupContentAuthData& auth, @@ -799,7 +813,7 @@ PrivateMessage::unprotect(CipherSuite suite, unmarshal_ciphertext_content(opt::get(content_pt), content, auth); return AuthenticatedContent{ - WireFormat::mls_ciphertext, + WireFormat::mls_private_message, std::move(content), std::move(auth), }; @@ -837,13 +851,13 @@ MLSMessage::wire_format() const return tls::variant::type(message); } -MLSMessage::MLSMessage(PublicMessage mls_plaintext) - : message(std::move(mls_plaintext)) +MLSMessage::MLSMessage(PublicMessage public_message) + : message(std::move(public_message)) { } -MLSMessage::MLSMessage(PrivateMessage mls_ciphertext) - : message(std::move(mls_ciphertext)) +MLSMessage::MLSMessage(PrivateMessage private_message) + : message(std::move(private_message)) { } @@ -892,9 +906,9 @@ external_proposal(CipherSuite suite, { /* no authenticated data */ }, { proposal } }; auto content_auth = AuthenticatedContent::sign( - WireFormat::mls_plaintext, std::move(content), suite, sig_priv, {}); + WireFormat::mls_public_message, std::move(content), suite, sig_priv, {}); return PublicMessage::protect(std::move(content_auth), suite, {}, {}); } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/session.cpp b/src/session.cpp index b9ea49f35..e1b615cb2 100644 --- a/src/session.cpp +++ b/src/session.cpp @@ -1,10 +1,10 @@ -#include - #include +#include +#include #include -namespace mls { +namespace MLS_NAMESPACE { /// /// Inner struct declarations for PendingJoin and Session @@ -200,14 +200,14 @@ Session::Inner::import_handshake(const bytes& encoded) const auto msg = tls::get(encoded); switch (msg.wire_format()) { - case WireFormat::mls_plaintext: + case WireFormat::mls_public_message: if (encrypt_handshake) { throw ProtocolError("Handshake not encrypted as required"); } return msg; - case WireFormat::mls_ciphertext: { + case WireFormat::mls_private_message: { if (!encrypt_handshake) { throw ProtocolError("Unexpected handshake encryption"); } @@ -435,4 +435,4 @@ operator!=(const Session& lhs, const Session& rhs) return !(lhs == rhs); } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/state.cpp b/src/state.cpp index 248ed7fc0..2dd02bfe8 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -1,7 +1,8 @@ #include +#include #include -namespace mls { +namespace MLS_NAMESPACE { /// /// Constructors @@ -67,11 +68,77 @@ State::import_tree(const bytes& tree_hash, throw InvalidParameterError("Tree does not match GroupInfo"); } - if (!tree.parent_hash_valid()) { - throw InvalidParameterError("Invalid tree"); + return tree; +} + +bool +State::validate_tree() const +{ + // The functionality here is somewhat duplicative of State::valid(const + // LeafNode&). Simply calling that method, however, would result in this + // method having quadratic scaling, since each call to valid() does a linear + // scan through the tree to check uniqueness of keys and compatibility of + // credential support. + + // Validate that the tree is parent-hash valid + if (!_tree.parent_hash_valid()) { + return false; } - return tree; + // Validate the signatures on all leaves + const auto signature_valid = + _tree.all_leaves([&](auto i, const auto& leaf_node) { + auto binding = std::optional{}; + switch (leaf_node.source()) { + case LeafNodeSource::commit: + case LeafNodeSource::update: + binding = LeafNode::MemberBinding{ _group_id, i }; + break; + + default: + // Nothing to do + break; + } + + return leaf_node.verify(_suite, binding); + }); + if (!signature_valid) { + return false; + } + + // Collect cross-tree properties + auto n_leaves = size_t(0); + auto encryption_keys = std::set{}; + auto signature_keys = std::set{}; + auto credential_types = std::set{}; + _tree.all_leaves([&](auto /* i */, const auto& leaf_node) { + n_leaves += 1; + encryption_keys.insert(leaf_node.encryption_key.data); + signature_keys.insert(leaf_node.signature_key.data); + credential_types.insert(leaf_node.credential.type()); + return true; + }); + + // Verify uniqueness of keys + if (encryption_keys.size() != n_leaves) { + return false; + } + + if (signature_keys.size() != n_leaves) { + return false; + } + + // Verify that each leaf indicates support for all required parameters + return _tree.all_leaves([&](auto /* i */, const auto& leaf_node) { + const auto supports_group_extensions = + leaf_node.verify_extension_support(_extensions); + const auto supports_own_extensions = + leaf_node.verify_extension_support(leaf_node.extensions); + const auto supports_group_credentials = + leaf_node.capabilities.credentials_supported(credential_types); + return supports_group_extensions && supports_own_extensions && + supports_group_credentials; + }); } State::State(SignaturePrivateKey sig_priv, @@ -91,6 +158,10 @@ State::State(SignaturePrivateKey sig_priv, , _index(0) , _identity_priv(std::move(sig_priv)) { + if (!validate_tree()) { + throw InvalidParameterError("Invalid tree"); + } + // The following are not set: // _index // _tree_priv @@ -173,6 +244,11 @@ State::State(const HPKEPrivateKey& init_priv, _extensions = group_info.group_context.extensions; + // Validate that the tree is in fact consistent with the group's parameters + if (!validate_tree()) { + throw InvalidParameterError("Invalid tree"); + } + // Construct TreeKEM private key from parts provided auto maybe_index = _tree.find(key_package.leaf_node); if (!maybe_index) { @@ -268,7 +344,7 @@ State::new_member_add(const bytes& group_id, { /* no authenticated data */ }, { std::move(proposal) } }; auto content_auth = AuthenticatedContent::sign( - WireFormat::mls_plaintext, std::move(content), suite, sig_priv, {}); + WireFormat::mls_public_message, std::move(content), suite, sig_priv, {}); return PublicMessage::protect(std::move(content_auth), suite, {}, {}); } @@ -298,8 +374,8 @@ State::sign(const Sender& sender, _group_id, _epoch, sender, authenticated_data, { inner_content } }; - auto wire_format = - (encrypt) ? WireFormat::mls_ciphertext : WireFormat::mls_plaintext; + auto wire_format = (encrypt) ? WireFormat::mls_private_message + : WireFormat::mls_public_message; auto content_auth = AuthenticatedContent::sign( wire_format, std::move(content), _suite, _identity_priv, group_context()); @@ -311,13 +387,13 @@ MLSMessage State::protect(AuthenticatedContent&& content_auth, size_t padding_size) { switch (content_auth.wire_format) { - case WireFormat::mls_plaintext: + case WireFormat::mls_public_message: return PublicMessage::protect(std::move(content_auth), _suite, _key_schedule.membership_key, group_context()); - case WireFormat::mls_ciphertext: + case WireFormat::mls_private_message: return PrivateMessage::protect(std::move(content_auth), _suite, _keys, @@ -812,9 +888,8 @@ State::handle(const AuthenticatedContent& content_auth, if (!external_commit) { sender_location = opt::get(sender); } else { - // Add the joiner - const auto& path = opt::get(commit.path); - sender_location = next._tree.add_leaf(path.leaf_node); + // Find where the joiner will be added + sender_location = next._tree.allocate_leaf(); // Extract the forced init secret auto kem_output = commit.valid_external(); @@ -1130,19 +1205,9 @@ State::apply(const GroupContextExtensions& gce) bool State::extensions_supported(const ExtensionList& exts) const { - for (LeafIndex i{ 0 }; i < _tree.size; i.val++) { - const auto& maybe_leaf = _tree.leaf_node(i); - if (!maybe_leaf) { - continue; - } - - const auto& leaf = opt::get(maybe_leaf); - if (!leaf.verify_extension_support(exts)) { - return false; - } - } - - return true; + return _tree.all_leaves([&](auto /* i */, const auto& leaf_node) { + return leaf_node.verify_extension_support(exts); + }); } void @@ -1360,7 +1425,7 @@ State::unprotect(const MLSMessage& ct) throw ProtocolError("Unprotect of handshake message"); } - if (content_auth.wire_format != WireFormat::mls_ciphertext) { + if (content_auth.wire_format != WireFormat::mls_private_message) { throw ProtocolError("Application data not sent as PrivateMessage"); } @@ -1416,44 +1481,25 @@ State::valid(const LeafNode& leaf_node, // credential types currently in use by other members. // // Verify that the following fields are unique among the members of the group: - // signature_key - // encryption_key - const auto& signature_key = leaf_node.signature_key; - const auto& encryption_key = leaf_node.encryption_key; - auto unique_signature_key = true; - auto unique_encryption_key = true; - auto mutual_credential_support = true; - for (auto i = LeafIndex{ 0 }; i < _tree.size; i.val++) { - const auto maybe_leaf = _tree.leaf_node(i); - if (!maybe_leaf) { - continue; - } - - const auto& leaf = opt::get(maybe_leaf); - - // Signature keys are allowed to repeat within a leaf - unique_signature_key = - unique_signature_key && - ((i == index) || (signature_key != leaf.signature_key)); - unique_encryption_key = - unique_encryption_key && (encryption_key != leaf.encryption_key); - mutual_credential_support = - mutual_credential_support && - leaf.capabilities.credential_supported(leaf_node.credential) && - leaf_node.capabilities.credential_supported(leaf.credential); - } + // signature_key + // encryption_key + // + // Note: Uniqueness of signature and encryption keys is assured by the + // tree operations (add/update), so we do not need to verify those here. + const auto mutual_credential_support = + _tree.all_leaves([&](auto /* i */, const auto& leaf) { + return leaf.capabilities.credential_supported(leaf_node.credential) && + leaf_node.capabilities.credential_supported(leaf.credential); + }); // Verify that the extensions in the LeafNode are supported by checking that // the ID for each extension in the extensions field is listed in the // capabilities.extensions field of the LeafNode. - auto all_extensions_supported = - stdx::all_of(leaf_node.extensions.extensions, [&](const auto& ext) { - return stdx::contains(leaf_node.capabilities.extensions, ext.type); - }); + auto supports_own_extensions = + leaf_node.verify_extension_support(leaf_node.extensions); return (signature_valid && supports_group_extensions && correct_source && - mutual_credential_support && unique_signature_key && - unique_encryption_key && all_extensions_supported); + mutual_credential_support && supports_own_extensions); } bool @@ -1553,19 +1599,7 @@ State::valid(const ExternalInit& external_init) const bool State::valid(const GroupContextExtensions& gce) const { - // Verify that each extension is supported by all members - for (auto i = LeafIndex{ 0 }; i < _tree.size; i.val++) { - const auto maybe_leaf = _tree.leaf_node(i); - if (!maybe_leaf) { - continue; - } - - const auto& leaf = opt::get(maybe_leaf); - if (!leaf.verify_extension_support(gce.group_context_extensions)) { - return false; - } - } - return true; + return extensions_supported(gce.group_context_extensions); } bool @@ -2089,19 +2123,14 @@ State::group_info(bool inline_tree) const std::vector State::roster() const { - auto leaves = std::vector(_tree.size.val); - auto leaf_count = uint32_t(0); + auto leaves = std::vector{}; + leaves.reserve(_tree.size.val); - for (uint32_t i = 0; i < _tree.size.val; i++) { - const auto& maybe_leaf = _tree.leaf_node(LeafIndex{ i }); - if (!maybe_leaf) { - continue; - } - leaves.at(leaf_count) = opt::get(maybe_leaf); - leaf_count++; - } + _tree.all_leaves([&](auto /* i */, auto leaf) { + leaves.push_back(leaf); + return true; + }); - leaves.resize(leaf_count); return leaves; } @@ -2114,20 +2143,19 @@ State::epoch_authenticator() const LeafIndex State::leaf_for_roster_entry(RosterIndex index) const { - auto non_blank_leaves = uint32_t(0); - - for (auto i = LeafIndex{ 0 }; i < _tree.size; i.val++) { - const auto& maybe_leaf = _tree.leaf_node(i); - if (!maybe_leaf) { - continue; - } - if (non_blank_leaves == index.val) { - return i; + auto visited = RosterIndex{ 0 }; + auto found = std::optional{}; + _tree.all_leaves([&](auto i, const auto& /* leaf_node */) { + if (visited == index) { + found = i; + return false; } - non_blank_leaves += 1; - } - throw InvalidParameterError("Invalid roster index"); + visited.val += 1; + return true; + }); + + return opt::get(found); } State @@ -2143,4 +2171,4 @@ State::successor() const return next; } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/tree_math.cpp b/src/tree_math.cpp index d67f43716..f1a8be95b 100644 --- a/src/tree_math.cpp +++ b/src/tree_math.cpp @@ -1,5 +1,6 @@ #include "mls/tree_math.h" #include "mls/common.h" +#include #include @@ -19,7 +20,7 @@ log2(uint32_t x) return k - 1; } -namespace mls { +namespace MLS_NAMESPACE { LeafCount::LeafCount(const NodeCount w) { @@ -220,4 +221,4 @@ NodeIndex::level() const return k; } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/src/treekem.cpp b/src/treekem.cpp index 211c2f8a8..c8c566881 100644 --- a/src/treekem.cpp +++ b/src/treekem.cpp @@ -1,10 +1,11 @@ #include +#include #if ENABLE_TREE_DUMP #include #endif -namespace mls { +namespace MLS_NAMESPACE { // Utility method used for removing leaves from a resolution static void @@ -381,7 +382,7 @@ TreeKEMPublicKey::TreeKEMPublicKey(CipherSuite suite_in) } LeafIndex -TreeKEMPublicKey::add_leaf(const LeafNode& leaf) +TreeKEMPublicKey::allocate_leaf() { // Find the leftmost blank leaf node auto index = LeafIndex(0); @@ -390,7 +391,6 @@ TreeKEMPublicKey::add_leaf(const LeafNode& leaf) } // Extend the tree if necessary - auto ni = NodeIndex(index); if (index.val >= size.val) { if (size.val == 0) { size.val = 1; @@ -401,11 +401,29 @@ TreeKEMPublicKey::add_leaf(const LeafNode& leaf) } } + return index; +} + +LeafIndex +TreeKEMPublicKey::add_leaf(const LeafNode& leaf) +{ + // Check that the leaf node's keys are not already present in the tree + if (exists_in_tree(leaf.encryption_key, std::nullopt)) { + throw InvalidParameterError("Duplicate encryption key"); + } + + if (exists_in_tree(leaf.signature_key, std::nullopt)) { + throw InvalidParameterError("Duplicate signature key"); + } + + // Allocate a blank leaf for this node + const auto index = allocate_leaf(); + // Set the leaf node_at(index).node = Node{ leaf }; // Update the unmerged list - for (auto& n : ni.dirpath(size)) { + for (auto& n : NodeIndex(index).dirpath(size)) { if (!node_at(n).node) { continue; } @@ -424,6 +442,16 @@ TreeKEMPublicKey::add_leaf(const LeafNode& leaf) void TreeKEMPublicKey::update_leaf(LeafIndex index, const LeafNode& leaf) { + // Check that the leaf node's keys are not already present in the tree, except + // for the signature key, which is allowed to repeat. + if (exists_in_tree(leaf.encryption_key, std::nullopt)) { + throw InvalidParameterError("Duplicate encryption key"); + } + + if (exists_in_tree(leaf.signature_key, index)) { + throw InvalidParameterError("Duplicate signature key"); + } + blank_path(index); node_at(NodeIndex(index)).node = Node{ leaf }; clear_hash_path(index); @@ -448,7 +476,7 @@ TreeKEMPublicKey::blank_path(LeafIndex index) void TreeKEMPublicKey::merge(LeafIndex from, const UpdatePath& path) { - node_at(from).node = Node{ path.leaf_node }; + update_leaf(from, path.leaf_node); auto dp = filtered_direct_path(NodeIndex(from)); if (dp.size() != path.nodes.size()) { @@ -468,7 +496,6 @@ TreeKEMPublicKey::merge(LeafIndex from, const UpdatePath& path) path.nodes[i].public_key, parent_hash, {} } }; } - clear_hash_path(from); set_hash_all(); } @@ -531,7 +558,7 @@ TreeKEMPublicKey::parent_hash_valid() const } std::vector -TreeKEMPublicKey::resolve(NodeIndex index) const // NOLINT(misc-no-recursion) +TreeKEMPublicKey::resolve(NodeIndex index) const { auto at_leaf = (index.level() == 0); if (!node_at(index).blank()) { @@ -797,7 +824,7 @@ struct TreeHashInput }; const bytes& -TreeKEMPublicKey::get_hash(NodeIndex index) // NOLINT(misc-no-recursion) +TreeKEMPublicKey::get_hash(NodeIndex index) { if (hashes.count(index) > 0) { return hashes.at(index); @@ -868,19 +895,19 @@ TreeKEMPublicKey::parent_hashes( const FilteredDirectPath& fdp, const std::vector& path_nodes) const { + // An empty filtered direct path indicates a one-member tree, since there's + // nobody else there to encrypt with. In this special case, there's no + // parent hashing to be done. + if (fdp.empty()) { + return {}; + } + // The list of nodes for whom parent hashes are computed, namely: Direct path // excluding root, including leaf auto from_node = NodeIndex(from); auto dp = fdp; - if (!dp.empty()) { - // pop_back() on an empty list is undefined behavior - dp.pop_back(); - } - - if (from_node != NodeIndex::root(size)) { - // Handle the special case of a one-leaf tree - dp.insert(dp.begin(), { from_node, {} }); - } + dp.pop_back(); + dp.insert(dp.begin(), { from_node, {} }); if (dp.size() != path_nodes.size()) { throw ProtocolError("Malformed UpdatePath"); @@ -905,7 +932,6 @@ TreeKEMPublicKey::parent_hashes( } const bytes& -// NOLINTNEXTLINE(misc-no-recursion) TreeKEMPublicKey::original_tree_hash(TreeHashCache& cache, NodeIndex index, std::vector parent_except) const @@ -1011,6 +1037,24 @@ TreeKEMPublicKey::parent_hash_valid(LeafIndex from, return leaf_ph && opt::get(leaf_ph) == hash_chain[0]; } +bool +TreeKEMPublicKey::exists_in_tree(const HPKEPublicKey& key, + std::optional except) const +{ + return any_leaf([&](auto i, const auto& node) { + return i != except && node.encryption_key == key; + }); +} + +bool +TreeKEMPublicKey::exists_in_tree(const SignaturePublicKey& key, + std::optional except) const +{ + return any_leaf([&](auto i, const auto& node) { + return i != except && node.signature_key == key; + }); +} + tls::ostream& operator<<(tls::ostream& str, const TreeKEMPublicKey& obj) { @@ -1080,4 +1124,4 @@ operator>>(tls::istream& str, TreeKEMPublicKey& obj) return str; } -} // namespace mls +} // namespace MLS_NAMESPACE diff --git a/test/credential.cpp b/test/credential.cpp index 34ba3b1c5..d89397471 100644 --- a/test/credential.cpp +++ b/test/credential.cpp @@ -1,7 +1,7 @@ #include #include -using namespace mls; +using namespace MLS_NAMESPACE; TEST_CASE("Basic Credential") { @@ -18,6 +18,101 @@ TEST_CASE("Basic Credential") REQUIRE(basic.identity == user_id); } +TEST_CASE("X.509 Credential") +{ + auto suite = CipherSuite{ CipherSuite::ID::P256_AES128GCM_SHA256_P256 }; + + const auto priv_data = from_hex( + "afdee46291cd304277fdd2599f125c4cc0aa1e539df7fd7c8032f632c5d0d3a4"); + + const auto leaf_der = + from_hex("308201363081dda003020102020100300a06082a8648ce3d0403023014311230" + "100603550403130946616b6520434120303020170d3233303730313138353334" + "385a180f32313232303630393138353334385a30123110300e06035504031307" + "5375626a6563743059301306072a8648ce3d020106082a8648ce3d0301070342" + "00041dd062efebc93e62c8c5a4984e6a1df6192ae87eaea0666e4fc5ff8d610c" + "2e465d077fafd72e249ce5ba602188df4203b78422380239bbcb9ab88b6940ba" + "f6a8a320301e300e0603551d0f0101ff0404030202a4300c0603551d130101ff" + "04023000300a06082a8648ce3d0403020348003045022041d8341e498806ec4b" + "7aa097deaeffe6b752ecc49e5093ad0d2ffba7ca629eee0221009f24accf35a9" + "05dcd8720ad6dfa881e7614f3b6abd9f49ebf21e2439060402eb"); + + auto priv = SignaturePrivateKey::parse(suite, priv_data); + auto pub = priv.public_key; + + auto cred = Credential::x509({ leaf_der }); + REQUIRE(cred.valid_for(pub)); + + const auto& x509 = cred.get(); + REQUIRE(x509.der_chain.size() == 1); + REQUIRE(x509.der_chain.front().data == leaf_der); +} + +TEST_CASE("Multi Credential") +{ + auto suite = CipherSuite{ CipherSuite::ID::P256_AES128GCM_SHA256_P256 }; + + // First X.509 Credential + const auto leaf_der_1 = + from_hex("308201363081dda003020102020100300a06082a8648ce3d0403023014311230" + "100603550403130946616b6520434120303020170d3233303730313138353334" + "385a180f32313232303630393138353334385a30123110300e06035504031307" + "5375626a6563743059301306072a8648ce3d020106082a8648ce3d0301070342" + "00041dd062efebc93e62c8c5a4984e6a1df6192ae87eaea0666e4fc5ff8d610c" + "2e465d077fafd72e249ce5ba602188df4203b78422380239bbcb9ab88b6940ba" + "f6a8a320301e300e0603551d0f0101ff0404030202a4300c0603551d130101ff" + "04023000300a06082a8648ce3d0403020348003045022041d8341e498806ec4b" + "7aa097deaeffe6b752ecc49e5093ad0d2ffba7ca629eee0221009f24accf35a9" + "05dcd8720ad6dfa881e7614f3b6abd9f49ebf21e2439060402eb"); + const auto cred_priv_data_1 = from_hex( + "afdee46291cd304277fdd2599f125c4cc0aa1e539df7fd7c8032f632c5d0d3a4"); + + auto cred_priv_1 = SignaturePrivateKey::parse(suite, cred_priv_data_1); + auto cred_pub_1 = cred_priv_1.public_key; + + auto cred_1 = Credential::x509({ leaf_der_1 }); + REQUIRE(cred_1.valid_for(cred_pub_1)); + + // Second X.509 Credential + const auto leaf_der_2 = + from_hex("308201353081dda003020102020100300a06082a8648ce3d0403023014311230" + "100603550403130946616b6520434120303020170d3233303730313139303234" + "305a180f32313232303630393139303234305a30123110300e06035504031307" + "5375626a6563743059301306072a8648ce3d020106082a8648ce3d0301070342" + "0004e7f7987f024d0d1b420018a585929e690f95b6fe7b23ec1ff6532b1c55c4" + "75ef36b826e4b54bd60b8823f3fc222c28369771a9ed0a644df351e16ad495dc" + "fb54a320301e300e0603551d0f0101ff0404030202a4300c0603551d130101ff" + "04023000300a06082a8648ce3d040302034700304402200d7e0e5362cfe4d551" + "cbb6a5b2b64541e30e86e10e734e84c1e24b46d1e098bc022037fc32a59b4062" + "c14b3323a20a0c7a5e05bbd3f27e22dc225ddd69ca771b90fc"); + const auto cred_priv_data_2 = from_hex( + "8915f49863cb24d6553fab0036da18a0fec431ae0cc94255010f6ed35555631e"); + + auto cred_priv_2 = SignaturePrivateKey::parse(suite, cred_priv_data_2); + auto cred_pub_2 = cred_priv_2.public_key; + + auto cred_2 = Credential::x509({ leaf_der_2 }); + REQUIRE(cred_2.valid_for(cred_pub_2)); + + // Multi-Credential + auto priv = SignaturePrivateKey::generate(suite); + auto pub = priv.public_key; + + auto cred = Credential::multi( + { { suite, cred_1, cred_priv_1 }, { suite, cred_2, cred_priv_2 } }, pub); + REQUIRE(cred.valid_for(pub)); + + auto multi = cred.get(); + const auto& bindings = multi.bindings; + REQUIRE(bindings.size() == 2); + REQUIRE(bindings[0].credential == cred_1); + REQUIRE(bindings[0].credential_key == cred_pub_1); + REQUIRE(bindings[0].credential.valid_for(bindings[0].credential_key)); + REQUIRE(bindings[1].credential == cred_2); + REQUIRE(bindings[1].credential_key == cred_pub_2); + REQUIRE(bindings[1].credential.valid_for(bindings[1].credential_key)); +} + TEST_CASE("X509 Credential Depth 2") { // Chain is of depth 2 diff --git a/test/crypto.cpp b/test/crypto.cpp index 8dd930c25..393a0a040 100644 --- a/test/crypto.cpp +++ b/test/crypto.cpp @@ -1,10 +1,9 @@ #include #include #include - #include -using namespace mls; +using namespace MLS_NAMESPACE; using namespace mls_vectors; TEST_CASE("Basic HPKE") @@ -91,6 +90,42 @@ TEST_CASE("Signature Key Serializion") } } +TEST_CASE("Signature Key JWK Import/Export") +{ + for (auto suite_id : all_supported_suites) { + const auto suite = CipherSuite{ suite_id }; + const auto priv = SignaturePrivateKey::generate(suite); + const auto pub = priv.public_key; + + const auto encoded_priv = priv.to_jwk(suite); + const auto decoded_priv = + SignaturePrivateKey::from_jwk(suite, encoded_priv); + REQUIRE(decoded_priv == priv); + + const auto encoded_pub = pub.to_jwk(suite); + const auto decoded_pub = SignaturePublicKey::from_jwk(suite, encoded_pub); + REQUIRE(decoded_pub == pub); + } + + // Test PublicJWK parsing + const auto full_jwk = R"({ + "kty": "OKP", + "crv": "Ed25519", + "kid": "059fc2ee-5ef6-456a-91d8-49c422c772b2", + "x": "miljqilAZV2yFkqIBhrxhvt2wIMvPtkNEFzuziEGOtI" + })"s; + + const auto known_scheme = SignatureScheme::ed25519; + const auto known_key_id = std::string("059fc2ee-5ef6-456a-91d8-49c422c772b2"); + const auto knwon_pub_data = from_hex( + "9a2963aa2940655db2164a88061af186fb76c0832f3ed90d105ceece21063ad2"); + + const auto jwk = PublicJWK::parse(full_jwk); + REQUIRE(jwk.signature_scheme == known_scheme); + REQUIRE(jwk.key_id == known_key_id); + REQUIRE(jwk.public_key == SignaturePublicKey{ knwon_pub_data }); +} + TEST_CASE("Crypto Interop") { for (auto suite : all_supported_suites) { diff --git a/test/key_schedule.cpp b/test/key_schedule.cpp index 7ccc8d27e..81880c158 100644 --- a/test/key_schedule.cpp +++ b/test/key_schedule.cpp @@ -2,7 +2,7 @@ #include #include -using namespace mls; +using namespace MLS_NAMESPACE; using namespace mls_vectors; TEST_CASE("Secret Tree Interop") diff --git a/test/messages.cpp b/test/messages.cpp index ababc97e7..033c9facb 100644 --- a/test/messages.cpp +++ b/test/messages.cpp @@ -4,7 +4,7 @@ #include #include -using namespace mls; +using namespace MLS_NAMESPACE; using namespace mls_vectors; TEST_CASE("Extensions") @@ -98,23 +98,34 @@ class MLSMessageTest TEST_CASE_FIXTURE(MLSMessageTest, "AuthenticatedContent Sign/Verify") { // Verify that a sign / verify round-trip works - auto content_auth = AuthenticatedContent::sign( - WireFormat::mls_ciphertext, application_content, suite, sig_priv, context); + auto content_auth = + AuthenticatedContent::sign(WireFormat::mls_private_message, + application_content, + suite, + sig_priv, + context); REQUIRE(content_auth.verify(suite, sig_priv.public_key, context)); REQUIRE(content_auth.content == application_content); // Verify that `mls_plaintext` is forbidden for ApplicationData // NOLINTNEXTLINE(llvm-else-after-return, readability-else-after-return) - REQUIRE_THROWS(AuthenticatedContent::sign( - WireFormat::mls_plaintext, application_content, suite, sig_priv, context)); + REQUIRE_THROWS(AuthenticatedContent::sign(WireFormat::mls_public_message, + application_content, + suite, + sig_priv, + context)); } TEST_CASE_FIXTURE(MLSMessageTest, "PublicMessage Protect/Unprotect") { auto content = proposal_content; - auto content_auth_original = AuthenticatedContent::sign( - WireFormat::mls_plaintext, std::move(content), suite, sig_priv, context); + auto content_auth_original = + AuthenticatedContent::sign(WireFormat::mls_public_message, + std::move(content), + suite, + sig_priv, + context); auto pt = PublicMessage::protect( content_auth_original, suite, membership_key, context); @@ -125,8 +136,12 @@ TEST_CASE_FIXTURE(MLSMessageTest, "PublicMessage Protect/Unprotect") TEST_CASE_FIXTURE(MLSMessageTest, "PrivateMessage Protect/Unprotect") { auto content = proposal_content; - auto content_auth_original = AuthenticatedContent::sign( - WireFormat::mls_ciphertext, std::move(content), suite, sig_priv, context); + auto content_auth_original = + AuthenticatedContent::sign(WireFormat::mls_private_message, + std::move(content), + suite, + sig_priv, + context); auto ct = PrivateMessage::protect( content_auth_original, suite, keys, sender_data_secret, padding_size); diff --git a/test/session.cpp b/test/session.cpp index 192634f60..60b99004e 100644 --- a/test/session.cpp +++ b/test/session.cpp @@ -2,7 +2,7 @@ #include #include -using namespace mls; +using namespace MLS_NAMESPACE; class SessionTest { @@ -286,23 +286,24 @@ TEST_CASE("Session with X509 Credential") "d9865c93f3952abc7e671e625b8479214c1c9b62a7cc6a51a84a3610f4"); const std::vector der_chain{ leaf_der, issuing_der }; - const mls::CipherSuite suite{ - mls::CipherSuite::ID::P256_AES128GCM_SHA256_P256 + const MLS_NAMESPACE::CipherSuite suite{ + MLS_NAMESPACE::CipherSuite::ID::P256_AES128GCM_SHA256_P256 }; auto alice_id = from_ascii("alice"); - auto alice_sig_priv = mls::SignaturePrivateKey::parse(suite, key_raw); - auto alice_cred = mls::Credential::x509(der_chain); - auto alice_client = mls::Client(suite, alice_sig_priv, alice_cred); + auto alice_sig_priv = + MLS_NAMESPACE::SignaturePrivateKey::parse(suite, key_raw); + auto alice_cred = MLS_NAMESPACE::Credential::x509(der_chain); + auto alice_client = MLS_NAMESPACE::Client(suite, alice_sig_priv, alice_cred); auto group_id = bytes{ 0, 1, 2, 3 }; auto alice_session = alice_client.begin_session(group_id); auto bob_id = from_ascii("bob"); - auto bob_sig_priv = mls::SignaturePrivateKey::generate(suite); - auto bob_cred = mls::Credential::basic(bob_id); + auto bob_sig_priv = MLS_NAMESPACE::SignaturePrivateKey::generate(suite); + auto bob_cred = MLS_NAMESPACE::Credential::basic(bob_id); - auto bob_client = mls::Client(suite, bob_sig_priv, bob_cred); + auto bob_client = MLS_NAMESPACE::Client(suite, bob_sig_priv, bob_cred); auto bob_pending_join = bob_client.start_join(); diff --git a/test/state.cpp b/test/state.cpp index 9d93220b9..4daf5cf97 100644 --- a/test/state.cpp +++ b/test/state.cpp @@ -3,7 +3,7 @@ #include #include -using namespace mls; +using namespace MLS_NAMESPACE; struct CustomExtension { @@ -365,6 +365,60 @@ TEST_CASE_FIXTURE(StateTest, "Two Person with PSK") REQUIRE(first1 == second0); } +TEST_CASE_FIXTURE(StateTest, "Two Person with Replacement") +{ + // Initialize the creator's state + auto first0 = State{ group_id, + suite, + leaf_privs[0], + identity_privs[0], + key_packages[0].leaf_node, + {} }; + + // Handle the Add proposal and create a Commit + const auto add1 = first0.add_proposal(key_packages[1]); + const auto [commit1, welcome1, first1_] = + first0.commit(fresh_secret(), CommitOpts{ { add1 }, true, false, {} }, {}); + silence_unused(commit1); + auto first1 = first1_; + + // Initialize the second participant from the Welcome + auto second1 = State{ init_privs[1], + leaf_privs[1], + identity_privs[1], + key_packages[1], + welcome1, + std::nullopt, + {} }; + REQUIRE(first1 == second1); + + // Create a new appearance of the first member + const auto [init_priv, leaf_priv, _identity_priv, key_package_] = + make_client(); + const auto identity_priv = identity_privs[0]; + auto key_package = key_package_; + key_package.leaf_node.signature_key = identity_priv.public_key; + key_package.leaf_node.sign(suite, identity_priv, std::nullopt); + key_package.sign(identity_priv); + + // Create a commit replacing the first member + const auto remove2 = second1.remove_proposal(LeafIndex{ 0 }); + const auto add2 = second1.add_proposal(key_package); + const auto [commit2, welcome2, second2_] = second1.commit( + fresh_secret(), CommitOpts{ { add2, remove2 }, true, false, {} }, {}); + auto second2 = second2_; + silence_unused(commit2); + + // Initialize the new first member from the Welcome + const auto first2 = + State{ init_priv, leaf_priv, identity_priv, key_package, welcome2, + std::nullopt, {} }; + REQUIRE(first2 == second2); + + auto group = std::vector{ first2, second2 }; + verify_group_functionality(group); +} + TEST_CASE_FIXTURE(StateTest, "External Join") { // Initialize the creator's state diff --git a/test/treekem.cpp b/test/treekem.cpp index c50251064..be2c8d3ec 100644 --- a/test/treekem.cpp +++ b/test/treekem.cpp @@ -4,7 +4,7 @@ #include #include -using namespace mls; +using namespace MLS_NAMESPACE; using namespace mls_vectors; class TreeKEMTest @@ -69,9 +69,9 @@ TEST_CASE_FIXTURE(TreeKEMTest, "TreeKEM Private Key") const auto hash_size = suite.digest().hash_size; // Create a tree with N blank leaves - auto [_leaf_priv, _sig_priv, leaf_node] = new_leaf_node(); auto pub = TreeKEMPublicKey(suite); for (auto i = uint32_t(0); i < size.val; i++) { + auto [_leaf_priv, _sig_priv, leaf_node] = new_leaf_node(); pub.add_leaf(leaf_node); } @@ -144,9 +144,10 @@ TEST_CASE_FIXTURE(TreeKEMTest, "TreeKEM Public Key") auto pub = TreeKEMPublicKey{ suite }; for (uint32_t i = 0; i < size.val; i++) { // Add a leaf - auto [init_priv, sig_priv, leaf_before_] = new_leaf_node(); + auto [_init_priv_before, _sig_priv_before, leaf_before_] = new_leaf_node(); auto leaf_before = leaf_before_; - silence_unused(init_priv); + silence_unused(_init_priv_before); + silence_unused(_sig_priv_before); auto index = LeafIndex(i); @@ -162,20 +163,23 @@ TEST_CASE_FIXTURE(TreeKEMTest, "TreeKEM Public Key") REQUIRE(found_leaf == leaf_before); // Manually construct a direct path to populate nodes above the new leaf - auto path = UpdatePath{ leaf_before, {} }; + const auto [_init_priv_after, sig_priv_after, leaf_after_] = + new_leaf_node(); + const auto leaf_after = leaf_after_; + auto path = UpdatePath{ leaf_after, {} }; auto dp = pub.filtered_direct_path(NodeIndex(index)); while (path.nodes.size() < dp.size()) { auto node_pub = HPKEPrivateKey::generate(suite).public_key; path.nodes.push_back({ node_pub, {} }); } - path.leaf_node.sign(suite, sig_priv, std::nullopt); + path.leaf_node.sign(suite, sig_priv_after, std::nullopt); // Merge the direct path (ignoring parent hash validity) pub.merge(index, path); - auto leaf_after = path.leaf_node; - found = pub.find(leaf_after); + const auto& re_signed_leaf = path.leaf_node; + found = pub.find(re_signed_leaf); REQUIRE(found); REQUIRE(found == index); for (const auto& [n_, _res] : dp) { @@ -185,7 +189,7 @@ TEST_CASE_FIXTURE(TreeKEMTest, "TreeKEM Public Key") found_leaf = pub.leaf_node(index); REQUIRE(found_leaf); - REQUIRE(found_leaf == leaf_after); + REQUIRE(found_leaf == re_signed_leaf); } // Remove a node and verify that the resolution comes out right @@ -237,7 +241,6 @@ TEST_CASE_FIXTURE(TreeKEMTest, "TreeKEM encap/decap") auto ok = ok_; REQUIRE(ok); - pubs[i].merge(adder, path); REQUIRE(privs[i].consistent(pubs[i])); // New joiner initializes their private key diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index b991cb641..c1612f59b 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -2,5 +2,16 @@ set(CURRENT_LIB_NAME third_party) add_library(${CURRENT_LIB_NAME} INTERFACE) target_include_directories(${CURRENT_LIB_NAME} - INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} + INTERFACE + $ + $ ) + +### +### Install +### + +install(TARGETS ${CURRENT_LIB_NAME} EXPORT mlspp-targets) +install(FILES variant.hpp + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}) diff --git a/vcpkg.json b/vcpkg.json index f58705611..0ffd459e8 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,7 +7,8 @@ "name": "openssl", "version>=": "1.1.1n" }, - "doctest" + "doctest", + "nlohmann-json" ], "builtin-baseline": "3b3bd424827a1f7f4813216f6b32b6c61e386b2e", "overrides": [