Squashed history.

This commit is contained in:
Graeme Connell 2022-10-20 16:52:03 -06:00
commit 76a9869339
278 changed files with 27698 additions and 0 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.gopath
.gocache
.git
enclave/build
enclave/core.*

View File

@ -0,0 +1,41 @@
name: Docker Caching
description: Cache a docker image
inputs:
dockerdir:
required: true
type: string
imagename:
required: true
type: string
target:
required: false
type: string
dockerfile:
required: true
type: string
runs:
using: composite
steps:
- name: Check for cached docker image
id: cached-docker
uses: actions/cache@v3
with:
path: dockerimage-${{ hashFiles(inputs.dockerfile) }}.tar
key: ${{ runner.os }}-dockerimagetar-${{ hashFiles(inputs.dockerfile) }}
restore-keys: ${{ runner.os }}-dockerimagetar-
- name: Load docker image
run: docker load --input dockerimage-*.tar || true
shell: bash
- name: Build/label docker image
run: docker build -t ${{ inputs.imagename }} -f ${{ inputs.dockerfile }} ${{ inputs.dockerdir }} --target=${{ inputs.target }} --cache-from ${{ inputs.imagename }}:latest
shell: bash
- name: Save docker image
if: steps.cached-docker.outputs.cache-hit != 'true'
run: docker save --output dockerimage-${{ hashFiles(inputs.dockerfile) }}.tar ${{ inputs.imagename }}:latest $(docker history -q ${{ inputs.imagename }}:latest | grep -v missing)
shell: bash

42
.github/workflows/push.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Build and push Docker image
on:
release:
types: [published]
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
steps:
- name: Checkout main project
uses: actions/checkout@v3
with:
submodules: true
- name: Docker cache
uses: ./.github/workflows/dockercache
with:
dockerdir: .
imagename: svr2_buildenv
target: builder
dockerfile: docker/Dockerfile
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: 'Docker login'
run:
az acr login --name ${{ secrets.AZURE_CONTAINER_REGISTRY_NAME }}
- name: Build and push container image
run: |
make container
docker tag svr2_runenv:latest "${{ secrets.REGISTRY_LOGIN_SERVER }}/svr2:${GITHUB_REF_NAME}"
docker push "${{ secrets.REGISTRY_LOGIN_SERVER }}/svr2:${GITHUB_REF_NAME}"

29
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
permissions:
packages: read
contents: read
steps:
- name: Checkout main project
uses: actions/checkout@v3
with:
submodules: true
- name: Docker cache
uses: ./.github/workflows/dockercache
with:
dockerdir: .
imagename: svr2_buildenv
target: builder
dockerfile: docker/Dockerfile
- name: Build and test
run: make
- name: Validate
run: make docker_validate

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
*.sw?
.gocache
.gopath
**/.devcontainer
**/.vscode
**/*.code-workspace
.tool-versions
.idea

18
.gitmodules vendored Normal file
View File

@ -0,0 +1,18 @@
[submodule "enclave/protobuf"]
path = enclave/protobuf
url = https://github.com/protocolbuffers/protobuf.git
[submodule "enclave/noise-c"]
path = enclave/noise-c
url = https://github.com/rweather/noise-c.git
[submodule "enclave/SipHash"]
path = enclave/SipHash
url = https://github.com/veorq/SipHash
[submodule "enclave/googletest"]
path = enclave/googletest
url = https://github.com/google/googletest
[submodule "enclave/libsodium"]
path = enclave/libsodium
url = https://github.com/jedisct1/libsodium
[submodule "docker/aws-nitro-enclaves-nsm-api"]
path = docker/aws-nitro-enclaves-nsm-api
url = https://github.com/aws/aws-nitro-enclaves-nsm-api.git

661
LICENSE Normal file
View File

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

68
Makefile Normal file
View File

@ -0,0 +1,68 @@
dockall: docker_all
all: validate host enclave control
MAKE_ARGS ?= --keep-going
enclave_testbin: | git
$(MAKE) $(MAKE_ARGS) -C enclave build/enclave.test
validate:
$(MAKE) $(MAKE_ARGS) -C enclave validate
$(MAKE) $(MAKE_ARGS) -C host validate
./check_copyrights.sh
git:
git submodule init || true
git submodule update || true
enclave: enclave_testbin
$(MAKE) $(MAKE_ARGS) -C enclave all
enclave_test:
$(MAKE) $(MAKE_ARGS) -C enclave test
host: enclave_testbin
$(MAKE) $(MAKE_ARGS) -C host all
control:
$(MAKE) $(MAKE_ARGS) -C host control
clean:
$(MAKE) $(MAKE_ARGS) -C enclave clean
$(MAKE) $(MAKE_ARGS) -C host clean
dockerbase: | git
docker build -f docker/Dockerfile -t svr2_buildenv --target=builder .
PARALLEL ?= $(shell cat /proc/cpuinfo | grep '^cpu cores' | awk '{ sum += $$4 } END { print sum }')
DOCKER_MAKE_ARGS ?= -j$(PARALLEL) MAKE_ARGS="$(MAKE_ARGS)"
ARCH ?= $(shell arch)
ifeq ($(ARCH),arm64)
DOCKER_MAKE_ARGS += 'GO_TEST_FLAGS=-short' # long tests can cause qemu crashes in x86 emulation
endif
DOCKER_ARGS ?=
docker_%: dockerbase
docker run \
-v "$$(pwd):/src" \
-u "$$(id -u):$$(id -g)" \
$(DOCKER_ARGS) \
svr2_buildenv /bin/bash -c "make V=$(V) $(DOCKER_MAKE_ARGS) $*"
dockersh: dockerbase
docker run --rm -it \
-v "$$(pwd):/src" \
-u "$$(id -u):$$(id -g)" \
-e "TERM=xterm-256color" \
$(DOCKER_ARGS) \
svr2_buildenv
container: dockerbase
docker build -f docker/Dockerfile -t svr2_runenv .
enclave_release: docker_enclave_releaser
enclave_releaser: enclave host # depends on 'host' so its tests will run
cp -vn enclave/build/enclave.signed "enclave/releases/default.$$(/opt/openenclave/bin/oesign dump -e enclave/build/enclave.signed | fgrep -i mrenclave | cut -d '=' -f2)"
cp -vn enclave/build/enclave.small "enclave/releases/small.$$(/opt/openenclave/bin/oesign dump -e enclave/build/enclave.small | fgrep -i mrenclave | cut -d '=' -f2)"
.PHONY: all clean enclave host dockersh docker dockerbase git validate enclave_testbin control enclave_release enclave_releaser

78
README.md Normal file
View File

@ -0,0 +1,78 @@
# Secure Value Recovery Service v2
The SecureValueRecovery2 (SVR2) project aims to store client-side secrets
server-side protected by a human-remembered (and thus, low-entropy) pin.
It does so by limiting the number of attempts to recover such a secret to
a very small guess count, to disallow brute-force attacks that would otherwise
trivially recover such a secret. To limit the number of recovery attempts,
SVR2 keeps persistent state on the guess count, along with the secret itself,
in a multi-replica, strong-consensus, shared storage mechanism based on
in-memory Raft.
SVR2 is designed, first and foremost, to not leak the secret
material, and, secondarily, to provide the material back to clients. Given
this, if there is a choice between "lose the secret material forever" and
"store the secret material but potentially leak it", we'll choose the former.
This means that, in some cases, we've chosen to allow the system to lose
_liveness_ (the ability to serve back anything) in order to maintain the
security properties of the system. We'll happily discard every secret in the
system rather than expose one of the secrets to a leak.
## History
SVR2 is a successor to the
[SecureValueRecovery](https://github.com/signalapp/SecureValueRecovery)
project that Signal already uses for the above stated purpose. We've built
a second version of this system to handle a few specific issues:
- Update to SGX DCAP capabilities
- Provide better operational handling of crashes/failures via self-healing
- Simplify to a single-replica-group model since SGX CPUs now have an EPC size of hundreds of gigabytes
As part of SGX DCAP updates, this project also attempts to be as safe as
possible while running on SGX TME memory, compared to the differing
security guarantees of the SGX MEE memory utilized in the original version.
## Building
In order to build and test everything in this repository, you should be able to
just run `make` at the top-level. You must have a valid `docker` installed
locally to do this. Running this at the top-level will:
- Create a docker image in which to build things
- Build `enclave/enclave.test` (a debug enclave for simulation/testing) and
`enclave/enclave.signed` (a production enclave)
- Build and test the host-side process in `host/`
If you'd like to incrementally build and change things, you can do so by
running `make dockersh`. This will build the aforementioned docker image,
then drop you inside of it in a `bash` shell. You can then run any of
```
make all # Make everything
make enclave # Make all of the enclave stuf
make host # Make all of the host stuff
(cd enclave && make $SOMETARGET) # Make just a specific target in enclave
(cd host && make $SOMETARGET) # Make just a specific target in host
```
## Code layout
Code is divided into a few main directories at the top-level
* `docker` - Contains the spec for the docker image used to build everything else.
* `shared` - Contains all code/configs that must be shared between the host and enclave.
This includes any protos that the host and enclave use to communicate,
and the definitions of ocalls/ecalls (the `*.edl` files).
* `enclave` - Contains all code and build rules for building the in-enclave binary.
This is a C++ codebase.
* `host` - Contains all code and build rules for building the host-side binary, which
starts up an enclave, then communicates with it. This is a Go codebase.
* `docs` - Contains additional documentation above and beyond the host/enclave `README.md`
docs on specific topics.
## License
Copyright 2023 Signal Messenger, LLC
Licensed under the [AGPLv3](LICENSE)

11
SECURITY.md Normal file
View File

@ -0,0 +1,11 @@
## Reporting a Vulnerability
If you've found a security vulnerability in this repository,
please report it via email to <security@signal.org>.
Please only use this address to report security flaws in the Signal application (including this
repository). For questions, support, or feature requests concerning the app, please submit a
[support request][] or join the [unofficial community forum][].
[support request]: https://support.signal.org/hc/requests/new
[unofficial community forum]: https://community.signalusers.org/

12
check_copyrights.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
OUT=0
for pattern in '*.c' '*.cc' '*.h' '*.go' '*.proto'; do
for file in `find ./ -name $pattern -type f | grep -v -f <(cat .gitmodules | grep path | awk '{print $3}') | egrep -v 'gopath|enclave/build|host/enclave/c'`; do
if ! grep -q Copyright $file; then
OUT=1
echo "Missing copyright in '$file'" 1>&2
fi
done
done
exit $OUT

139
docker/Dockerfile Normal file
View File

@ -0,0 +1,139 @@
# syntax=docker/dockerfile:1
# To build use:
# docker build -t oebuild .
FROM amd64/debian:bullseye-20220912 AS base
LABEL description="linux build environment for sgx."
COPY docker/apt.conf docker/sources.list /etc/apt/
RUN apt-get update && \
apt-get -y install \
gpg \
gnupg2 \
wget \
software-properties-common
COPY docker/sgx.sources.list docker/ms.sources.list /etc/apt/sources.list.d/
# ms and intel repos keep old packages around,
# however if they remove some of these in the future
# binary packages can be retrieved from github releases
RUN wget -qO - https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | apt-key add - && \
wget -qO - https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \
apt-get update && \
apt -y install \
libsgx-ae-id-enclave=1.16.100.2-focal1 \
libsgx-ae-pce=2.19.100.3-focal1 \
libsgx-ae-qe3=1.16.100.2-focal1 \
libsgx-dcap-ql=1.16.100.2-focal1 \
libsgx-dcap-ql-dev=1.16.100.2-focal1 \
libsgx-enclave-common=2.19.100.3-focal1 \
libsgx-headers=2.19.100.3-focal1 \
libsgx-pce-logic=1.16.100.2-focal1 \
libsgx-qe3-logic=1.16.100.2-focal1 \
libsgx-urts=2.19.100.3-focal1 \
open-enclave=0.19.0
FROM public.ecr.aws/amazonlinux/amazonlinux@sha256:94e7183b0739140dbd5b639fb7600f0a2299cec5df8780c26d9cb409da5315a9 AS nsmbuild
ENV HOST_MACHINE=x86_64
ENV RUST_VERSION=1.58.1
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
RUN yum install -y gcc
RUN set -eux; \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs/ | sh -s -- --default-toolchain ${RUST_VERSION} -y ; \
chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \
rustup --version; \
cargo --version; \
rustc --version
COPY docker/aws-nitro-enclaves-nsm-api /build
COPY docker/aws-nitro.Cargo.lock /build/Cargo.lock
WORKDIR /build
RUN set -eux; \
(cd nsm-lib && cargo build --release --locked)
RUN ar mD target/release/libnsm.a $(ar t target/release/libnsm.a | env -u LANG LC_ALL=C sort)
COPY docker/check_hash.sh docker/sha256.* ./
RUN ./check_hash.sh target/release/libnsm.a
FROM base AS builder
RUN mkdir /src && \
apt-get update && \
apt-get -y install \
clang-11 \
libssl-dev \
gdb \
libtool \
bison \
automake \
flex \
libcurl4 \
pkg-config \
make \
unzip \
git \
gcc \
libgtest-dev
COPY docker/check_hash.sh docker/sha256.* ./
ARG PROTOBUF_PLATFORM=linux-x86_64
ARG PROTOBUF_VERSION=21.8
ARG PROTOBUF_BASE=protoc-${PROTOBUF_VERSION}-${PROTOBUF_PLATFORM}
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/${PROTOBUF_BASE}.zip \
&& /bin/bash ./check_hash.sh ${PROTOBUF_BASE}.zip \
&& mkdir -p ${PROTOBUF_BASE} \
&& cd ${PROTOBUF_BASE} \
&& unzip -o ../${PROTOBUF_BASE}.zip \
&& cd .. \
&& mv ${PROTOBUF_BASE} /opt/protobuf
ARG GOLANG_PLATFORM=linux-amd64
ARG GOLANG_VERSION=1.20.2
ARG GOLANG_TAR_GZ=go${GOLANG_VERSION}.${GOLANG_PLATFORM}.tar.gz
RUN wget https://go.dev/dl/${GOLANG_TAR_GZ} \
&& /bin/bash ./check_hash.sh ${GOLANG_TAR_GZ} \
&& tar xzf ${GOLANG_TAR_GZ} \
&& mv go /opt/
ENV PATH="/opt/openenclave/bin:/opt/go/bin:/opt/protobuf/bin:${PATH}"
ENV GOROOT="/opt/go"
ENV GOBIN="/opt/go/bin"
ENV PKG_CONFIG_PATH="/opt/openenclave/share/pkgconfig"
ARG PROTOC_GEN_GO_GITREV=6875c3d7242d1a3db910ce8a504f124cb840c23a
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOC_GEN_GO_GITREV}
RUN echo "export PS1='buildenv: \w$ '" >> /etc/bash.bashrc
# Set this after `go install` so we don't use the same cache as root.
ENV GOPATH="/src/.gopath"
ENV GOCACHE="/src/.gocache"
WORKDIR /src
COPY --from=nsmbuild /build/target/release/libnsm.a /opt/nsm/libnsm.a
COPY --from=nsmbuild /build/target/release/nsm.h /opt/nsm/nsm.h
CMD ["/bin/bash"]
FROM builder AS build
COPY . /src
RUN cd /src && make clean && make -j16 all enclave_releaser
FROM base AS runner
RUN apt-get update && apt-get install -y \
libsgx-dcap-default-qpl=1.16.100.2-focal1 \
libsgx-dcap-default-qpl-dev=1.16.100.2-focal1
COPY docker/sgx_default_qcnl_azure.conf /etc/sgx_default_qcnl.conf
COPY --from=build /src/host/main /bin/svr2
COPY --from=build /src/enclave/releases /enclaves
COPY --from=build /src/host/cmd/control/control /bin/svr2control
ENTRYPOINT ["/bin/svr2"]

15
docker/apt.conf Normal file
View File

@ -0,0 +1,15 @@
Apt {
Architecture "amd64";
Architectures "amd64";
};
Acquire::Check-Valid-Until "false";
Acquire::Languages "none";
Binary::apt-get::Acquire::AllowInsecureRepositories "false";
APT::Install-Recommends "false";
// go easy on snapshot.debian.org
Acquire::http::Dl-Limit "10000";
Acquire::https::Dl-Limit "10000";
Acquire::Retries "5";

@ -0,0 +1 @@
Subproject commit 944562dacce23dc947bea1df60b5dd3a51fb8c4f

561
docker/aws-nitro.Cargo.lock generated Normal file
View File

@ -0,0 +1,561 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "aws-nitro-enclaves-nsm-api"
version = "0.2.1"
dependencies = [
"libc",
"log",
"nix 0.20.2",
"serde",
"serde_bytes",
"serde_cbor",
]
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "cbindgen"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb"
dependencies = [
"heck",
"indexmap",
"log",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn 1.0.109",
"tempfile",
"toml",
]
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "errno"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.45.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]]
name = "half"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "io-lifetimes"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "itoa"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "libc"
version = "0.2.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
[[package]]
name = "linux-raw-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "nix"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e06129fb611568ef4e868c14b326274959aa70ff7776e9d55323531c374945"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "nix"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "nsm-lib"
version = "0.2.1"
dependencies = [
"aws-nitro-enclaves-nsm-api",
"cbindgen",
"serde_bytes",
]
[[package]]
name = "nsm-test"
version = "0.2.1"
dependencies = [
"aws-nitro-enclaves-nsm-api",
"nix 0.20.2",
"nsm-lib",
"serde_bytes",
"serde_cbor",
"signal-hook",
"threadpool",
"vsock",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi 0.2.6",
"libc",
]
[[package]]
name = "proc-macro2"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags",
]
[[package]]
name = "rustix"
version = "0.37.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aef160324be24d31a62147fae491c14d2204a3865c7ca8c3b0d7f7bcb3ea635"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
]
[[package]]
name = "ryu"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "serde"
version = "1.0.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294"
dependencies = [
"serde",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.13",
]
[[package]]
name = "serde_json"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "signal-hook"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix",
"windows-sys 0.45.0",
]
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "vsock"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8e1df0bf1e1b28095c24564d1b90acae64ca69b097ed73896e342fa6649c57"
dependencies = [
"libc",
"nix 0.24.3",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.0",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc 0.48.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"

8
docker/check_hash.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -e
EXPECTED_HASH="$(cat sha256."$(basename "$1")")"
ACTUAL_HASH="$(sha256sum "$1")"
echo "Checking hash for '$1'"
echo "Expected: '$EXPECTED_HASH'"
echo "Actual: '$ACTUAL_HASH'"
exec [ "$EXPECTED_HASH" == "$ACTUAL_HASH" ]

1
docker/ms.sources.list Normal file
View File

@ -0,0 +1 @@
deb [arch=amd64] https://packages.microsoft.com/ubuntu/20.04/prod focal main

1
docker/sgx.sources.list Normal file
View File

@ -0,0 +1 @@
deb [arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu focal main

View File

@ -0,0 +1,29 @@
{
"pccs_url": "https://global.acccache.azure.net/sgx/certification/v3/",
"use_secure_cert": false,
"collateral_service": "https://pccs/sgx/certification/v3/",
"pccs_api_version": "3.1",
"retry_times": 6,
"retry_delay": 5,
"local_pck_url": "http://169.254.169.254/metadata/THIM/sgx/certification/v3/",
"pck_cache_expire_hours": 48,
"custom_request_options" : {
"get_cert" : {
"headers": {
"metadata": "true"
},
"params": {
"api-version": "2021-07-22-preview"
}
}
}
}

View File

@ -0,0 +1 @@
4eaea32f59cde4dc635fbc42161031d13e1c780b87097f4b4234cfce671f1768 go1.20.2.linux-amd64.tar.gz

1
docker/sha256.libnsm.a Normal file
View File

@ -0,0 +1 @@
350d5fde8e139301aaf39a47509aa0fa0c9ced472d4ae30c45c5504b1ef45490 target/release/libnsm.a

View File

@ -0,0 +1 @@
f90d0dd59065fef94374745627336d622702b67f0319f96cee894d41a974d47a protoc-21.8-linux-x86_64.zip

5
docker/sources.list Normal file
View File

@ -0,0 +1,5 @@
deb http://snapshot.debian.org/archive/debian/20220912T000000Z/ bullseye main
deb http://snapshot.debian.org/archive/debian/20220912T000000Z/ bullseye-updates main
deb http://snapshot.debian.org/archive/debian/20220912T000000Z/ buster main
deb http://snapshot.debian.org/archive/debian/20220912T000000Z/ buster-updates main

98
docs/Healing.md Normal file
View File

@ -0,0 +1,98 @@
# Healing
When we talk about "healing" in SVR2, we're currently talking about membership
change in the Raft replica group. In SVR2, we break healing down into
the following sub-problems:
- Remove old nodes when they're unable to serve (rebooted, etc)
- Add new nodes to replace removed nodes
Removing of nodes is currently unimplemented, more on that later.
## Adding new nodes
A new SVR2 node that wants to become a replica within the Raft cluster
currently goes through the following state transitions to get it to a
serving state. These are currently driven by host-side requests, but
in near-future we hope to make the decision to promote replicas to
voting status an in-enclave decision.
In short, a node starts up without any Raft state. It then decides
to follow one of two paths:
- Start a new Raft group as the sole replica/leader.
- Join an existing Raft group by talking to some replica in that group.
Starting a new Raft group is out of scope of this doc: it just does :).
Joining an existing group, though, is the primary mechanism by which new
nodes are added. We assume that we're running in an environment where
broken nodes are replaced (by shutting down the old node and starting up
a new one) as K8S and most other cloud provider workflows allow. In this
case, "adding a new node" is actually "starting a new node, and having
it request to join the group".
Breaking this down in more detail, a node that wants to join a group
goes through a set of state transitions by talking to other nodes:
1. Host tells the enclave about a single peer ID
1. Get information about the Raft group (group ID, other replicas, etc)
1. Replicate existing state (logs/database) up to a recent commit
1. Send a `request_membership` request to the leader
1. Send a `request_vote` request to the leader
These steps are accomplished by calling enclave-to-enclave (e2e)
transactions (protos in `enclave/proto/e2e.proto`).
### Host join request
The host starts the join by sending a `HostToEnclaveRuequst.join_raft` call
to the enclave, with a PeerID it knows about that's part of the existing group.
### Get information about Raft group
The enclave calls the `e2e::TransactionRequest.get_raft` transaction on the one
peer ID it knows about (the one passed in by `join_raft`). This gives it the
`RaftGroupConfig` (immutable Raft configuration) and `raft.ReplicaGroup`
(current membership in the group). It then transitions to the next state.
### Replicate existing state
The enclave picks a random peer from among those in the `ReplicaGroup`
(it will eventually make a more interesting decision about which peer to talk
to), then makes a series of `e2e::TransactionRequest.replicate_state`
requests against that peer. These requests first pull in all logs from
the remote peer until the new node reaches the responder's commit index.
At this point, the new node will start to request and receive a combination
of any new logs committed since that first commit point and database state.
When it's read in the full keyspace of the database (applying as it goes
any newly-committed logs it recieves), it will be at a point where it has
all logs and all database state up to the latest committed index of the
responder. It then transitions to the next state.
### Request join
The enclave then requests to join the group as a non-voting member.
It sends an `e2e::TransactionRequest.request_raft_membership` to the
leader of the group (it actually sends it to all members, but should be
changed in the near future to target just the suspected leader).
If this request succeeds, it is now in a ReplicaGroup config on a
non-committed leader log. The leader will begin to treat it as a normal
non-voting member initially, including replicating to it via AppendEntries
any uncommitted logs and telling it when those logs commits. The node
stays in this state, watching its raft log, until it sees that a
ReplicaGroup log containing its PeerID has been committed. At this point,
it knows that it is now a member, and transitions its local state to
act as such.
### Request vote
This is another mechanism that's currently driven by the host, but should
probably become an automatic enclave function. After an enclave becomes
a non-voting member of the Raft group, the host can send a
`HostToEnclaveRuequst.request_voting` request to the enclave. This
instructs the enclave to send an `e2e::TransactionRequest.request_raft_voting`
call to its current leader. On success, the leader switches the replica's
voting status from non-voting to voting by writing a new ReplicaGroup with
the associated changes to its log. The requesting node (and all other
nodes in the Raft group) hear about this change via normal mechanisms for
ReplicaGroup change.

176
docs/Messages.md Normal file
View File

@ -0,0 +1,176 @@
# Enclave Messages: The Enclave's Logical Interface
The SVR2 enclave interface defined in [svr2.edl](../../shared/svr2.edl) is
generic. It provides initialization and message passing functions that are
independent of the application logic. The _logical_ interface of the enclave
is defined by these messages and how the enclave responds to them. In what
follows we will think of the different messages that can be sent to the enclave
as RPCs and refer to them as "calls" or "commands".
## Three Interfaces: Host, Peer, and Client
SVR2 enclaves interact with three different types of entities: the _host_ that
makes ECALLs and receives OCALLs from the enclave, other _peer_ enclaves, and
_clients_ that are using the service to store and recover secure values.
The host interface includes a number of administrative commands (create or join
a replica group, get enclave status, tick the Raft timer, etc.). It also has
commands to forward wrapped peer or client requests.
The peer interface includes Raft protocol messages, attestation updates, and as
a number of other "Enclave to Enclave [E2E] transactions" used to get
information about a replica group, transfer database state, and join a replica
group.
The client interface is the raison d'être for SVR2. It allows clients to backup,
restore, or delete a secure value. Everything else in this system is here to
ensure that this is done securely and reliably.
We will use this abstraction to organize this document, but it does *not* align
perfectly with the organization of the code. The code organization reflects
important implementation details as follows:
* All messages to the enclave are sent in an `UntrustedMessage`
([shared/proto/msgs.proto](../../shared/proto/msgs.proto)). These
may be direct commands or forwarded messages from peers or clients.
* Host calls that will not trigger response messages are sent as a simple
`UntrustedMessage`.
We will call these _synchronous host calls_.
* Host calls that MAY trigger response messages are sent as a
`HostToEnclaveRequest` inside an `UntrustedMessage`. It is important to note
that *all* client requests are sent this way.
* `HostToEnclaveRequest`s are further subdivided into administrative requests
and requests on behalf of clients. Client requests may be Noise encrypted
(backup, restore, delete) or unencrypted (create new client, create backup).
Encrypted client messages are defined in
([shared/proto/msgs.proto](../../shared/proto/msgs.proto)). Unencrypted ones
are defined as submessages of `HostToEnclaveRequest` in
([shared/proto/msgs.proto](../../shared/proto/msgs.proto)).
* Peer calls are all sent as `PeerMessage` messages inside an
`UntrustedMessage`. These messages contain raw bytes that either hold handshake
information or a Noise encrypted `EnclaveToEnclaveMessage`. These messages
are defined in [enclave/proto/e2e.proto](../proto/e2e.proto)
There is another important property that we will note on all of the calls we
describe: some calls require that a new Raft log entry be accepted and committed
by this node's replica group in order to complete, others do not. We will say that
the calls that succeed or fail based on whether a log entry was successfully
committed "require Raft consensus".
## The Host Interface
### Synchronous Calls
There are two messages the host can send to the enclave that act as
synchronous calls - once the ECALL returns the action is complete. No messages
will be sent from the enclave in response to these calls. None of these require Raft
consensus. They are:
1. `TimerTick` passes a unix timestamp that causes the enclave's to update its
internal time (which is used to obtain a consensus `group_time` with its peers),
then perform a `RaftStep`.
1. `ResetPeer` lets this Raft instance know that the given peer ID
may have lost some of the messages we sent to it previously.
### Asynchronous Calls
All other calls from the host may cause the enclave to send response messages
that must be handled asynchronously. These are all sent as a
`HostToEnclaveRequest` inside an `UntrustedMessage`. These calls include:
1. **Reconfigure** (`enclaveconfig.EnclaveConfig`) Reconfigure the replica with
new host-supplied configuration.
1. **GetEnclaveStatus** (`bool`) Retrieves basic
information about the status of a replica. Has more detail if the
replica is a leader.
1. **DeleteBackup** (`DeleteBackupRequest` - _requires consensus_) Used by host
to delete a backup, e.g., when the account is deleted.
1. **CreateNewRaftGroup** (`RaftConfig`) Request that we create a new raft group
from scratch, setting ourselves as the sole member and leader. This should be
done to seed a new Raft, after which we should requst `JoinRaft` instead.
1. **JoinRaft** (`JoinRaftRequest` - _requires consensus_) This tells the
enclave to join a particular Replica group. This call requires that the
target raft group be up and running. Raft joining is a
multi-step process described in detail in [Healing.md](./Healing.md). In
this process there will be an enclave-to-enclave call that creates a new
Raft configuration. This change must requires consensus of the existing
voting members. If successful the enclave will be a non-voting,
up-to-date member of the specified Raft.
1. **PingPeer** (`EnclavePeer`) Tells an enclave to check connectivity with
another peer.
1. **RequestVoting** (`bool` - _requires consensus_) Tells an enclave that
is already a member of a replica group to request voting status. This
requires a new Raft configuration to be accepted by a majority of the
voting members of the *new* configuration.
1. **RequestMetrics** Get all metrics and gauges collected by the enclave.
1. **RefreshAttestation** Refresh attestations for peer and client connections.
1. **SetLogLevel** Sets the enclave's logging level with an `::svr2::EnclaveLogLevel`
enum. These enum values match Open Enclave's [oe_log_level_t](https://github.com/openenclave/openenclave/blob/master/include/openenclave/log.h).
1. **RelinquishLeadership** (`bool` - _requires consensus_) If we are the Raft
leader, give it up and attempt to pass leadership to an up-to-date peer without
waiting for the election timers.
1. **RequestRemoval** (`bool`- _requires consensus_) Request that this replica be removed from the Raft
group.
1. **Hashes** (`bool`)Compute and return to the host a hash of the current DB.
## The Peer Interface
Peer to peer calls fall into three categories:
1. Raft messages
1. Connectivity messages
1. E2E Transactions
### Raft Messages
The Raft protocol messages defined in [enclave/raft.proto](../proto/raft.proto)
closely follow the Raft protocol defined in
[Ongaro's thesis](https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf).
### Connectivity Messages
These messages are defined in [enclave/proto/e2e.proto](../proto/e2e.proto).
1. **Connect** (`e2e.ConnectRequest`) Sends attestation and handshake
information to initiate a connection with a peer. The response to this
call contains attestation and handshake information for the called
enclave.
1. **AttestationUpdate** (`Attestation`) sends a new attestation to a peer so
that peers can ensure their long-term connection with another enclave is
still secure.
### Enclave to Enclave (E2E) Transactions
These messages are defined in [enclave/proto/e2e.proto](../proto/e2e.proto).
1. **GetRaft** (`e2e.GetRaftRequest`) Gets Raft membership information so that
the enclave can initiate the joining process.
1. **ReplicateState** (`e2e.ReplicateStateRequest`) Requests a chunk of
database state from a peer. This can include log messages and database rows.
1. **ReplicateStatePush** (`e2e.ReplicateStatePush`)
1. **RaftMembershipRequest** (`bool` - _requires consensus_) Request
to become a non-voting member of a replica group by setting this `true`.
Assumes that the calling peer is loaded and up to date.
1. **RaftVotingRequest** (`bool` - _requires consensus_) Request
to become a voting member of a replica group by setting this `true`.
assumes that the calling peer is a non-voting member of the group.
1. **RaftWrite** (`bytes` - _requires consensus_) Forward a log entry to
Raft leader to be added to the log.
1. **Ping** (`bool`) Request from a peer for simple acknowledgement to confirm
the connection to the requesting peer's host.
1. **NewTimestampUnixSecs** (`uint64`) Contains the sending peers timestamp.
Recipient will update peer and group times.
1. **RaftRemovalRequest** (`bool`) Creates a new replica group configuration without
the requesting peer in it, and submits this change to the new voting peers
for committment.
## The Client Interface
The `client.*` messages are defined in
[client.proto](../../shared/proto/client.proto). These are sent over the Noise
encrypted channel between the client and the enclave, wrapped in an
`ExistingClientRequest` submessage of a `HostToEnclaveRequest`.
1. **NewClient** (`NewClientRequest`)
1. **CreateBackup** (`CreateBackupRequest` - _requires consensus_) Creates an
empty backup row in the database.
1. **Backup** (`client.BackupRequest` - _requires consensus_) Stores a new value
and resets the number of allowed tries for a given backup ID.
1. **RestoreBackup** (`client.RestoreRequest` - _requires consensus_) Presents an
authorization token/PIN for a backup ID. If the token is correct, the secure
value is retrieved from the database and sent to the client over the Noise
connection. If it is incorrect the number of allowed tries is decremented.
If no more tries remain, the database row is deleted.
1. **DeleteBackup - client request** (`client.DeleteRequest` - _requires consensus_)

5
docs/svr3spec/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
svr3.aux
svr3.bbl
svr3.blg
svr3.log
svr3.out

11
docs/svr3spec/README.md Normal file
View File

@ -0,0 +1,11 @@
## Building the PDF
To build the pdf from source, you will need to install pdflatex and bibtex - the [TeXLive](https://www.tug.org/texlive/) distribution is probably the simplest way to do this. Alternatively tou can use an online system like [OVerleaf](https://overleaf.com) that will take care of most of the LaTeX related headaches for you.
With these installed, run the following commands:
```
pdflatex svr3.tex # produces initial pdf and computes references needed in svr3.aux
bibtex svr3 # builds bibliography
pdflatex svr3.tex # incorporates bilbiography into the pdf
```
If prompted for input during the last run of pdflatex, press "enter" to continue.

40
docs/svr3spec/svr3.bib Normal file
View File

@ -0,0 +1,40 @@
@misc{jkkx,
author = {Stanislaw Jarecki and Aggelos Kiayias and Hugo Krawczyk and Jiayu Xu},
title = {Highly-Efficient and Composable Password-Protected Secret Sharing (Or: How to Protect Your Bitcoin Wallet Online)},
howpublished = {Cryptology ePrint Archive, Paper 2016/144},
year = {2016},
note = {\url{https://eprint.iacr.org/2016/144}},
url = {https://eprint.iacr.org/2016/144}
}
@misc{poprf,
author = {Nirvan Tyagi and Sofı́a Celi and Thomas Ristenpart and Nick Sullivan and Stefano Tessaro and Christopher A. Wood},
title = {A Fast and Simple Partially Oblivious PRF, with Applications},
howpublished = {Cryptology ePrint Archive, Paper 2021/864},
year = {2021},
note = {\url{https://eprint.iacr.org/2021/864}},
url = {https://eprint.iacr.org/2021/864}
}
@book{bonehshoup,
author = {Dan Boneh and Victor Shoup},
title = {A Graduate Course in Applied Cryptography},
note = {\url{https://toc.cryptobook.us/book.pdf}},
url = {https://toc.cryptobook.us/book.pdf}
}
@misc{2hashdh,
author = {Stanislaw Jarecki and Aggelos Kiayias and Hugo Krawczyk},
title = {Round-Optimal Password-Protected Secret Sharing and T-PAKE in the Password-Only Model},
howpublished = {Cryptology ePrint Archive, Paper 2014/650},
year = {2014},
note = {\url{https://eprint.iacr.org/2014/650}},
url = {https://eprint.iacr.org/2014/650}
}
@misc {ietf-oprf,
author = {A. Davidson, A. Faz-Hernandez, N. Sullivan, C. A. Wood},
title = {Oblivious Pseudorandom Functions (OPRFs) using Prime-Order Groups},
url = {https://www.ietf.org/id/draft-irtf-cfrg-voprf-21.html#name-informative-references-7},
note = {\url{https://www.ietf.org/id/draft-irtf-cfrg-voprf-21.html}}
}

BIN
docs/svr3spec/svr3.pdf Normal file

Binary file not shown.

349
docs/svr3spec/svr3.tex Normal file
View File

@ -0,0 +1,349 @@
\documentclass{article}
% Language setting Replace `english' with e.g. `spanish' to change the document
% language
\usepackage[english]{babel}
% Set page size and margins Replace `letterpaper' with `a4paper' for UK/EU
% standard size
\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry}
% Useful packages
\usepackage{amsmath}
\usepackage{graphicx}
\usepackage[colorlinks=true, allcolors=blue]{hyperref}
\usepackage[ n, % or lambda
advantage, operators, sets, adversary, landau , probability, notions, logic, ff,
mm, primitives, events, complexity, oracles, asymptotics, keys]{cryptocode}
%% Primitives
\newcommand{\OPRF}{\pcalgostyle{OPRF}}
\newcommand{\POPRF}{\pcalgostyle{POPRF}}
\newcommand{\VOPRF}{\pcalgostyle{VOPRF}}
\newcommand{\Blind}{\pcalgostyle{Blind}}
\newcommand{\BlindEvaluate}{\pcalgostyle{BlindEvaluate}}
\newcommand{\BlindEvaluateForClient}{\pcalgostyle{BlindEvaluateForClient}}
\newcommand{\Finalize}{\pcalgostyle{Finalize}}
\newcommand{\PPSSStore}{\pcalgostyle{PPSSStore}}
\newcommand{\PPSSRecover}{\pcalgostyle{PPSSRecover}}
\newcommand{\ServerCreateOPRFVersion}{\pcalgostyle{ServerCreateOPRFVersion}}
%% Hashes
\newcommand{\HashToPoint}{\pcalgostyle{HashToPoint}}
\newcommand{\HashToScalar}{\pcalgostyle{HashToScalar}}
\newcommand{\HashToField}{\pcalgostyle{HashToField}}
\newcommand{\EncodeToField}{\pcalgostyle{EncodeToField}}
%% Variables
\newcommand{\oprfinput}{\pcalgostyle{oprf\_input}}
\newcommand{\oprfkeys}{\pcalgostyle{oprf\_keys}}
\newcommand{\usage}{\pcalgostyle{usage}}
\newcommand{\usagecount}{\pcalgostyle{usage\_count}}
\newcommand{\maxuses}{\pcalgostyle{max\_uses}}
\newcommand{\blind}{\pcalgostyle{blind}}
\newcommand{\blindedElement}{\pcalgostyle{blindedElement}}
\newcommand{\evaluatedElement}{\pcalgostyle{evaluatedElement}}
\newcommand{\clientstate}{\pcalgostyle{client\_state}}
\newcommand{\serverstate}{\pcalgostyle{server\_state}}
\newcommand{\clientid}{\pcalgostyle{client\_id}}
\newcommand{\client}{\pcalgostyle{client}}
\newcommand{\server}{\pcalgostyle{server}}
\newcommand{\servers}{\pcalgostyle{servers}}
\newcommand{\name}{\pcalgostyle{name}}
\newcommand{\context}{\pcalgostyle{context}}
\newcommand{\return}{\ensuremath{\mathbf{return}\ }}
\title{DRAFT Guess Limited Password Protected Secret Sharing Proposal}
\author{Rolfe Schmidt}
\begin{document}
\maketitle
\section{Overview}
This is a protocol for {\em guess-limited password-based secure value recovery}.
It allows clients to interact with servers to securely reconstruct a secret
using a password while providing protection against both offline and online
dictionary attacks - even in the event of server compromise. It protects against
online dictionary attacks through guess limiting: after a configured number of
failed reconstruction attempts, the secure value becomes unrecoverable.
\subsection{Outline}
This protocol is a variation of the PPSS protocol of \cite{jkkx} implemented
with a {\em usage limited} version of the standards track 2HashDH $\OPRF$ of
\cite{2hashdh} as specified in \cite{ietf-oprf} that can be used safely with
smaller curves like Ristretto255.
After covering notation in section \ref{sec:notation} we present an augmentation
of the standards track $\OPRF$ of \cite{ietf-oprf} in section \ref{sec:oprf}
that has servers generate per-client $\OPRF$ keys, enforces strict usage limits
on these keys, and allows clients to rotate their keys to avoid running into
usage limits.
In section \ref{sec:ppss} we use this usage limited $\OPRF$ to construct a
secure PPSS. This protocol is close to that of \cite{jkkx}, but does not mandate
storage of masked shares on servers and eliminates the share commitment storage
on servers. We discuss ways to obtain robustness in section \ref{sec:robustness}.
Importantly, we observe that if the underlying $\OPRF$ limits clients to
$\maxuses$ per key, then against a $(t,N)$ threshold scheme an attacker will be
limited to $\lfloor \frac{N}{t+1}\maxuses\rfloor$ password guess attempts before
the secret becomes unrecoverable. Thus our PPSS is {\em guess limited}. We also
show how keys can be deleted from the server to offer a form of forward security
in case of server compromise.
\section{Notation}
\label{sec:notation}
{\bf Algebraic objects.} This protocol will use a prime order cyclic group,
$\GG$ among those specified in \cite{ietf-oprf}. Since key use will be limited,
we can take $\GG$ to be Ristretto255. We
denote the order of $\GG$ by $q$ and thus denote the set of scalars for $\GG$ by
$\ZZ_{q}$. Group elements will be denoted by capital Latin letters, e.g. $A, B,
C, \ldots$. Scalars will be denoted by lower case Latin letters, e.g. $a, b,
c,\ldots$. $G$ denotes a public generator of $\GG$. Scalar multiplication will
usually be denoted without a symbol - $aG$ - but in places the infix operator
$*$ will be used for clarity, as in $\sk_{oprf}*G$.
Secret sharing will be performed using polynomials over a finite field, $\FF$,
that is not related to the group $\GG$.
{\bf Domain separation.} Throughout the protocol we will use $\context$ to
denote a domain separation prefix unique to the application performing the
protocol.
{\bf Server and client state.} Each server will have state information captured
in the variable \serverstate. The public part of this state is available in the
variable \server.
Similarly, each client will have persistent information captured in the variable
\clientstate, and the public part of this state will be accessible through the
variable \client.
{\bf Function parameters.} We will use a number of functions associated with
$\GG$ and $\FF$ which we consider as protocol parameters. In an instantiation of
the protocol the parameters will be identified in the \context string. The
function parameters are:
\begin{itemize}
\item All parameters for the $\OPRF$ specified in \cite{ietf-oprf}
\item $\HashToField: \bin^{*} \rightarrow \FF$
\end{itemize}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% OPRF
%%
\section{ The $\OPRF$}
\label{sec:oprf}
The PPSS protocol relies on the verifiable 2HashDH $\OPRF$ of \cite{2hashdh} as
specified in the IRTF draft standard \cite{ietf-oprf}. We describe the protocol
using the $\OPRF$ mode of the standard but in section \ref{sec:robustness} note
situations where follow up calls to the $\VOPRF$ mode can be used to ensure
robustness.
The IRTF standard specifies the following functions:
\begin{enumerate}
\item $(\blind, \blindedElement) \leftarrow \Blind(\oprfinput)$: Used by a
client to prepare the $\OPRF$ input to be sent to the server.
\item $\evaluatedElement \leftarrow \BlindEvaluate(\sk,
\blindedElement)$: Executed by the server to evaluate the $\OPRF$
parameterized by the server-secret key $\sk$ on $\blindedElement$ to compute
a blinded output.
\item $v \leftarrow \Finalize(\oprfinput, \blind, \evaluatedElement,
\blindedElement)$: Takes the values returned by $\BlindEvaluate$
along with original input, and values returned by $\Blind$ to compute the
final $\prf$ value, $v$.
\end{enumerate}
\subsection{Usage Limited Evaluation}
The server for our protocol adds to these functions in two ways: it uses a
random per-client $\OPRF$ key stored in the dictionary $\serverstate.\oprfkeys$
and it enforces a usage limit. Each $\OPRF$ key can only be used a fixed number
of times. Setting and rotation of these keys is discussed in
\ref{sec:versioning}. This is done with the function $\BlindEvaluateForClient$:
\procedureblock[linenumbering]{$\BlindEvaluateForClient(\serverstate, \clientid,
\blindedElement)$}{ \usagecount \leftarrow \serverstate.\usagecount[\clientid]
\\
\serverstate.\usagecount[\clientid] \leftarrow \usagecount + 1 \\
\pcif \usagecount \geq \serverstate.\maxuses: \\
\t \pcreturn \perp \\
(\sk, \pk) \leftarrow \serverstate.\oprfkeys[\clientid] \pccomment{The client
MAY obtain the \ensuremath{\pk} corresponding to \ensuremath{\sk} at
registration} \\
\pcreturn \BlindEvaluate(\sk, \blindedElement) }
\subsection{$\OPRF$ Key Creation and Versioning}
\label{sec:versioning}
As noted in the previous section, $\OPRF$ keys are created per-client and each
key is strictly limited to a fixed number of uses. The usage limitation has two
useful purposes. First, since the security of the $\OPRF$ is based on the
one-more Diffie Hellman assumption, the security of a key used for $Q$ queries
is reduced by $\log(Q)/2$ bits (see, e.g.,
\href{https://www.ietf.org/id/draft-irtf-cfrg-voprf-21.html#section-7.2.3}{7.2.3
of the IRTF draft}). So, for example, by limiting key usage to no more than 16
queries we only lose 2 bits of security and can safely use a group like
Ristretto255. Second, this limit enforcement will be the basis of the guess
limiting in the PPSS described in section \ref{sec:ppss}.
Clients in our PPSS will need to reconstruct their secret an unlimited number of
times, though. To do this, upon successful reconstruction the client will create
a new version of their $\OPRF$ key. This new version will be constructed
with the function $\ServerCreateOPRFVersion$, which creates a new key pair,
stores it indexed by the client's identifier, clears the usage count, and
evaluates the $\OPRF$ with the new key on a blinded element:
\procedureblock[linenumbering]{$\ServerCreateOPRFVersion(\serverstate,
\clientid, \blindedElement)$}{ \sk \sample \ZZ_q \\
\pk \leftarrow kG \\
\serverstate.\oprfkeys[\clientid] \leftarrow (\sk, \pk)\\
\serverstate.\usagecount[\clientid] \leftarrow 0 \\
\evaluatedElement \leftarrow \BlindEvaluate(\sk, \blindedElement) \\
\pcreturn (\evaluatedElement, \pk) \pccomment{Return the public key in case \nizk\ proof is needed later}
}
\subsection{A Note About $\POPRF$ Mode}
It is tempting to use the $\POPRF$ mode introduced in \cite{poprf} rather than
generating client specific keys. If usage limitation were not a requirement this
would have a clear advantage - the server state would be no more than one secret
$\OPRF$ scalar. However, once we introduce the need for key usage limits and
key rotation this advantage disappears. Usage limitation requires storage of
per-client state. Key rotation requires the use of a nonce or a counter for each
client, effectively requiring the same storage as the proposed per-client key
solution.
\section{A Guess Limited PPSS from the \OPRF}
\label{sec:ppss}
With these primitives in place we define the PPSS scheme with the functions
$\PPSSStore$ and $\PPSSRecover$. The idea is simple. To create a $(t,N)$
threshold PPSS to store a secret $s$ with $N$ servers we
\begin{enumerate}
\item Create a degree $t$ polynomial $f\in\FF[x]$ with $s$ as the leading
coefficient, all other coefficients random.
\item Create a share for each server: $s_i = f(x_i)$ where $x_i =
\HashToField(\server_{i}.id)$
\item Use the $\OPRF$ values to mask the shares: $m_i = s_i +
\server_i.\OPRF(\clientid, pwd)$.
\item Store the values $m_i$ somewhere reliable, but confidentiality is not
important.
\item To reconstruct, simply call $(t+1)$ or more servers to get their
$\OPRF$ values and use these to unmask the shares: $s_i = m_i -
\server_i.\OPRF(\clientid, pwd)$.
\item These shares can now be used to reconstruct the secret $s$.
\item Upon successful reconstruction the client can create new key versions
on all servers, refresh their guess counts, and create new masked shares.
All of this can be done without changing the password or master secret.
\end{enumerate}
In the following $\servers$ is a set of $N$ $\server$ objects, $\mathbf{e}$
is a dictionary that will store masked shares of a secret $s$, and $\mathbf{pks}$
is a dictionary that stores server $\OPRF$ public keys.
\procedureblock[linenumbering]{$\PPSSStore(\clientstate, \servers, t, pwd, s)$}{
r \concat K \leftarrow \hash(\context \concat ``keygen", s) \\
\forall i \in [0,t-1] : f_i \sample \FF \\
f_{t} \leftarrow \EncodeToField(s) \\
\pcfor \server \in \servers: \\
\t \oprfinput \leftarrow \context \concat \server.id \concat pwd \\
\t x \leftarrow \HashToField(\server.id) \\
\t y \leftarrow \sum_{i=0}^{t} f_i x^i \\
\t (\blind, \blindedElement) \leftarrow \Blind(\oprfinput) \\
\t (\evaluatedElement, \pk) \leftarrow
\server.\ServerCreateOPRFVersion(\clientstate.id, \blindedElement) \\
\t \rho \leftarrow \Finalize(\oprfinput, \blind, \evaluatedElement,\blindedElement) \\
\pccomment{ \ensuremath{\mathbf{e}} and \ensuremath{\mathbf{pks}} should be
stored somewhere reliable, but confidentiality is not needed} \\
\t \mathbf{s}[x] \leftarrow y \\
\t \clientstate.\mathbf{e}[x] \leftarrow y + \rho \\
\t \clientstate.\mathbf{pks}[\server.id] \leftarrow \pk \\
\clientstate.C \leftarrow \hash(\context\concat ``commitment", pwd, \clientstate.\mathbf{e}, \mathbf{s}, r) \\
\pcreturn K
}
\procedureblock[linenumbering]{$\PPSSRecover(\clientstate, \servers, t, pwd)$}{
\text{Choose } \mathcal{R} \subset \servers, |\mathcal{R}| > t \\
pairs \leftarrow \{\} \\
\pcfor \server \in \mathcal{R}: \\
\t \oprfinput \leftarrow \context \concat \server.id \concat pwd \\
\t x \leftarrow \HashToField(\server.id) \\
\t (\blind, \blindedElement) \leftarrow \Blind(\oprfinput) \\
\t (\evaluatedElement, \pk) \leftarrow
\server.\BlindEvaluateForClient(\clientstate.id, \blindedElement) \\
\t r \leftarrow \Finalize(\oprfinput, \blind, \evaluatedElement,\blindedElement) \\
\t y \leftarrow \clientstate.\mathbf{m}[x] - r \\
\t \mathbf{s}[x] \leftarrow y \\
\t pairs \leftarrow pairs \cup \{(x,y)\} \\
(f_{t}, \ldots, f_0) \leftarrow \pcalgostyle{Interpolate}_{\FF}(pairs) \\
s \leftarrow f_{t} \\
r \concat K \leftarrow \hash(\context \concat ``keygen", s) \\
C \leftarrow \hash(\context\concat ``commitment", pwd, \clientstate.\mathbf{e}, \mathbf{s}, r) \\
\pcif C \neq \clientstate.C: \\
\t \pcreturn \perp \\
\pcelse \\
\t \PPSSStore(\clientstate, servers,t,pwd, f_{t}) \pccomment{store the secret again
to reset keys, counters, and shares} \\
\t \pcreturn K }
\subsection{Usage Limits on the $\OPRF$ lead to Guess Limits on the PPSS}
Now we can see how the usage limit we enforce on the $\OPRF$ naturally creates a
guess limit on the PPSS that provides protection against online dictionary
attacks. Consider the scenario where a client has constructed a $(t,N)$-sharing
scheme to protect a secret $s$ with password $pwd$ using $\PPSSStore$. Now an
attacker trying to guess the password and recover the secret faces the following
fact: each password guess requires using $t+1$ $\OPRF$ calls, and only
$\maxuses$ are possible on each of the $N$ servers. Thus the attacker has no
more than $\lfloor\frac{N}{t+1}\maxuses\rfloor$ guesses before the secret
becomes unrecoverable.
\subsection{Deleting Keys}
\label{sec:deleting}
A client can protect themselves from future server compromise by deleting keys
from the server. This can be done by simply calling $\ServerCreateOPRFVersion$
with arbitrary $\oprfinput$ and discarding the result. For the user to have
confidence that the keys were in fact deleted - now and during each $\PPSSStore$
call - server functions can be executed in an attested, confidential TEE.
Additionally, in the case that a TEE based server is being retired it can
produce an attested certificate of secret deletion. If a client has confidence
that their secrets have, in fact, been deleted from a server then they know that
their $(t,N)$ threshold scheme has become a $(t,N-1)$ scheme and they can safely
add a new server.
\section{Robustness}
\label{sec:robustness}
Unlike \cite{jkkx} we do not store the commitment, $C$, and the masked shares,
$\mathbf{e}$, on each server. Instead we will have clients store $\clientstate$,
which includes both both these values, in a reliable place as suggested in \cite{2hashdh}.
We then rely on subset testing or follow-up $\VOPRF$ calls to
detect incorrect servers.
The sole reason the server $\pk$ values are stored by the client in the protocol
above is to allow follow-up $\nizk$ proof verification if the $\VOPRF$ mode
is used for robustness. If only subset testing will be used (e.g. for small
values of $N$) then these public keys do not need to be stored.
\section{Acknowledgements}
We would like to thank Mark Johnson for helping to develop an earlier version of
this protocol and Trevor Perrin for important feedback and pointers to the
literature. We also thank Emma Dautermann, Vivian Fang, and Raluca Ada Popa for
discussion that led to significant design decisions and simplifications of this
protocol.
\bibliographystyle{alpha}
\bibliography{svr3}
\end{document}

4
enclave/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
build/
.testdepends
*.pem
noise-c/

232
enclave/Makefile Normal file
View File

@ -0,0 +1,232 @@
all: test build sign
MAKEFILTER=| (grep --line-buffered -v '^make\[' || true)
include Makefile.base
.PHONY: all build sign clean sign protos validate generatem
build: build/enclave.bin build/enclave.nsm
sign: build/enclave.signed build/enclave.test build/enclave.small
PROTO_FILES= \
$(patsubst ../shared/proto/%.proto,build/proto/%.pb.cc,$(wildcard ../shared/proto/*.proto)) \
$(patsubst ../shared/proto/%.proto,build/proto/%.pb.h,$(wildcard ../shared/proto/*.proto)) \
$(patsubst proto/%.proto,build/proto/%.pb.cc,$(wildcard proto/*.proto)) \
$(patsubst proto/%.proto,build/proto/%.pb.h,$(wildcard proto/*.proto)) \
## PROTO_FILES
protos: $(PROTO_FILES)
build/proto:
$(QUIET) echo -e "MKDIR $@"
$(QUIET) mkdir -p $@
build/proto/%.pb.h build/proto/%.pb.cc: proto/%.proto | build/proto
$(QUIET) echo -e "PROTO\t$^"
$(QUIET) protoc --proto_path=../shared/proto --proto_path=proto --cpp_out=build/proto $^
build/proto/%.pb.h build/proto/%.pb.cc: ../shared/proto/%.proto | build/proto
$(QUIET) echo -e "PROTO\t$^"
$(QUIET) protoc --proto_path=../shared/proto --cpp_out=build/proto $^
build/gtest/TEST.a:
$(QUIET) $(MAKE) -f Makefile.subdir DIR=gtest ENV=TEST ADDITIONAL_CFLAGS="-I$(CURDIR)/googletest/googletest" $(MAKEFILTER)
build/noise-c/TEST.a: build/libsodium/TEST.a
$(QUIET) echo -e "BUILD\t$@"
$(QUIET) mkdir -p $(@D)
$(QUIET) (cd noise-c && \
./autogen.sh && \
libsodium_CFLAGS=-I$$PWD/../build/libsodium/TEST.a.dir/include/ libsodium_LIBS=$$PWD/../build/libsodium/TEST.a \
CC=$(CC) CFLAGS="$(TEST_CFLAGS) -I$(shell ./find_header.sh $(CC) immintrin.h)" ./configure --with-libsodium && \
$(MAKE) clean && \
$(MAKE)) $(QUIET_OUT)
$(QUIET) cp noise-c/src/protocol/libnoiseprotocol.a $@
$(QUIET) echo -e "BUILT\t$@"
build/noise-c/SGX.a: build/libsodium/SGX.a | build/noise-c/TEST.a
$(QUIET) echo -e "BUILD\t$@"
$(QUIET) mkdir -p $(@D)
$(QUIET) (cd noise-c && \
./autogen.sh && \
libsodium_CFLAGS=-I$$PWD/../build/libsodium/SGX.a.dir/include/ libsodium_LIBS=$$PWD/../build/libsodium/SGX.a \
CC=$(CC) CFLAGS="$(SGX_CFLAGS) -I$(shell ./find_header.sh $(CC) immintrin.h)" ./configure --with-libsodium && \
$(MAKE) clean && \
$(MAKE)) $(QUIET_OUT)
$(QUIET) cp noise-c/src/protocol/libnoiseprotocol.a $@
$(QUIET) echo -e "BUILT\t$@"
build/noise-c/NSM.a: build/libsodium/NSM.a | build/noise-c/SGX.a
$(QUIET) echo -e "BUILD\t$@"
$(QUIET) mkdir -p $(@D)
$(QUIET) (cd noise-c && \
./autogen.sh && \
libsodium_CFLAGS=-I$$PWD/../build/libsodium/NSM.a.dir/include/ libsodium_LIBS=$$PWD/../build/libsodium/NSM.a \
CC=$(CC) CFLAGS="$(NSM_CFLAGS) -I$(shell ./find_header.sh $(CC) immintrin.h)" ./configure --with-libsodium && \
$(MAKE) clean && \
$(MAKE)) $(QUIET_OUT)
$(QUIET) cp noise-c/src/protocol/libnoiseprotocol.a $@
$(QUIET) echo -e "BUILT\t$@"
# libsodium's ./configure script incorrectly detects that mmap, mlock, madvise, mprotect,
# and raise are all available, when in fact they are not in the enclave. This set of flags
# allows us to undo that.
LIBSODIUM_UNDEFS=-UHAVE_MMAP -UHAVE_MLOCK -UHAVE_MADVISE -UHAVE_MPROTECT -UHAVE_RAISE
##LIBSODIUM_UNDEFS
build/libsodium/TEST.a:
$(QUIET) echo -e "BUILD\t$@"
$(QUIET) mkdir -p $@.dir $(@D)
$(QUIET) (cd libsodium && (git clean -fx || true) && ./configure \
CFLAGS="$(TEST_CFLAGS)" \
CXXFLAGS="$(TEST_CXXFLAGS)" \
CC=$(CC) CXX=$(CXX) --prefix=$$PWD/../$@.dir && $(MAKE) clean && $(MAKE) install) $(QUIET_OUT)
$(QUIET) ln -s $$PWD/$@.dir/lib/libsodium.a $@
$(QUIET) echo -e "BUILT\t$@"
build/libsodium/SGX.a: | build/libsodium/TEST.a
$(QUIET) echo -e "BUILD\t$@"
$(QUIET) mkdir -p $@.dir $(@D)
$(QUIET) (cd libsodium && (git clean -fx || true) && ./configure \
CFLAGS="$(SGX_CFLAGS) $(LIBSODIUM_UNDEFS)" \
CXXFLAGS="$(SGX_CXXFLAGS) $(LIBSODIUM_UNDEFS)" \
CC=$(CC) CXX=$(CXX) --prefix=$$PWD/../$@.dir && $(MAKE) clean && $(MAKE) install) $(QUIET_OUT)
$(QUIET) ln -s $$PWD/$@.dir/lib/libsodium.a $@
$(QUIET) echo -e "BUILT\t$@"
build/libsodium/NSM.a: | build/libsodium/SGX.a
$(QUIET) echo -e "BUILD\t$@"
$(QUIET) mkdir -p $@.dir $(@D)
$(QUIET) (cd libsodium && (git clean -fx || true) && ./configure \
CFLAGS="$(NSM_CFLAGS) $(LIBSODIUM_UNDEFS)" \
CXXFLAGS="$(NSM_CXXFLAGS) $(LIBSODIUM_UNDEFS)" \
CC=$(CC) CXX=$(CXX) --prefix=$$PWD/../$@.dir && $(MAKE) clean && $(MAKE) install) $(QUIET_OUT)
$(QUIET) ln -s $$PWD/$@.dir/lib/libsodium.a $@
$(QUIET) echo -e "BUILT\t$@"
EDGER8R_FILES=build/svr2/svr2_t.h build/svr2/svr2_t.c build/svr2/svr2_args.h
# This $(firstword) trick allows for grouped targets.
$(filter-out $(firstword $(EDGER8R_FILES)),$(EDGER8R_FILES)): $(firstword $(EDGER8R_FILES))
$(firstword $(EDGER8R_FILES)): ../shared/svr2.edl
$(QUIET) echo -e "EDGER8\t$(EDGER8R_FILES)"
$(QUIET) mkdir -p $(@D)
$(QUIET) $(OE_EDGER8R) $< --trusted \
--trusted-dir build/svr2 \
--search-path $(OE_INCDIR) \
--search-path $(OE_INCDIR)/openenclave/edl/sgx $(QUIET_OUT)
generated: $(EDGER8R_FILES) $(PROTO_FILES)
build/%/SGX.a: generated
$(QUIET) $(MAKE) -f Makefile.subdir DIR=$* ENV=SGX $(MAKEFILTER)
build/%/NSM.a: generated
$(QUIET) $(MAKE) -f Makefile.subdir DIR=$* ENV=NSM $(MAKEFILTER)
build/%/TEST.a: generated
$(QUIET) $(MAKE) -f Makefile.subdir DIR=$* ENV=TEST $(MAKEFILTER)
build/%/HOST.a: generated
$(QUIET) $(MAKE) -f Makefile.subdir DIR=$* ENV=HOST $(MAKEFILTER)
.PHONY: build/%/SGX.a build/%/TEST.a build/%/HOST.a build/%/NSM.a
# All libraries which will become part of enclave.bin. If A depends on B, then A should be added before B.
SGX_LIBRARIES = \
svr2 \
ecalls \
core \
timeout \
client \
db \
raft \
groupclock \
peers \
peerid \
sender \
util \
context \
hmac \
noise \
noise-c \
noisewrap \
env \
env/sgx \
sip \
attestation \
metrics \
proto \
protobuf-lite \
libsodium \
## SGX_LIBRARIES
build/enclave.bin: $(patsubst %,build/%/SGX.a,$(SGX_LIBRARIES))
$(QUIET) echo -e "BUILD\t$@"
$(QUIET) $(CXX) -o $@ $(SGX_LDFLAGS) $^ $(SGX_LDFLAGS)
build/enclave.signed: build/enclave.bin build/public.pem build/private.pem svr2.conf
$(QUIET) echo -e "SIGN\t$@"
$(QUIET) $(OE_DIR)/bin/oesign sign -e $< -c svr2.conf -k build/private.pem -o $@ $(QUIET_OUT)
build/enclave.small: build/enclave.bin build/public.pem build/private.pem svr2_small.conf
$(QUIET) echo -e "SIGN\t$@"
$(QUIET) $(OE_DIR)/bin/oesign sign -e $< -c svr2_small.conf -k build/private.pem -o $@ $(QUIET_OUT)
build/enclave.test: build/enclave.bin build/public.pem build/private.pem svr2_test.conf
$(QUIET) echo -e "SIGN\t$@"
$(QUIET) $(OE_DIR)/bin/oesign sign -e $< -c svr2_test.conf -k build/private.pem -o $@ $(QUIET_OUT)
NSM_LIBRARIES = \
nitromain \
core \
timeout \
client \
db \
raft \
groupclock \
peers \
peerid \
sender \
util \
hmac \
noise \
noise-c \
noisewrap \
env \
env/nsm \
sip \
socketwrap \
context \
metrics \
proto \
protobuf-lite \
libsodium \
## NSM_LIBRARIES
build/enclave.nsm: $(patsubst %,build/%/NSM.a,$(NSM_LIBRARIES))
$(QUIET) echo -e "BUILD\t$@"
$(QUIET) $(CXX) -o $@ $(NSM_LDFLAGS) $^ $(NSM_LDFLAGS)
clean:
$(QUIET) (cd protobuf ; make clean ; git clean -fx ; true) $(QUIET_OUT)
$(QUIET) (cd noise-c ; make clean ; git clean -fx ; true) $(QUIET_OUT)
$(QUIET) (cd SipHash ; make clean ; git clean -fx ; true) $(QUIET_OUT)
$(QUIET) rm -vfr build $(QUIET_OUT)
$(QUIET) rm -vf .testdepends $(QUIET_OUT)
build/private.pem:
$(QUIET) echo -e "KEY\t$@"
$(QUIET) mkdir -p $(@D)
$(QUIET) openssl genrsa -out $@ -3 3072 $(QUIET_OUT)
build/public.pem: build/private.pem
$(QUIET) echo -e "KEY\t$@"
$(QUIET) openssl rsa -in $< -pubout -out $@ $(QUIET_OUT)
%.test.out: %.test
$(QUIET) echo -e "TEST\t$<"
$(QUIET) ./$^ --gtest_color=yes &>$@ || (cat $@; false)
$(QUIET) echo -e "TEST\xE2\x9c\x85\t$<"
build/testhost/libsvr2.a:
$(QUIET) mkdir -p $(@D)
$(CC) -c -o build/testhost/svr2.o $(HOST_CFLAGS) ../host/enclave/c/svr2_u.c
ar rcs $@ build/testhost/svr2.o
build/testhost.bin: testhost/testhost.cc build/testhost/libsvr2.a build/attestation/HOST.a build/metrics/HOST.a build/proto/HOST.a build/protobuf-lite/HOST.a
$(CXX) -o $@ $(HOST_CXXFLAGS) $(HOST_LDFLAGS) $^ $(HOST_LDFLAGS)
.testdepends: $(shell find ./ -type f | grep /tests/ | grep cc$)
$(QUIET) ./test_deps.sh $(QUIET_OUT)
include .testdepends
test:
validate:

4
enclave/Makefile.HOST Normal file
View File

@ -0,0 +1,4 @@
include Makefile.base
CFLAGS ?= $(HOST_CFLAGS)
CXXFLAGS ?= $(HOST_CXXFLAGS)

4
enclave/Makefile.NSM Normal file
View File

@ -0,0 +1,4 @@
include Makefile.base
CFLAGS ?= $(NSM_CFLAGS)
CXXFLAGS ?= $(NSM_CXXFLAGS)

4
enclave/Makefile.SGX Normal file
View File

@ -0,0 +1,4 @@
include Makefile.base
CFLAGS ?= $(SGX_CFLAGS)
CXXFLAGS ?= $(SGX_CXXFLAGS)

4
enclave/Makefile.TEST Normal file
View File

@ -0,0 +1,4 @@
include Makefile.base
CFLAGS ?= $(TEST_CFLAGS)
CXXFLAGS ?= $(TEST_CXXFLAGS)

164
enclave/Makefile.base Normal file
View File

@ -0,0 +1,164 @@
SHELL=/bin/bash -o pipefail # needed for pipefail
CXX=clang++-11
CC=clang-11
OE_DIR ?= /opt/openenclave
OE_EDGER8R = $(OE_DIR)/bin/oeedger8r
ADDITIONAL_CFLAGS ?=
ifeq ($(V),)
QUIET=@
QUIET_OUT=&>/dev/null
else
QUIET=
QUIET_OUT=
endif
SECURITY_CFLAGS = \
-fstack-protector-strong \
-fstack-clash-protection \
-mshstk \
-D_FORTIFY_SOURCE=3 \
-fsanitize=bounds \
-fsanitize-undefined-trap-on-error \
## SECURITY_CFLAGS
BASE_CFLAGS = \
-fPIC \
-iquote $(CURDIR) \
-iquote $(CURDIR)/build \
-g \
-DOE_API_VERSION=2 \
-Wthread-safety \
-O2 \
$(SECURITY_CFLAGS) \
$(ADDITIONAL_CFLAGS) \
## BASE_CFLAGS
BASE_CXXFLAGS = \
$(BASE_CFLAGS) \
-std=c++17 \
## BASE_CXXFLAGS
BASE_LDFLAGS = \
-Wl,-wrap=noise_rand_bytes \
-Wl,-z,relro \
-Wl,-z,now \
-Wl,-z,noexecstack \
-Wl,-z,separate-code \
## BASE_LDFLAGS
LIBRARY_CFLAGS = \
-I$(CURDIR)/protobuf/src \
-I$(CURDIR)/noise-c/include \
-I$(CURDIR)/googletest/googletest/include \
-I$(CURDIR)/libsodium/src/libsodium/include \
## LIBRARY_CFLAGS
TEST_CFLAGS ?= \
$(BASE_CFLAGS) \
$(LIBRARY_CFLAGS) \
-DIS_TEST \
## TEST_CFLAGS
TEST_CXXFLAGS ?= \
$(BASE_CXXFLAGS) \
$(LIBRARY_CFLAGS) \
-DIS_TEST \
## TEST_CXXFLAGS
TEST_LDFLAGS ?= \
$(BASE_LDFLAGS) \
-lpthread \
## TEST_LDFLAGS
OE_CFLAGS ?= $(shell pkg-config oeenclave-clang --cflags)
SGX_CFLAGS ?= \
$(BASE_CFLAGS) \
$(OE_CFLAGS) \
$(LIBRARY_CFLAGS) \
## SGX_CFLAGS
OE_CXXFLAGS ?= $(shell pkg-config oeenclave-clang++ --cflags)
SGX_CXXFLAGS ?= \
$(BASE_CXXFLAGS) \
$(OE_CXXFLAGS) \
$(LIBRARY_CFLAGS) \
## SGX_CXXFLAGS
OE_LDFLAGS ?= $(shell pkg-config oeenclave-clang++ --libs)
OE_MBEDTLS_LDFLAGS ?= $(shell pkg-config oeenclave-clang++ --variable=mbedtlslibs)
SGX_LDFLAGS ?= \
$(BASE_LDFLAGS) \
$(OE_LDFLAGS) \
$(OE_MBEDTLS_LDFLAGS) \
## SGX_LDFLAGS
NSM_CFLAGS ?= \
$(BASE_CFLAGS) \
$(LIBRARY_CFLAGS) \
-I/opt/nsm \
-mllvm -x86-speculative-load-hardening \
## NSM_CFLAGS
NSM_CXXFLAGS ?= \
$(BASE_CXXFLAGS) \
$(LIBRARY_CFLAGS) \
-I/opt/nsm \
## NSM_CXXFLAGS
NSM_LDFLAGS ?= \
$(BASE_LDFLAGS) \
/opt/nsm/libnsm.a \
-lpthread \
-lrt \
-ldl \
## NSM_LDFLAGS
OE_INCDIR = $(shell pkg-config oeenclave-clang++ --variable=includedir)
OE_HOST_CFLAGS ?= $(shell pkg-config oehost-clang --cflags)
OE_HOST_CXXFLAGS ?= $(shell pkg-config oehost-clang++ --cflags)
OE_HOST_LDFLAGS ?= $(shell pkg-config oehost-clang++ --libs)
OE_HOST_MBEDTLS_LDFLAGS ?= $(shell pkg-config oehost-clang++ --variable=mbedtlslibs)
HOST_CFLAGS ?= \
$(BASE_CFLAGS) \
$(OE_HOST_CFLAGS) \
$(LIBRARY_CFLAGS) \
## HOST_CFLAGS
HOST_CXXFLAGS ?= \
$(BASE_CXXFLAGS) \
$(OE_HOST_CXXFLAGS) \
$(LIBRARY_CFLAGS) \
## HOST_CXXFLAGS
HOST_LDFLAGS ?= \
$(BASE_LDFLAGS) \
$(OE_HOST_LDFLAGS) \
$(OE_HOST_MBEDTLS_LDFLAGS) \
## HOST_LDFLAGS
WARNING_CFLAGS ?= \
-Werror \
-Wall \
-Wextra \
-Wpedantic \
-Walloca \
-Wcast-qual \
-Wformat=2 \
-Wformat-security \
-Wnull-dereference \
-Wstack-protector \
-Wvla \
-Warray-bounds \
-Warray-bounds-pointer-arithmetic \
-Wassign-enum \
-Wbad-function-cast \
-Wfloat-equal \
-Wformat-type-confusion \
-Widiomatic-parentheses \
-Wimplicit-fallthrough \
-Wloop-analysis \
-Wpointer-arith \
-Wshift-sign-overflow \
-Wtautological-constant-in-range-compare \
-Wunreachable-code-aggressive \
-Wthread-safety \
-Wthread-safety-beta \
-Wcomma \
-Wno-unused-parameter \
-Wno-bitwise-op-parentheses \
-Wno-shift-op-parentheses \
-Wno-c++20-designator \
-Wno-zero-length-array \
-Wno-c99-extensions \
##WARNING_CFLAGS

41
enclave/Makefile.subdir Normal file
View File

@ -0,0 +1,41 @@
include Makefile.$(ENV)
all:
.PHONY: all
BUILD = build/$(DIR)
$(BUILD):
$(QUIET) echo -e "MKDIR\t$@"
$(QUIET) mkdir -p $(BUILD)
# We use WARNING_CFLAGS only when the file exists, is not a symlink,
# and isn't generated code (IE: it's not in build/...)
NO_WARNINGS=-w
OBJECTS=$(patsubst %,build/%.$(ENV).o,$(wildcard $(DIR)/*.c) $(wildcard $(DIR)/*.cc)) $(patsubst %,%.$(ENV).o,$(wildcard $(BUILD)/*.c) $(wildcard $(BUILD)/*.cc))
$(BUILD)/%.cc.$(ENV).d $(BUILD)/%.cc.$(ENV).o: $(BUILD)/%.cc | $(BUILD)
$(QUIET) echo -e "CXX\t$<.$(ENV)"
$(QUIET) $(CXX) -c -o $(BUILD)/$*.cc.$(ENV).o $(CXXFLAGS) -MF $(BUILD)/$*.cc.$(ENV).d -MMD $< $(NO_WARNINGS)
$(BUILD)/%.c.$(ENV).d $(BUILD)/%.c.$(ENV).o: $(BUILD)/%.c | $(BUILD)
$(QUIET) echo -e "CC\t$<.$(ENV)"
$(QUIET) $(CC) -c -o $(BUILD)/$*.c.$(ENV).o $(CFLAGS) -MF $(BUILD)/$*.c.$(ENV).d -MMD $< $(NO_WARNINGS)
$(BUILD)/%.cc.$(ENV).d $(BUILD)/%.cc.$(ENV).o: $(DIR)/%.cc | $(BUILD)
$(QUIET) echo -e "CXX\t$<.$(ENV)"
$(QUIET) $(CXX) -c -o $(BUILD)/$*.cc.$(ENV).o $(CXXFLAGS) -MF $(BUILD)/$*.cc.$(ENV).d -MMD $< \
$(shell if [ ! -L $(DIR)/$*.cc ]; then echo $(WARNING_CFLAGS); else echo $(NO_WARNINGS); fi)
$(BUILD)/%.c.$(ENV).d $(BUILD)/%.c.$(ENV).o: $(DIR)/%.c | $(BUILD)
$(QUIET) echo -e "CC\t$<.$(ENV)"
$(QUIET) $(CC) -c -o $(BUILD)/$*.c.$(ENV).o $(CFLAGS) -MF $(BUILD)/$*.c.$(ENV).d -MMD $< \
$(shell if [ ! -L $(DIR)/$*.c ]; then echo $(WARNING_CFLAGS); else echo $(NO_WARNINGS); fi)
$(BUILD)/$(ENV).a: $(OBJECTS) | $(BUILD)
$(QUIET) echo -e "AR\t$@"
$(QUIET) ar rcs $@ $^
$(foreach f,$(patsubst %.o,%.d,$(OBJECTS)),$(eval include $f))
all: $(BUILD)/$(ENV).a

159
enclave/README.md Normal file
View File

@ -0,0 +1,159 @@
# SVR2 Enclave Code
SVR2 uses C++ as its language for building an in-enclave binary, with the
OpenEnclave (hereafter 'OE') SDK. The binary, `enclave.bin` is then signed via
OE's `oesign`, which doesn't matter to us because we don't trust the signature,
just the unique ID (SGX "mrenclave") of the resulting signed config. However,
the `oesign` process does one important thing: it binds a config (either
`svr2_test.conf` or `svr2.conf` to the resulting object. Once this process
is complete, the resulting `enclave.signed` file is ready to be loaded into a
DCAP-based SGX enclave.
# Host/enclave communication
Most (all?) host/enclave communication happens through a single ocall/ecall
combination, defined in `../shared/svr2.edl`:
- `svr2_input_message`: Enclave receives a message (a serialized
`HostToEnclaveMessage` protobuf) from the host.
- `svr2_output_message`: Enclave sends a message (a serialized
`EnclaveToHostMessage` protobuf) to the host.
The enclave expects (and enforces, via locking) that the `svr2_input_message`
function is called sequentially. It also guarantees that it will call the
`svr2_output_message` function only during such a call, and sequentially.
Thus, you might get the following control flow:
```
svr2_input_message(HostToEnclaveMessage1)
svr2_output_message(EnclaveToHostMessage1.1)
svr2_output_message(EnclaveToHostMessage1.2)
svr2_output_message(EnclaveToHostMessage1.3)
svr2_input_message(HostToEnclaveMessage2)
svr2_output_message(EnclaveToHostMessage2.1)
svr2_output_message(EnclaveToHostMessage2.2)
svr2_input_message(HostToEnclaveMessage3)
svr2_input_message(HostToEnclaveMessage4)
svr2_output_message(HostToEnclaveMessage4.1)
```
In such a flow, we can reason that input message 1 has output messages
1.1, 1.2, and 1.3, etc. Note that each input can have an arbitrary number of
outputs, including zero (e.g. message 3). In other words, the enclave can
be treated as a function:
```
func CallEnclave(HostToEnclaveMessage) []EnclaveToHostMessage
```
taking in a single HostToEnclaveMessage and returning a list of zero or more
EnclaveToHostMessages.
Certain messages are 'transactions', or messages with a `Request` that want
a specific `Reply`. It is important to note that if a request is
passed in via a message, the response associated with it may not be part of
the returned list. IE: the host may pass in a transaction request, above,
via `EnclaveToHostMessage1`, but may not get back the reply until
`HostToEnclaveMessage4.1`. Transactions have associated transaction IDs,
which allow for disambiguating requests and their associated responses.
Hosts may send transactions to enclaves and enclaves to hosts. Each direction
maintains a unique keyspace for transaction IDs (so HostToEnclave transaction 1
and EnclaveToHost transaction 1 are distinct), and each is responsible for
making sure that transaction requests that they pass are uniquely identified.
## Code Layout
Code is broken into a set of modules, where each module is a one-level-deep
subdirectory within the top-level `enclave` directory. Each module is
independently compiled, then all modules are combined in a final linking step
to form the resulting binary. Modules are listed as `LIBRARIES` within
`Makefile`, and must form a DAG of dependencies. Within the `LIBRARIES` list,
higher libraries may depend on lower libraries, but not vice versa.
Code roughly follows the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html).
# Concurrency in SVR2 Enclave
With SVR2, we're aiming to utilize a single replica group to serve all traffic.
This, of course, brings up issues around scalability. We can of course add
new replicas to the replica group, but with a strong consensus model relying
on agreement between a quorum (in our case, a simple majority) of voting
replicas, additional replicas have the potential to add load rather than shed
it.
To handle this, SVR2 is built to, as much as possible, utilize the resources
of non-leader and non-voting replicas. While we're unable to shed or reduce
load on RAM with added members (each replica needs to store the entire
database), we can shed load in the form of CPU and network resources.
## Utilizing multiple cores
Even without considering excess replicas, we aim to utilize the resources
of each replica to the fullest extent. To do this, the SVR2 enclave binary
is built as a true multi-threaded process, with targetted locking of code
subsections allowing parallel processing as much as possible.
One of the most CPU-intensive tasks that SVR2 partakes in is encryption
and decryption. This takes place when replicas communicate with each
other ("peer communication") and when they accept and service connections
from clients ("client communication"). When establishing these secure
connections, the initial handshake is more CPU-intensive, followed
by less intensive block cipher encryption/decryption. Peer communication
uses long-lived sessions that amortize handshake cost over a long period
of time, while client communication requires a new handshake, a small
amount of communication, and a subsequent closing of the connection.
For both peer and client communication, we aim to be highly parallel on
a single machine: handshaking and block-cipher encryption/decryption
are done with client- and peer-level locks, rather than global ones.
This approach, though, lays some requirements on the host side, as
for both cases, reordering of messages breaks the block-cipher
assumptions of the clients/peers. Internally, SVR2 maintains correct
order of messages it outputs to peers and clients: if message A
to a peer or client happens before message B, then `svr2_output_message(A)`
will be called and allowed to complete before `svr2_output_message(B)`.
However, on the host side, care must be taken to respect this
ordering: when messages are forwarded externally or received from external
hosts, their calls to `svr2_input_message` should follow the same pattern:
if A is received before B in either a peer or client stream, then
`svr2_input_message(A)` should be called and allowed to complete before
`svr2_input_message(B)` is called.
Some global locks are of course still required, in particular around Raft
and its associated logs/database. However, these locked sections are kept
at a minimimum, with as much work done as possible before/after the locks
are acquired.
## Utilizing multiple machines
The primary means to scale SVR2 is the addition of replicas. However,
as mentioned, this has the potential to hinder scaling, especially if
the leader alone is allowed to perform CPU-intensive tasks like servicing
client requests. For this reason, SVR2 is built to allow any replica to
service requests from any client.
When a client connects to SVR2 in a non-leader replica, it will perform
the client handshake and receive/decrypt the client's request entirely
on its own. Once it has done so, it will forward the request to the current
leader as an enclave-to-enclave transaction, receiving in response either
a failure or a log location (an `(index, term)` pair) associated with the
write. Failures are immediately returned to the client. A success, though,
creates a watch-point in the non-leader replica's raft log at `index`.
The replica will wait until `index` is a committed part of its own log (via
normal Raft `AppendEntries` mechanisms), then will check the `term` of the
committed log. If that matches the `term` returned from its write request,
by definition the log at `index` contains the client's request, and when
applying that request to its local database, it can safely return the
response to that client over its still-open channel.
By this mechanism, load (especially client handshake and communication
load) can be shared across all replicas. Crucially, this includes
non-voting replicas, which can be added with minimal increase to the
load on the voting replicas. As non-voting replicas still receive
Raft logs and their commitments, they can happily service client
requests.
## More topics
- [Raft healing](../docs/Healing.md)
- [Enclave messages](../docs/Messages.md)

1
enclave/SipHash Submodule

@ -0,0 +1 @@
Subproject commit eee7d0d84dc7731df2359b243aa5e75d85f6eaef

View File

@ -0,0 +1,90 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "attestation/attestation.h"
#include <openenclave/attestation/custom_claims.h>
#include <openenclave/attestation/verifier.h>
#include <openenclave/attestation/sgx/evidence.h>
#include <array>
#include "noise/noise.h"
#include "metrics/metrics.h"
#include "proto/error.pb.h"
#include "util/macros.h"
namespace svr2::attestation {
const oe_uuid_t sgx_remote_uuid = {OE_FORMAT_UUID_SGX_ECDSA};
/**
* Helper function used to make the claim-finding process more convenient. Given
* the claim name, claim list, and its size, returns the claim with that claim
* name in the list.
*/
const oe_claim_t* FindClaim(const oe_claim_t* claims, size_t claims_size,
const char* name) {
for (size_t i = 0; i < claims_size; i++) {
if (strcmp(claims[i].name, name) == 0) return &(claims[i]);
}
return nullptr;
}
error::Error ReadKeyFromVerifiedClaims(oe_claim_t* claims, size_t claims_length,
std::array<uint8_t, 32>& out) {
const oe_claim_t* claim;
oe_claim_t* custom_claims = nullptr;
size_t custom_claims_length = 0;
// read the custom claims
if ((claim = FindClaim(claims, claims_length,
OE_CLAIM_CUSTOM_CLAIMS_BUFFER)) == nullptr) {
return COUNTED_ERROR(Env_CustomClaimsMissing);
}
// deserialize custom claims
if (oe_deserialize_custom_claims(claim->value, claim->value_size,
&custom_claims,
&custom_claims_length) != OE_OK) {
return COUNTED_ERROR(Env_CustomClaimsDeserialize);
}
auto free_custom_claims_known_size = [custom_claims_length](oe_claim_t* ptr) {
return oe_free_custom_claims(ptr, custom_claims_length);
};
std::unique_ptr<oe_claim_t, decltype(free_custom_claims_known_size)>
free_custom_claims(custom_claims, free_custom_claims_known_size);
// There is one custom claim with name "pk". The value is the key we will
// return.
if (strcmp(custom_claims[0].name, "pk") != 0) {
return COUNTED_ERROR(Env_AttestationPubkeyMissing);
}
if (custom_claims[0].value_size != out.size()) {
return COUNTED_ERROR(Env_AttestationPubkeyInvalidSize);
}
std::copy(custom_claims[0].value,
custom_claims[0].value + custom_claims[0].value_size, out.begin());
return error::OK;
}
std::pair<oe_claim_t*, size_t> VerifyAndReadClaims(
const std::string& evidence, const std::string& endorsements) {
const uint8_t* evidence_data =
reinterpret_cast<const uint8_t*>(evidence.data());
const uint8_t* endorsements_data =
reinterpret_cast<const uint8_t*>(endorsements.data());
oe_claim_t* claims = nullptr;
size_t claims_length = 0;
CHECK(OE_OK == oe_verify_evidence(&sgx_remote_uuid, evidence_data,
evidence.size(), endorsements_data,
endorsements.size(), nullptr, 0, &claims,
&claims_length));
return std::make_pair(claims, claims_length);
}
}; // namespace svr2::attestation

View File

@ -0,0 +1,51 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_ATTESTATION_ATTESTATION_H__
#define __SVR2_ATTESTATION_ATTESTATION_H__
#include <array>
#include <openenclave/attestation/custom_claims.h>
#include <openenclave/attestation/verifier.h>
#include "proto/error.pb.h"
namespace svr2::attestation {
extern const oe_uuid_t sgx_remote_uuid;
/**
* Helper function used to make the claim-finding process more convenient. Given
* the claim name, claim list, and its size, returns the claim with that claim
* name in the list.
*/
const oe_claim_t* FindClaim(const oe_claim_t* claims, size_t claims_size,
const char* name);
/**
* Deserializes Open Enclave format custom claims then finds, validates,
* and returns the public key claim.
*
* claims serialized OpenEnclave claims
* claims_length number of claims
* out: array where public key will be written
* returns Env_CustomClaimsMissing, Env_CustomClaimsDeserialize,
* Env_AttestationPubkeyMissing, Env_AttestationPubkeyInvalidSize
*/
error::Error ReadKeyFromVerifiedClaims(oe_claim_t* claims, size_t claims_length,
std::array<uint8_t, 32>& out);
/**
* Verifies evidence and endorsements and returns the parsed array
* of claims in Open Enclave format.
*
* The returned pointer most be freed with `oe_free_claims`
*/
std::pair<oe_claim_t*, size_t> VerifyAndReadClaims(
const std::string& evidence, const std::string& endorsements);
}; // namespace svr2::attestation
#endif // __SVR2_ATTESTATION_ATTESTATION_H__

225
enclave/client/client.cc Normal file
View File

@ -0,0 +1,225 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "client/client.h"
#include <atomic>
#include "env/env.h"
#include "util/log.h"
#include "metrics/metrics.h"
namespace svr2::client {
static std::atomic<uint64_t> id_gen{1};
const NoiseProtocolId client_protocol = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NK,
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = 0,
};
Client::Client(const std::string& authenticated_id)
: hs_(noise::WrapHandshakeState(nullptr)),
tx_(noise::WrapCipherState(nullptr)),
rx_(noise::WrapCipherState(nullptr)),
id_(id_gen.fetch_add(1)),
authenticated_id_(authenticated_id) {
}
Client::~Client() {
}
error::Error Client::Init(const noise::DHState& dhstate, const e2e::Attestation& attestation) {
util::unique_lock lock(mu_);
NoiseHandshakeState* hs;
if (NOISE_ERROR_NONE != noise_handshakestate_new_by_id(&hs, &client_protocol, NOISE_ROLE_RESPONDER)) {
return COUNTED_ERROR(Client_HandshakeState);
}
auto hs_wrap = noise::WrapHandshakeState(hs);
if (NOISE_ERROR_NONE != noise_dhstate_copy(
noise_handshakestate_get_local_keypair_dh(hs),
dhstate.get())) {
return COUNTED_ERROR(Client_CopyDHState);
}
if (NOISE_ERROR_NONE != noise_handshakestate_start(hs)) {
return COUNTED_ERROR(Client_HandshakeStart);
}
hs_start_.mutable_test_only_pubkey()->resize(32, '\0');
if (NOISE_ERROR_NONE != noise_dhstate_get_public_key(
dhstate.get(),
noise::StrU8Ptr(hs_start_.mutable_test_only_pubkey()),
hs_start_.mutable_test_only_pubkey()->size())) {
return COUNTED_ERROR(Client_ExtractPublicKey);
}
*hs_start_.mutable_evidence() = attestation.evidence();
*hs_start_.mutable_endorsement() = attestation.endorsements();
hs_.swap(hs_wrap);
return error::OK;
}
std::pair<std::string, error::Error> Client::FinishHandshake(context::Context* ctx, const std::string& data) {
ACQUIRE_LOCK(mu_, ctx, lock_client);
MEASURE_CPU(ctx, cpu_client_hs_finish);
if (!hs_.get() || tx_.get() || rx_.get()
|| noise_handshakestate_get_action(hs_.get()) != NOISE_ACTION_READ_MESSAGE) {
return std::make_pair("", COUNTED_ERROR(Client_HandshakeState));
}
std::string buffer = data;
NoiseBuffer read_buf = noise::BufferInputFromString(&buffer);
if (NOISE_ERROR_NONE != noise_handshakestate_read_message(hs_.get(), &read_buf, nullptr)) {
return std::make_pair("", COUNTED_ERROR(Client_FinishReadHandshake));
}
if (NOISE_ACTION_WRITE_MESSAGE != noise_handshakestate_get_action(hs_.get())) {
return std::make_pair("", COUNTED_ERROR(Client_HandshakeState));
}
buffer.resize(noise::HANDSHAKE_INIT_SIZE, '\0');
NoiseBuffer write_buf = noise::BufferOutputFromString(&buffer);
if (NOISE_ERROR_NONE != noise_handshakestate_write_message(hs_.get(), &write_buf, nullptr)) {
return std::make_pair("", COUNTED_ERROR(Client_FinishWriteHandshake));
}
buffer.resize(write_buf.size);
if (NOISE_ACTION_SPLIT != noise_handshakestate_get_action(hs_.get())) {
return std::make_pair("", COUNTED_ERROR(Client_HandshakeState));
}
NoiseCipherState* tx;
NoiseCipherState* rx;
if (NOISE_ERROR_NONE != noise_handshakestate_split(hs_.get(), &tx, &rx)) {
return std::make_pair("", COUNTED_ERROR(Client_FinishSplit));
}
tx_.reset(tx);
rx_.reset(rx);
hs_.reset(nullptr);
return std::make_pair(buffer, error::OK);
}
error::Error Client::DecryptRequest(context::Context* ctx, const std::string& data, google::protobuf::MessageLite* request) {
ACQUIRE_LOCK(mu_, ctx, lock_client);
MEASURE_CPU(ctx, cpu_client_decrypt);
if (hs_.get() || !tx_.get() || !rx_.get()) {
return COUNTED_ERROR(Client_DecryptState);
}
auto [plaintext, err] = noise::Decrypt(rx_.get(), data);
if (err != error::OK) {
return err;
}
if (!request->ParseFromString(plaintext)) {
return COUNTED_ERROR(Client_DecryptParse);
}
return error::OK;
}
std::pair<std::string, error::Error> Client::EncryptResponse(context::Context* ctx, const google::protobuf::MessageLite& response) {
ACQUIRE_LOCK(mu_, ctx, lock_client);
MEASURE_CPU(ctx, cpu_client_encrypt);
if (hs_.get() || !tx_.get() || !rx_.get()) {
return std::make_pair("", COUNTED_ERROR(Client_EncryptState));
}
std::string plaintext;
if (!response.SerializeToString(&plaintext)) {
return std::make_pair("", COUNTED_ERROR(Client_EncryptSerialize));
}
return noise::Encrypt(tx_.get(), plaintext);
}
std::pair<Client*, error::Error> ClientManager::NewClient(context::Context* ctx, const std::string& authenticated_id) {
ACQUIRE_LOCK(mu_, ctx, lock_clientmanager);
MEASURE_CPU(ctx, cpu_client_hs_start);
std::unique_ptr<Client> c(new Client(authenticated_id));
error::Error err = c->Init(dhstate_, attestation_);
if (err != error::OK) {
return std::make_pair(nullptr, err);
}
Client* ptr = c.get();
clients_[ptr->ID()] = std::move(c);
GAUGE(client, clients)->Set(clients_.size());
COUNTER(client, created)->Increment();
return std::make_pair(ptr, error::OK);
}
Client* ClientManager::GetClient(context::Context* ctx, ClientID id) const {
ACQUIRE_LOCK(mu_, ctx, lock_clientmanager);
auto find = clients_.find(id);
if (find == clients_.end()) { return nullptr; }
return find->second.get();
}
bool ClientManager::RemoveClient(context::Context* ctx, ClientID id) {
ACQUIRE_LOCK(mu_, ctx, lock_clientmanager);
auto find = clients_.find(id);
if (find == clients_.end()) { return false; }
clients_.erase(find);
GAUGE(client, clients)->Set(clients_.size());
COUNTER(client, closed)->Increment();
return true;
}
noise::DHState ClientManager::NewDHState() {
COUNTER(client, new_dh_state)->Increment();
noise::DHState out = noise::WrapDHState(nullptr);
NoiseDHState* dhstate;
if (NOISE_ERROR_NONE != noise_dhstate_new_by_id(&dhstate, client::client_protocol.dh_id)) {
return out;
}
noise::DHState client_dh = noise::WrapDHState(dhstate);
if (NOISE_ERROR_NONE != noise_dhstate_generate_keypair(dhstate)) {
return out;
}
client_dh.swap(out);
return out;
}
error::Error ClientManager::RotateKeyAndRefreshAttestation(context::Context* ctx, const enclaveconfig::RaftGroupConfig& config) {
auto dhstate = NewDHState();
auto [attestation, err] = GetAttestation(dhstate, config);
if (err != error::OK) {
COUNTER(client, key_rotate_failure)->Increment();
return err;
}
ACQUIRE_LOCK(mu_, ctx, lock_clientmanager);
dhstate_.swap(dhstate);
attestation_.CopyFrom(attestation);
COUNTER(client, key_rotate_success)->Increment();
return error::OK;
}
error::Error ClientManager::RefreshAttestation(context::Context* ctx, const enclaveconfig::RaftGroupConfig& config) {
auto dhstate = DHState(ctx);
auto [attestation, err] = GetAttestation(DHState(ctx), config);
if (err != error::OK) {
COUNTER(client, attestation_refresh_failure)->Increment();
return err;
}
ACQUIRE_LOCK(mu_, ctx, lock_clientmanager);
attestation_.CopyFrom(attestation);
// There's a chance that a RotateKeyAndRefreshAttestation call
// could have happened between when we got dhstate and when we're
// setting attestation here... reset to the one we received just
// in case.
dhstate_.swap(dhstate);
COUNTER(client, attestation_refresh_success)->Increment();
return error::OK;
}
std::pair<e2e::Attestation, error::Error> ClientManager::GetAttestation(const noise::DHState& dhstate, const enclaveconfig::RaftGroupConfig& config) {
e2e::Attestation attestation;
// get attestation for its public key
uint8_t public_key[32];
if (NOISE_ERROR_NONE != noise_dhstate_get_public_key(dhstate.get(), public_key, sizeof(public_key))) {
return std::make_pair(attestation, error::Peers_NewKeyPublic);
}
env::PublicKey public_key_array {};
std::copy(std::begin(public_key), std::end(public_key), std::begin(public_key_array));
return env::environment->Evidence(public_key_array, config);
}
noise::DHState ClientManager::DHState(context::Context* ctx) const {
ACQUIRE_LOCK(mu_, ctx, lock_clientmanager);
return noise::CloneDHState(dhstate_);
}
} // namespace svr2::client

86
enclave/client/client.h Normal file
View File

@ -0,0 +1,86 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_CLIENT_CLIENT_H__
#define __SVR2_CLIENT_CLIENT_H__
#include <mutex>
#include "proto/error.pb.h"
#include "proto/e2e.pb.h"
#include "noise/noise.h"
#include "sip/hasher.h"
#include "util/endian.h"
#include "util/mutex.h"
#include "context/context.h"
namespace svr2::client {
class ClientManager;
typedef uint64_t ClientID;
extern const NoiseProtocolId client_protocol;
class Client {
public:
ClientID ID() const { return id_; }
// Returns ClientHandshakeStart, with std::move semantics, so this
// function should be used only once.
ClientHandshakeStart MovedHandshakeStart() EXCLUDES(mu_) {
util::unique_lock lock(mu_);
return std::move(hs_start_);
}
bool Handshaking() const {
util::unique_lock lock(mu_);
return hs_.get() != nullptr;
}
std::pair<std::string, error::Error> FinishHandshake(context::Context* ctx, const std::string& data) EXCLUDES(mu_);
error::Error DecryptRequest(context::Context* ctx, const std::string& data, google::protobuf::MessageLite* request) EXCLUDES(mu_);
std::pair<std::string, error::Error> EncryptResponse(context::Context* ctx, const google::protobuf::MessageLite& response) EXCLUDES(mu_);
const std::string& authenticated_id() const { return authenticated_id_; }
private:
~Client();
explicit Client(const std::string& authenticated_id);
error::Error Init(const noise::DHState& dhstate, const e2e::Attestation& attestation) EXCLUDES(mu_);
friend class ClientManager;
friend std::unique_ptr<Client>::deleter_type;
mutable util::mutex mu_;
ClientHandshakeStart hs_start_ GUARDED_BY(mu_);
noise::HandshakeState hs_ GUARDED_BY(mu_);
noise::CipherState tx_ GUARDED_BY(mu_);
noise::CipherState rx_ GUARDED_BY(mu_);
const size_t id_;
const std::string authenticated_id_;
};
class ClientManager {
public:
ClientManager(noise::DHState dhstate) : dhstate_(std::move(dhstate)) {}
error::Error RefreshAttestation(context::Context* ctx, const enclaveconfig::RaftGroupConfig& config) EXCLUDES(mu_);
error::Error RotateKeyAndRefreshAttestation(context::Context* ctx, const enclaveconfig::RaftGroupConfig& config) EXCLUDES(mu_);
static noise::DHState NewDHState();
std::pair<Client*, error::Error> NewClient(context::Context* ctx, const std::string& authenticated_id) EXCLUDES(mu_);
Client* GetClient(context::Context* ctx, ClientID id) const EXCLUDES(mu_);
// Deallocate and remove a client by its ID.
// Client pointers are owned by the ClientManager and can only be deallocated
// via a call to RemoveClient.
bool RemoveClient(context::Context* ctx, ClientID id) EXCLUDES(mu_);
private:
noise::DHState DHState(context::Context* ctx) const EXCLUDES(mu_);
static std::pair<e2e::Attestation, error::Error> GetAttestation(const noise::DHState& dhstate, const enclaveconfig::RaftGroupConfig& config);
mutable util::mutex mu_;
noise::DHState dhstate_ GUARDED_BY(mu_);
e2e::Attestation attestation_ GUARDED_BY(mu_);
std::unordered_map<ClientID, std::unique_ptr<Client>> clients_ GUARDED_BY(mu_);
};
} // namespace svr2::client
#endif // __SVR2_CLIENT_CLIENT_H__

View File

@ -0,0 +1,47 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "context/context.h"
#include "metrics/metrics.h"
#include "util/cpu.h"
namespace svr2::context {
Context::Context() : cpu_current_(nullptr), cpu_top_(nullptr, COUNTER(context, cpu_uncategorized)) {
cpu_top_.SetContext(this);
}
CPUMeasurement::CPUMeasurement(Context* ctx, metrics::Counter* counter)
: ctx_(nullptr), counter_(counter), ticks_(util::asm_rdtsc()) {
if (ctx != nullptr) {
SetContext(ctx);
}
}
CPUMeasurement::~CPUMeasurement() {
uint64_t ticks = util::asm_rdtsc();
counter_->IncrementBy(ticks - ticks_);
if (parent_ != nullptr) {
parent_->ticks_ = ticks;
}
ctx_->cpu_current_ = parent_;
}
void CPUMeasurement::SetContext(Context* ctx) {
CHECK(ctx_ == nullptr);
ctx_ = ctx;
parent_ = ctx_->cpu_current_;
ctx_->cpu_current_ = this;
if (parent_ != nullptr) {
// If there's a parent CPUMeasurement, increment its ticks-so-far.
// When we're destroyed, we'll push parent_->ticks_ forward so ticks
// during our lifetime are not double-counted.
parent_->counter_->IncrementBy(ticks_ - parent_->ticks_);
}
}
CPUMeasurement Context::MeasureCPU(metrics::Counter* counter) {
return CPUMeasurement(this, counter);
}
} // namespace svr2::context

101
enclave/context/context.h Normal file
View File

@ -0,0 +1,101 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_CONTEXT_CONTEXT_H__
#define __SVR2_CONTEXT_CONTEXT_H__
#include <google/protobuf/arena.h>
#include <mutex>
#include "util/macros.h"
#include "metrics/metrics.h"
#include "util/mutex.h"
namespace svr2::context {
class Context;
// Class CPUMeasurement allows for counting of CPU ticks used in parts of code.
// On creation, it records the number of CPU ticks, and on destruction it adds
// those ticks to a counter. It's not stand-alone - use Context.MeasureCPU or
// better-yet use the MEASURE_CPU macro.
//
// Usage:
//
// void Foo(ctx) {
// MEASURE_CPU(ctx, cpu_foo);
// ... stuff #1 ...
// Bar(ctx)
// ... stuff #2 ...
// }
// void Bar(ctx) {
// MEASURE_CPU(ctx, cpu_bar); // turns off cpu_foo ticking, then back on when destroyed
// ... stuff #3 ...
// }
//
// This will count CPU ticks of stuff#1 and stuff#2 (but NOT stuff#3) in
// COUNTER(context, cpu_foo), and measure stuff#3 in COUNTER(context, cpu_bar).
class CPUMeasurement {
public:
~CPUMeasurement();
private:
friend class Context;
CPUMeasurement(Context* ctx, metrics::Counter* counter);
void SetContext(Context* ctx);
Context* ctx_;
metrics::Counter* counter_;
CPUMeasurement* parent_; // Provides a singly-linked list back to parent CPUMeasurements.
uint64_t ticks_;
};
class Context {
public:
DELETE_COPY_AND_ASSIGN(Context);
Context();
// Protobuf<T> creates a protobuf of type <T> that has a lifetime tied
// to the lifetime of this Context (IE: when this context falls out of scope,
// it will be cleaned up) using a protobuf Arena. This optimization allows
// for much faster creation and deletion of intermediate protobufs. However,
// care should be taken to not store the output of this function long-term
// in a class that will live beyond the scope of this Context, as the pointer
// will be invalidated at that time.
template <class T>
T* Protobuf() {
return google::protobuf::Arena::CreateMessage<T>(&arena_);
}
CPUMeasurement MeasureCPU(metrics::Counter* counter);
// All protobufs returned by Protobuf() are no longer valid after this call.
void GarbageCollectProtobufs() { arena_.Reset(); }
private:
friend class CPUMeasurement;
google::protobuf::Arena arena_;
CPUMeasurement* cpu_current_;
CPUMeasurement cpu_top_;
};
} // namespace svr2::context
#define MEASURE_CPU(ctx, name) \
::svr2::context::CPUMeasurement __cpumeasure_ ## __COUNTER__ = (ctx)->MeasureCPU(COUNTER(context, name))
// Creates an RAII util::unique_lock named `lockname`. Use this
// if you need to do things with the lock after you create it (e.g., explicitly
// calling `unlock()`).
#define ACQUIRE_NAMED_LOCK(lockname, mu, ctx, name) \
util::unique_lock lockname(mu, std::defer_lock); \
{ \
MEASURE_CPU(ctx, name); \
lockname.lock(); \
}
// Creates an RAII util::unique_lock on the given mu with an arbitrary
// name, for when you need `mu` locked but you're not doing anything
// tricky with it like manually unlocking it after. This is more like
// std::lock_guard.
#define ACQUIRE_LOCK(mu, ctx, name) ACQUIRE_NAMED_LOCK(__lock_ ## __COUNTER__, mu, ctx, name)
#endif // __SVR2_CONTEXT_CONTEXT_H__

View File

@ -0,0 +1,69 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP context
//TESTDEP metrics
//TESTDEP proto
//TESTDEP protobuf-lite
#include <gtest/gtest.h>
#include "context/context.h"
#include "util/macros.h"
#include "util/mutex.h"
#include <time.h>
#include <pthread.h>
#include <mutex>
namespace svr2::util {
class AcquireLockTest : public ::testing::Test {
public:
util::mutex mu;
int in_use GUARDED_BY(mu) = 0;
static void* AcquireAndSleep(void* in) {
auto t = (AcquireLockTest*) in;
context::Context ctx;
ACQUIRE_LOCK(t->mu, &ctx, lock_test);
t->in_use++;
for (int i = 0; i < 10; i++) {
usleep(100000);
CHECK(t->in_use == 1);
}
t->in_use--;
CHECK(t->in_use == 0);
return NULL;
}
static void* AcquireNamedAndSleep(void* in) {
auto t = (AcquireLockTest*) in;
context::Context ctx;
ACQUIRE_NAMED_LOCK(lock, t->mu, &ctx, lock_test);
t->in_use++;
for (int i = 0; i < 10; i++) {
usleep(100000);
CHECK(t->in_use == 1);
}
t->in_use--;
CHECK(t->in_use == 0);
return NULL;
}
};
TEST_F(AcquireLockTest, Unnamed) {
pthread_t t1, t2, t3, t4;
auto start = time(NULL);
CHECK(0 == pthread_create(&t1, NULL, &AcquireLockTest::AcquireAndSleep, this));
CHECK(0 == pthread_create(&t2, NULL, &AcquireLockTest::AcquireNamedAndSleep, this));
CHECK(0 == pthread_create(&t3, NULL, &AcquireLockTest::AcquireAndSleep, this));
CHECK(0 == pthread_create(&t4, NULL, &AcquireLockTest::AcquireNamedAndSleep, this));
CHECK(0 == pthread_join(t1, NULL));
CHECK(0 == pthread_join(t2, NULL));
CHECK(0 == pthread_join(t3, NULL));
CHECK(0 == pthread_join(t4, NULL));
auto diff = time(NULL) - start;
ASSERT_GE(diff, 3);
ASSERT_LE(diff, 5);
}
} // namespace svr2::util

1689
enclave/core/core.cc Normal file

File diff suppressed because it is too large Load Diff

311
enclave/core/core.h Normal file
View File

@ -0,0 +1,311 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_CORE_CORE_H__
#define __SVR2_CORE_CORE_H__
#include <memory>
#include <mutex>
#include <atomic>
#include "proto/enclaveconfig.pb.h"
#include "proto/error.pb.h"
#include "proto/msgs.pb.h"
#include "util/macros.h"
#include "peerid/peerid.h"
#include "peers/peers.h"
#include "context/context.h"
#include "raft/log.h"
#include "raft/raft.h"
#include "client/client.h"
#include "sip/hasher.h"
#include "db/db.h"
#include "core/internal.h"
#include "util/macros.h"
#include "util/ticks.h"
#include "timeout/timeout.h"
#include "groupclock/groupclock.h"
namespace svr2::core {
// Core is the core singleton of a running enclave. Each running enclave
// should have exactly one of these, created on initialization.
class Core {
public:
DELETE_COPY_AND_ASSIGN(Core);
// Receive a message from the host.
error::Error Receive(context::Context* ctx, const UntrustedMessage& msg);
// Peer ID for this core.
const peerid::PeerID& ID() const { return peer_manager_->ID(); }
// Create a core from a given config.
static std::pair<std::unique_ptr<Core>, error::Error> Create(
context::Context* ctx,
const enclaveconfig::InitConfig& config);
#ifdef IS_TEST
bool serving() const {
util::unique_lock lock(raft_.mu);
return raft_.state == svr2::RAFTSTATE_LOADED_PART_OF_GROUP;
}
bool leader() const {
util::unique_lock lock(raft_.mu);
return raft_.state == svr2::RAFTSTATE_LOADED_PART_OF_GROUP && raft_.loaded.raft->is_leader();
}
bool voting() const {
util::unique_lock lock(raft_.mu);
return raft_.state == svr2::RAFTSTATE_LOADED_PART_OF_GROUP && raft_.loaded.raft->voting();
}
size_t num_voting() const {
util::unique_lock lock(raft_.mu);
return raft_.loaded.raft->membership().voting_replicas().size();
}
size_t num_members() const {
util::unique_lock lock(raft_.mu);
return raft_.loaded.raft->membership().all_replicas().size();
}
std::set<peerid::PeerID> all_replicas() const {
util::unique_lock lock(raft_.mu);
return raft_.loaded.raft->membership().all_replicas();
}
#endif
private:
struct ReplicationPushState {
ReplicationPushState(raft::LogIdx idx, const peerid::PeerID& to, const e2e::TransactionRequest& req)
: logs_from_idx_inclusive(idx),
db_from_key_exclusive(""),
finished_sending(false),
target(to),
tx(req.request_id()),
replication_id(req.replicate_state().replication_id()),
replication_sequence(0),
sent_response(false) {}
raft::LogIdx logs_from_idx_inclusive; // GUARDED_BY(raft_.mu)
std::string db_from_key_exclusive; // GUARDED_BY(raft_.mu)
bool finished_sending; // GUARDED_BY(raft_.mu)
const peerid::PeerID target;
const internal::TransactionID tx;
const uint64_t replication_id;
uint64_t replication_sequence; // GUARDED_BY(raft_mu)
std::atomic<bool> sent_response;
};
Core(const enclaveconfig::RaftGroupConfig& group_config);
// Init this core object. This function should be
// called exactly once for each Core object, and sould be the first function
// called subsequent to construction.
error::Error Init(
context::Context* ctx,
const enclaveconfig::EnclaveConfig& config,
util::UnixSecs initial_timestamp_unix_secs);
//// Top-level callers, called by Receive(), and their subfunctions.
// Handle a request from the host
error::Error HandleHostToEnclave(context::Context* ctx, const HostToEnclaveRequest& msg);
// Handle a request for a new client
void HandleNewClient(context::Context* ctx, const NewClientRequest& msg, internal::TransactionID tx);
// Handle a message being passed through the host to an existing client
error::Error HandleExistingClient(context::Context* ctx, const ExistingClientRequest& msg, internal::TransactionID tx);
// Request that we create a new raft group from scratch, setting ourselves
// as the sole member and leader. This should be done to seed a new
// Raft, after which we should requst JoinRaft instead.
void HandleCreateNewRaftGroupRequest(context::Context* ctx, internal::TransactionID tx) EXCLUDES(raft_.mu);
// Creates a test account within the Raft DB.
error::Error AddTestAccount(context::Context* ctx, uint32_t i);
// Join an existing Raft group.
void HandleJoinRaft(context::Context* ctx, const JoinRaftRequest& msg, internal::TransactionID tx) EXCLUDES(raft_.mu);
// Given a single seed peer, connect to it and get the existing configs.
void JoinRaftFromFirstPeer(context::Context* ctx) REQUIRES(raft_.mu);
// Replicate all data from our existing peer(s) until we've got a full set of data.
void RequestRaftReplication(context::Context* ctx) REQUIRES(raft_.mu);
// Now that we've got a full set of Raft data (logs+db), set up our local Raft objects.
void PromoteRaftToLoaded(context::Context* ctx) REQUIRES(raft_.mu);
// Request to become a (nonvoting) member of the Raft group we have data for.
void RaftRequestMembership(context::Context* ctx, internal::TransactionID tx) REQUIRES(raft_.mu);
// Refresh attestations for peer and client connections.
error::Error HandleRefreshAttestation(context::Context* ctx, bool rotate_key) EXCLUDES(raft_.mu);
// Get the current status of this replica to be returned to the host.
std::pair<EnclaveReplicaStatus, error::Error> HandleGetEnclaveStatus(context::Context* ctx) const EXCLUDES(raft_.mu);
// Handle a host-requested delete of a backup ID.
error::Error HandleHostDatabaseRequest(context::Context* ctx, internal::TransactionID tx, const DatabaseRequest& req);
// Reconfigure the replica with new host-supplied configuration.
error::Error HandleReconfigure(context::Context* ctx, internal::TransactionID tx, const enclaveconfig::EnclaveConfig& req) EXCLUDES(raft_.mu);
// If we're the raft leader, give it up.
void HandleRelinquishLeadership(context::Context* ctx, internal::TransactionID tx) EXCLUDES(raft_.mu);
// Request that this replica be removed from the Raft group.
void HandleHostRequestedRaftRemoval(context::Context* ctx, internal::TransactionID tx) EXCLUDES(raft_.mu);
// Compute and return to the host a hash of the current DB.
error::Error HandleHostHashes(context::Context* ctx, internal::TransactionID tx) EXCLUDES(raft_.mu);
// Handle the inevitable march of time.
void HandleTimerTick(context::Context* ctx, const TimerTick& tick);
// Update our group-based concept of time.
void MaybeUpdateGroupTime(context::Context* ctx) EXCLUDES(raft_.mu);
// If we're in Raft with some other replicas but don't yet have peer connections
// to them, try to establish them.
void ConnectToRaftMembers(context::Context* ctx) REQUIRES(raft_.mu);
// Return either a nullptr, or a replica config (in scope [ctx]) that
// this instance believes should be the next config for this raft group.
raft::ReplicaGroup* NextReplicaGroup(context::Context* ctx) REQUIRES(raft_.mu);
// Decode a new message proxied from a peer replica through our host.
error::Error HandlePeerMessage(context::Context* ctx, const UntrustedMessage& msg);
// Handle an EnclaveToEnclaveMessage decoded from the peer message
error::Error HandleE2E(context::Context* ctx, const peerid::PeerID& from, const e2e::EnclaveToEnclaveMessage& msg);
// Handle the case where we've just successfully established a connection to the peer `from`
void HandlePeerConnect(context::Context* ctx, const peerid::PeerID& from);
// Handle an enclave-to-enclave transaction requested by a remote peer client.
error::Error HandleE2ETransaction(context::Context* ctx, const peerid::PeerID& from, const e2e::TransactionRequest& msg);
// Handle a request to replicate our state (Raft DB and logs) to `from`
error::Error HandleReplicateStateRequest(context::Context* ctx, const peerid::PeerID& from, const e2e::TransactionRequest& req) EXCLUDES(raft_.mu);
// Send the next set of replicating state to `from`, in the form of a ReplicateStatePush E2E transaction.
void SendNextReplicationState(context::Context* ctx, std::shared_ptr<ReplicationPushState> push_state) REQUIRES(raft_.mu);
// Handle receipt of the next piece of state from a server that's replicating their state to us.
error::Error HandleReplicateStatePush(context::Context* ctx, const e2e::ReplicateStatePush& push) EXCLUDES(raft_.mu);
// Handle applying replicated state to an as-yet-unfinished Raft database (in raft_.loading.db)
error::Error MaybeApplyLogToReplicatingDatabase(context::Context* ctx, const raft::LogEntry& entry) REQUIRES(raft_.mu);
// Handle a request to join our Raft group.
error::Error HandleRequestRaftMembership(context::Context* ctx, const peerid::PeerID& from, e2e::TransactionResponse* resp) EXCLUDES(raft_.mu);
// Handle a request to become a voting member of our Raft group.
error::Error HandleRequestRaftVoting(context::Context* ctx, const peerid::PeerID& from, e2e::TransactionResponse* resp) EXCLUDES(raft_.mu);
// Handle a request to write a client log into our Raft group.
error::Error HandleRaftWrite(context::Context* ctx, const std::string& data, e2e::TransactionResponse* resp) EXCLUDES(raft_.mu);
// Handle receipt of a new timestamp supplied by `from`.
void HandleNewTimestamp(context::Context* ctx, const peerid::PeerID& from, uint64_t unix_secs);
// Handle a request to remove the sender from Raft.
error::Error HandlePeerRequestedRaftRemoval(context::Context* ctx, const peerid::PeerID& from, internal::TransactionID tx) EXCLUDES(raft_.mu);
//// Common or utility functions called by multiple handlers.
// RaftStep handles sending any outstanding raft messages and applying
// any committed transactions. It should be called after any change to
// Raft state, including receiving a raft message, requesting a client
// log, etc.
void RaftStep(context::Context* ctx) REQUIRES(raft_.mu);
// Send any messages buffered by raft to our peers.
void RaftSendMessages(context::Context* ctx) REQUIRES(raft_.mu);
// See if any logs have been committed since last we looked, and apply them to our
// internal state if there are some.
void RaftHandleCommittedLogs(context::Context* ctx) REQUIRES(raft_.mu);
// Handle a Raft log that changes group membership, which may either
// add us to a group or remove us from our group.
void HandleRaftMembershipChange(
context::Context* ctx,
raft::LogIdx idx,
raft::TermId term,
const raft::ReplicaGroup& membership_change) REQUIRES(raft_.mu);
// Attempt to apply the committed log entry to the db::DB. On success,
// return a db::DB::Response (owned by [ctx]). On failure, return
// nullptr. Regardless, [committed_entry] is considered to be successfully
// committed to the database after this call.
db::DB::Response* RaftApplyLogToDatabase(
context::Context* ctx,
raft::LogIdx idx,
const raft::LogEntry& committed_entry) REQUIRES(raft_.mu);
// HandleLogTransactionsForRaftLog handles any queued log
// transactions in outstanding_log_transactions_ associated with the given
// log entry.
void HandleLogTransactionsForRaftLog(
context::Context* ctx,
raft::LogIdx idx,
const raft::LogEntry& entry,
// response may be null in the case where we failed to parse it from the Raft log.
const db::DB::Response* response) REQUIRES(raft_.mu);
// Send a local timestamp to remote peer `to`.
void SendTimestamp(context::Context* ctx, peerid::PeerID to, uint64_t unix_seconds);
// Send our local timestamp to all connected peers.
void SendTimestampToAll(context::Context* ctx);
static error::Error ValidateConfig(const enclaveconfig::EnclaveConfig& config);
static error::Error ValidateConfigChange(const enclaveconfig::EnclaveConfig& old_config, const enclaveconfig::EnclaveConfig& new_config);
mutable util::mutex config_mu_;
enclaveconfig::EnclaveConfig enclave_config_ GUARDED_BY(config_mu_);
const enclaveconfig::RaftGroupConfig raft_config_template_;
enclaveconfig::EnclaveConfig* enclave_config(context::Context* ctx) const EXCLUDES(config_mu_);
std::unique_ptr<peers::PeerManager> peer_manager_;
std::unique_ptr<client::ClientManager> client_manager_;
internal::Raft raft_;
const enclaveconfig::DatabaseVersion db_version_;
const db::DB::Protocol* const db_protocol_;
groupclock::Clock clock_;
// Handle timeouts.
timeout::Timeout timeout_;
typedef std::function<void(
// Context in which to run this callback.
context::Context*,
// Error that may have occurred disallowing this transaction from completing.
// Will be Core_LogTransactionCancelled if this log was not the one requested.
error::Error,
// The committed log entry. Null if err!=OK.
const raft::LogEntry* entry,
// If this log was a client request, the associated client response.
// Null if err!=OK.
const db::DB::Response* response)> LogTransactionCallback;
// When we submit a transaction to the log, we get back the idx/term
// at which it should be committed. Later, we see that LogIdx go by, and
// if the term matches, we're in business and can execute the transaction.
// If the term does _not_ match, then this transaction was overridden or
// cancelled by a Raft election.
struct LogTransaction {
raft::TermId term;
LogTransactionCallback cb;
// If the expected_hash_chain is the empty string it is ignored. Otherwise
// if the hash_chain for this long index does not match the
// expected_hash_chain the transaction is aborted.
std::string expected_hash_chain;
};
// This is a multimap because, if the leader changes, we could possibly
// have multiple transactions mapped to the same log index (with different
// terms).
util::mutex outstanding_log_transactions_mu_;
std::unordered_multimap<raft::LogIdx, LogTransaction> outstanding_log_transactions_ GUARDED_BY(outstanding_log_transactions_mu_);
// Adds a callback to be run when the log at the given location has been commited.
// NOTE: when cb is called, raft_.mu will be locked already.
void AddLogTransaction(context::Context* ctx, const raft::LogLocation& loc, LogTransactionCallback cb) EXCLUDES(outstanding_log_transactions_mu_);
error::Error RaftWriteLogTransaction(context::Context* ctx, const std::string& data, LogTransactionCallback cb) EXCLUDES(raft_.mu);
LogTransactionCallback ClientLogTransaction(context::Context* ctx, client::ClientID client_id, internal::TransactionID tx);
// State for transactions that this enclave sends to other enclaves.
// Transactions are kept locally as a map of callbacks (of type
// E2ECallback). On receipt of a response, we look for the appropriate
// callback in the outstanding_e2e_transactions_ map and call it.
util::mutex e2e_txn_mu_ ACQUIRED_AFTER(raft_.mu);
internal::TransactionID e2e_txn_id_ GUARDED_BY(e2e_txn_mu_);
typedef std::function<void(
// Context in which to run this callback
context::Context*,
// Error that may have occurred disallowing this transaction from
// completing. For example, if we were unable to encrypt correctly
// for the associated peer, etc.
error::Error,
// If error==OK, the transaction response (otherwise nullptr)
const e2e::TransactionResponse*)> E2ECallback;
struct E2ECall {
E2ECallback callback;
timeout::Cancel timeout_cancel;
};
std::unordered_map<internal::TransactionID, E2ECall> outstanding_e2e_transactions_ GUARDED_BY(e2e_txn_mu_);
// Send an Enclave-to-enclave transaction.
void SendE2ETransaction(
context::Context* ctx,
const peerid::PeerID& to,
const e2e::TransactionRequest& req,
bool with_timeout, // If false, allow to run forever.
E2ECallback callback) EXCLUDES(e2e_txn_mu_);
error::Error SendE2EError(context::Context* ctx, const peerid::PeerID& from, internal::TransactionID id, error::Error err);
};
} // namespace svr2::core
#endif // __SVR2_CORE_CORE_H__

View File

@ -0,0 +1,145 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "replicagroup.h"
#include <gtest/gtest.h>
namespace svr2::core::test {
bool ReplicaGroup::IsQuiet() const {
for (const auto& [peer_id, core] : peers_by_id_) {
if (core->active() && core->input_messages().size() > 0) return false;
}
return true;
}
error::Error ReplicaGroup::SendMessage(peerid::PeerID to, PeerMessage msg) {
peerid::PeerID from;
from.FromString(msg.peer_id());
PartitionID to_partition = partition_[to];
PartitionID from_partition = partition_[from];
if (to_partition == from_partition) {
LOG(VERBOSE) << "#####################################################";
LOG(VERBOSE) << "# peer message to " << to << " from " << from;
RETURN_IF_ERROR(peers_by_id_[to]->AddPeerMessage(std::move(msg)));
} else {
LOG(VERBOSE) << "#---------------------------------------------------#";
LOG(VERBOSE) << "# BLOCKED peer message to " << to << " from " << from;
blocked_peer_messages_[to].emplace_back(std::move(msg));
}
return error::OK;
}
error::Error ReplicaGroup::PassMessagesUntilQuiet(PartitionID pid) {
error::Error err = error::OK;
while (!IsQuiet()) {
for (auto& core : peers_) {
if (pid == FULL_GROUP_PARTITION_ID ||
partition_.find(core->ID())->second == pid) {
RETURN_IF_ERROR(core->ProcessIncomingMessage());
RETURN_IF_ERROR(core->ForwardOutgoingMessages());
}
}
}
return err;
}
error::Error ReplicaGroup::ProcessAllH2EResponses(PartitionID pid) {
for (auto& core : peers_) {
if (pid == FULL_GROUP_PARTITION_ID ||
partition_.find(core->ID())->second == pid) {
RETURN_IF_ERROR(core->ProcessAllH2EResponses());
}
}
return error::OK;
}
error::Error ReplicaGroup::TickAllTimers(PartitionID pid) {
for (auto& [peer_id, core] : peers_by_id_) {
if (pid == FULL_GROUP_PARTITION_ID ||
partition_.find(peer_id)->second == pid) {
RETURN_IF_ERROR(core->TimerTick());
RETURN_IF_ERROR(core->ProcessIncomingMessage());
}
}
for (auto& [peer_id, core] : peers_by_id_) {
if (pid == FULL_GROUP_PARTITION_ID ||
partition_.find(peer_id)->second == pid) {
RETURN_IF_ERROR(core->ForwardOutgoingMessages());
}
}
return error::OK;
}
void ReplicaGroup::TickTock(bool ignore_h2e_errors) {
TickTock(FULL_GROUP_PARTITION_ID, ignore_h2e_errors);
}
void ReplicaGroup::TickTock(PartitionID pid, bool ignore_h2e_errors) {
ASSERT_EQ(error::OK, TickAllTimers(pid));
ASSERT_EQ(error::OK, PassMessagesUntilQuiet());
auto err = ProcessAllH2EResponses();
if (!ignore_h2e_errors) ASSERT_EQ(error::OK, err);
}
void ReplicaGroup::add_peer() {
peers_.emplace_back(std::make_unique<TestingCore>(*this));
auto peer = peers_.rbegin()->get();
peers_by_id_[peer->ID()] = peer;
}
void ReplicaGroup::Init(enclaveconfig::InitConfig cfg,
size_t initial_voting,
size_t initial_nonvoting, size_t initial_nonmember) {
init_config_ = cfg;
enclave_config_ = cfg.enclave_config();
size_t num_cores = initial_voting + initial_nonvoting + initial_nonmember;
LOG(INFO) << "ADDING " << num_cores << " PEERS";
for (size_t i = 0; i < num_cores; ++i) {
add_peer();
}
LOG(INFO) << "CREATING RAFT";
ASSERT_EQ(error::OK, peers_[0]->CreateNewRaftGroup());
ASSERT_EQ(error::OK, PassMessagesUntilQuiet());
for (size_t i = 1; i < initial_voting + initial_nonvoting; ++i) {
LOG(INFO) << "JOINING " << i << " of " << (initial_nonvoting + initial_voting);
// request to join raft from the previous peer (so not always the leader)
ASSERT_EQ(error::OK, peers_[i]->JoinRaft(peers_[i - 1]->ID()));
ASSERT_EQ(error::OK, PassMessagesUntilQuiet());
CHECK(peers_[i]->serving());
}
for (size_t i = 1; i < initial_voting; ++i) {
LOG(INFO) << "VOTING " << i << " of " << initial_voting;
ASSERT_EQ(error::OK, peers_[i]->RequestVoting());
ASSERT_EQ(error::OK, PassMessagesUntilQuiet());
}
std::vector<peerid::PeerID> partition_members;
for (const auto& peer : peers_) {
auto peer_id = peer->ID();
partition_[peer_id] = 1;
partition_members.emplace_back(std::move(peer_id));
}
partition_members_.emplace(std::make_pair(1, partition_members));
ASSERT_EQ(error::OK, PassMessagesUntilQuiet());
}
void ReplicaGroup::ForwardBlockedMessages() {
for (auto&& [peer_id, msgs] : blocked_peer_messages_) {
for (auto&& msg : msgs) {
peerid::PeerID from;
from.FromString(msg.peer_id());
LOG(VERBOSE) << "#******************************************#";
LOG(VERBOSE) << "# Forwarding blocked peer message to " << peer_id
<< " from " << from;
peers_by_id_[peer_id]->AddPeerMessage(std::move(msg));
}
}
}
}; // namespace svr2::core::test

View File

@ -0,0 +1,249 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_CORE_CORETEST_REPLICAGROUP_H__
#define __SVR2_CORE_CORETEST_REPLICAGROUP_H__
#include <algorithm>
#include <map>
#include <numeric>
#include "core/core.h"
#include "peerid/peerid.h"
#include "testingcore.h"
#include "util/macros.h"
namespace svr2::core::test {
using PartitionID = uint32_t;
using TestingCoreMap = std::map<peerid::PeerID, TestingCore *>;
using PartitionMap = std::map<peerid::PeerID, PartitionID>;
using ReversePartitionMap = std::map<PartitionID, std::vector<peerid::PeerID>>;
template <typename T>
std::pair<PartitionID, size_t> LargestPartition(
const std::map<T, PartitionID> &partition) {
std::map<PartitionID, size_t> counts;
size_t max_count{0};
PartitionID largest_partition{0};
for (const auto &[key, val] : partition) {
counts[val]++;
if (counts[val] > max_count) {
max_count = counts[val];
largest_partition = val;
}
}
return std::make_pair(largest_partition, max_count);
}
class ReplicaGroup {
// This PartitionID represents the full replica group and is used
// internally to override an existing partition.
static const PartitionID FULL_GROUP_PARTITION_ID = UINT32_MAX;
public:
ReplicaGroup() {}
// This is not copyable because `peers_` is not copyable
DELETE_COPY_AND_ASSIGN(ReplicaGroup);
const TestingCore *get_core(size_t i) const {
CHECK(i < peers_.size());
return peers_[i].get();
}
TestingCore *get_core(size_t i) {
CHECK(i < peers_.size());
return peers_[i].get();
}
TestingCore *get_leader_core() { return get_core(GroupLeaderIndex()); }
const TestingCore *get_leader_core() const {
return get_core(GroupLeaderIndex());
}
TestingCore *get_voting_nonleader_core() {
auto peer = std::find_if(peers_.cbegin(), peers_.cend(), [](const auto &p) {
return p->voting() && !p->leader();
});
return peer != peers_.cend() ? peer->get() : nullptr;
}
size_t partition_size(size_t i) const {
auto id = peers_[i]->ID();
PartitionID part_id = partition_.find(id)->second;
return partition_members_.find(part_id)->second.size();
}
enclaveconfig::EnclaveConfig get_enclave_config() const {
return enclave_config_;
}
enclaveconfig::InitConfig get_init_config() const {
return init_config_;
}
size_t num_voting() const { return get_leader_core()->num_voting(); }
size_t num_serving() const { return get_leader_core()->num_serving(); }
/***
* Creates and initializes TestingCores with given configuration. The first
* `initial_voting` items in the returned vector will be accepted voting
* members, the next `initial_nonvoting` will be up-to-date non-voting
* members, and the rest will be connected non-members
*/
void Init(enclaveconfig::InitConfig cfg,
size_t initial_voting,
size_t initial_nonvoting, size_t initial_nonmember);
/**
* @brief Check whether any replicas have messages to process
*
* @return true Some replica has a message to process
* @return false No messages to process
*/
bool IsQuiet() const;
/**
* @brief Get the ID of the group leader if a quorum with a leader exists.
*
* @return peerid::PeerID A valid ID if a quorum is possible and a leader
* exists
*/
peerid::PeerID GroupLeader() const {
auto index = GroupLeaderIndex();
return index < peers_.size() ? peers_[index]->ID() : peerid::PeerID();
}
/**
* @brief Get the index of the group leader if a quorum with a leader exists.
*
* @return size_t SIZE_MAX if no leader is possible, index of the leader
* otherwise.
*/
size_t GroupLeaderIndex() const {
auto [largest_partition, partition_size] = LargestPartition(partition_);
auto found = std::find_if(
peers_.cbegin(), peers_.cend(),
[this, largest_partition = largest_partition](const auto &p) {
return p->leader() && p->active() &&
partition_.find(p->ID())->second == largest_partition;
});
return found - peers_.cbegin();
}
/**
* @brief Find ID of group leader in a peer's partition
*
* @param peer_id ID of peer looking for reachable leader
* @return peerid::PeerID ID of a replica that (1) believes it is leader and
* (2) is in same partition as peer_id OR, if not found, returns invalid
* PeerID.
*/
peerid::PeerID GroupLeaderInPartition(peerid::PeerID peer_id) const {
auto found = std::find_if(peers_.cbegin(), peers_.cend(),
[this, peer_id](const auto &p) {
return p->leader() && p->active() &&
partition_.find(p->ID())->second ==
partition_.find(peer_id)->second;
});
return found == peers_.cend() ? peerid::PeerID() : (*found)->ID();
}
/**
* @brief Find index of group leader in a peer's partition
*
* @param peer_id ID of peer looking for reachable leader
* @return size_t of a replica that (1) believes it is leader and (2) is
* in same partition as peer_id OR, if not found, returns peers_.size().
*/
size_t GroupLeaderIndexInPartition(peerid::PeerID peer_id) const {
auto found = std::find_if(peers_.cbegin(), peers_.cend(),
[this, peer_id](const auto &p) {
return p->leader() && p->active() &&
partition_.find(p->ID())->second ==
partition_.find(peer_id)->second;
});
return found - peers_.cbegin();
}
/**
* @brief Send a message (through the `replica_group_` fabric) to a peer.
*
* @param to Recipient ID
* @param msg
* @return error::Error Error from `TestingCore::AddPeerMessage` or
* `error::OK`.
*/
error::Error SendMessage(peerid::PeerID to, PeerMessage msg);
/**
* @brief All peers in a partition process incoming messages then forward
* resulting outgoing messages until there are no more incoming messages to
* process
*
* @param pid Optional partition ID. If not provided then partitioning is
* ignored and it applies to full group
* @return error::Error
*/
error::Error PassMessagesUntilQuiet(
PartitionID pid = FULL_GROUP_PARTITION_ID);
/**
* @brief All peers in a partition process all responses from enclaves to
* hosts.
*
* @param pid Optional partition ID. If not provided then partitioning is
* ignored and it applies to full group
* @return error::Error returns any error from a HostToEnclaveResponse
*/
error::Error ProcessAllH2EResponses(
PartitionID pid = FULL_GROUP_PARTITION_ID);
/**
* @brief All peers in a partition get a timer tick, process it, then forward
* any outgoung messages
*
* @param pid
* @return error::Error
*/
error::Error TickAllTimers(PartitionID pid = FULL_GROUP_PARTITION_ID);
/**
* @brief Tick all timers, pass messages until quiet, and then optionally
* check to see if any errors came back in the HostToEnclaveResponses
*
* @param ignore_h2e_errors
*/
void TickTock(bool ignore_h2e_errors);
void TickTock(PartitionID pid, bool ignore_h2e_errors);
void CreatePartition(std::map<size_t, PartitionID> partition) {
partition_.clear();
partition_members_.clear();
// map the array indices to PeerIDs
for (auto [idx, partition_id] : partition) {
auto peer_id = get_core(idx)->ID();
partition_[peer_id] = partition_id;
partition_members_[partition_id].emplace_back(peer_id);
}
}
void ClearPartition() {
partition_.clear();
partition_members_.clear();
for (const auto &peer : peers_) {
partition_[peer->ID()] = 1;
partition_members_[1].emplace_back(peer->ID());
}
}
void ForwardBlockedMessages();
void ClearBlockedMessages() { blocked_peer_messages_.clear(); }
private:
void add_peer();
enclaveconfig::EnclaveConfig enclave_config_;
enclaveconfig::InitConfig init_config_;
std::vector<std::unique_ptr<TestingCore>> peers_;
TestingCoreMap peers_by_id_;
PartitionMap partition_;
ReversePartitionMap partition_members_;
std::map<peerid::PeerID, std::vector<PeerMessage>> blocked_peer_messages_;
};
}; // namespace svr2::core::test
#endif // __SVR2_CORE_CORETEST_REPLICAGROUP_H__

View File

@ -0,0 +1,184 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "testingclient.h"
#include <gtest/gtest.h>
#include <noise/protocol/errors.h>
#include "testingcore.h"
#include "util/bytes.h"
#define NOISE_OK(x) \
do { \
int out = (x); \
if (out != NOISE_ERROR_NONE) { \
char buf[64]; \
noise_strerror(out, buf, sizeof(buf)); \
ASSERT_EQ(out, NOISE_ERROR_NONE) << "Noise error: " << buf; \
} \
} while (0)
namespace svr2::core::test {
using svr2::util::ByteArrayToString;
TestingClient::TestingClient(TestingCore& core, const std::string& authenticated_id)
: core_(core),
client_authenticated_id_(authenticated_id),
hs_(noise::WrapHandshakeState(nullptr)),
tx_(noise::WrapCipherState(nullptr)),
rx_(noise::WrapCipherState(nullptr)) {}
void TestingClient::RequestHandshake() {
state_ = State::HANDSHAKING;
ASSERT_EQ(error::OK, core_.NewClientRequest(this, client_authenticated_id_));
NoiseHandshakeState* hsp;
NOISE_OK(noise_handshakestate_new_by_id(&hsp, &client::client_protocol,
NOISE_ROLE_INITIATOR));
hs_ = noise::WrapHandshakeState(hsp);
}
void TestingClient::RequestBackup(SecretData data, PIN pin, uint32_t tries) {
LOG(INFO) << "sending backup request";
client::Request req;
auto b = req.mutable_backup();
b->set_data(ByteArrayToString(data));
b->set_pin(ByteArrayToString(pin));
b->set_max_tries(tries);
// serialize and encrypt
std::string req_str;
ASSERT_TRUE(req.SerializeToString(&req_str));
auto [ciphertext, encrypt_err] = noise::Encrypt(tx_.get(), req_str);
ASSERT_EQ(error::OK, encrypt_err);
ASSERT_EQ(error::OK,
core_.ExistingClientRequest(this, client_id_, ciphertext));
state_ = State::AWAITING_BACKUP;
}
void TestingClient::RequestExpose(SecretData data) {
LOG(INFO) << "sending expose request";
client::Request req;
auto b = req.mutable_expose();
b->set_data(ByteArrayToString(data));
// serialize and encrypt
std::string req_str;
ASSERT_TRUE(req.SerializeToString(&req_str));
auto [ciphertext, encrypt_err] = noise::Encrypt(tx_.get(), req_str);
ASSERT_EQ(error::OK, encrypt_err);
ASSERT_EQ(error::OK,
core_.ExistingClientRequest(this, client_id_, ciphertext));
state_ = State::AWAITING_AVAILABLE;
}
void TestingClient::RequestRestore(PIN pin) {
LOG(INFO) << "sending restore request";
client::Request req;
auto b = req.mutable_restore();
b->set_pin(ByteArrayToString(pin));
// serialize and encrypt
std::string req_str;
ASSERT_TRUE(req.SerializeToString(&req_str));
auto [ciphertext, encrypt_err] = noise::Encrypt(tx_.get(), req_str);
ASSERT_EQ(error::OK, encrypt_err);
ASSERT_EQ(error::OK,
core_.ExistingClientRequest(this, client_id_, ciphertext));
state_ = State::AWAITING_RESTORE;
}
void TestingClient::HandleNewClientReply(NewClientReply ncr) {
client_id_ = ncr.client_id();
ASSERT_GT(client_id_, 0ul);
LOG(VERBOSE) << "new client " << client_id_;
auto hsp = hs_.get();
auto hs_msg = ncr.handshake_start();
NOISE_OK(noise_dhstate_set_public_key(
noise_handshakestate_get_remote_public_key_dh(hsp),
noise::StrU8Ptr(hs_msg.test_only_pubkey()),
hs_msg.test_only_pubkey().size()));
NOISE_OK(noise_handshakestate_start(hsp));
ASSERT_EQ(NOISE_ACTION_WRITE_MESSAGE, noise_handshakestate_get_action(hsp));
// Now pass a message to complete the handshake
std::string data;
data.resize(noise::HANDSHAKE_INIT_SIZE, '\0');
NoiseBuffer write_buf = noise::BufferOutputFromString(&data);
NOISE_OK(noise_handshakestate_write_message(hsp, &write_buf, nullptr));
data.resize(write_buf.size, '\0');
core_.ExistingClientRequest(this, client_id_, data);
// now we wait for the existing client reply to finish the handshake
}
void TestingClient::FinishHandshake(ExistingClientReply ecr) {
LOG(VERBOSE) << "finish handshake client: " << client_id_;
auto hsp = hs_.get();
NoiseCipherState* txp;
NoiseCipherState* rxp;
ASSERT_EQ(NOISE_ACTION_READ_MESSAGE, noise_handshakestate_get_action(hsp));
NoiseBuffer read_buf = noise::BufferInputFromString(ecr.mutable_data());
NOISE_OK(noise_handshakestate_read_message(hsp, &read_buf, nullptr));
ASSERT_EQ(NOISE_ACTION_SPLIT, noise_handshakestate_get_action(hsp));
NOISE_OK(noise_handshakestate_split(hsp, &txp, &rxp));
tx_ = noise::WrapCipherState(txp);
rx_ = noise::WrapCipherState(rxp);
state_ = State::READY;
}
void TestingClient::DecryptClientReply(ExistingClientReply ecr,
client::Response* rsp) {
auto [plaintext, decrypt_err] = noise::Decrypt(rx_.get(), ecr.data());
ASSERT_EQ(error::OK, decrypt_err);
ASSERT_TRUE(rsp->ParseFromString(plaintext));
}
void TestingClient::HandleBackupResponse(ExistingClientReply ecr) {
client::Response response;
DecryptClientReply(ecr, &response);
ASSERT_EQ(response.inner_case(), client::Response::kBackup);
backup_response_ = response.backup();
state_ = State::BACKUP_READY;
}
void TestingClient::HandleExposeResponse(ExistingClientReply ecr) {
client::Response response;
DecryptClientReply(ecr, &response);
ASSERT_EQ(response.inner_case(), client::Response::kExpose);
expose_response_ = response.expose();
state_ = State::AVAILABLE_READY;
}
void TestingClient::HandleRestoreResponse(ExistingClientReply ecr) {
client::Response response;
DecryptClientReply(ecr, &response);
ASSERT_EQ(response.inner_case(), client::Response::kRestore);
restore_response_ = response.restore();
state_ = State::RESTORE_READY;
}
void TestingClient::HandleExistingClientReply(ExistingClientReply ecr) {
LOG(VERBOSE) << "state_: "
<< static_cast<std::underlying_type<State>::type>(state_);
switch (state_) {
case State::HANDSHAKING:
return FinishHandshake(ecr);
case State::AWAITING_BACKUP:
return HandleBackupResponse(ecr);
case State::AWAITING_RESTORE:
return HandleRestoreResponse(ecr);
case State::AWAITING_AVAILABLE:
return HandleExposeResponse(ecr);
default:
CHECK(false);
}
}
}; // namespace svr2::core::test

View File

@ -0,0 +1,81 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_CORE_CORETEST_CLIENT_H__
#define __SVR2_CORE_CORETEST_CLIENT_H__
#include <array>
#include <string>
#include "db/db.h" // for BACKUP_ID_SIZE
#include "noise/noise.h"
#include "proto/client.pb.h"
namespace svr2::core::test {
class TestingCore;
class TestingClient {
public:
using PIN = std::array<uint8_t, 32>;
using SecretData = std::array<uint8_t, 48>;
client::BackupResponse* get_backup_response() {
return state_ == State::BACKUP_READY ? &backup_response_ : nullptr;
}
client::RestoreResponse* get_restore_response() {
return state_ == State::RESTORE_READY ? &restore_response_ : nullptr;
}
client::ExposeResponse* get_expose_response() {
return state_ == State::AVAILABLE_READY ? &expose_response_ : nullptr;
}
// These functions return void so that we can use gtest assertions inside
// them. (gtest asertions that generate a fatal failure can only be used with
// void-returning functions:
// https://chromium.googlesource.com/external/github.com/google/googletest/+/HEAD/docs/advanced.md#assertion-placement)
void RequestHandshake();
void RequestBackup(SecretData data, PIN pin, uint32_t tries);
void RequestExpose(SecretData data);
void RequestRestore(PIN pin);
void HandleNewClientReply(NewClientReply ncr);
void HandleExistingClientReply(ExistingClientReply ecr);
TestingClient(TestingCore& core, const std::string& authenticated_id);
private:
enum class State {
NO_HANDSHAKE,
HANDSHAKING,
READY,
AWAITING_BACKUP,
AWAITING_RESTORE,
AWAITING_AVAILABLE,
BACKUP_READY,
RESTORE_READY,
AVAILABLE_READY
};
void FinishHandshake(ExistingClientReply ecr);
void HandleBackupResponse(ExistingClientReply ecr);
void HandleExposeResponse(ExistingClientReply ecr);
void HandleRestoreResponse(ExistingClientReply ecr);
void DecryptClientReply(ExistingClientReply ecr, client::Response* rsp);
TestingCore& core_;
std::string client_authenticated_id_;
uint64_t client_id_{0};
State state_{State::NO_HANDSHAKE};
noise::HandshakeState hs_;
noise::CipherState tx_;
noise::CipherState rx_;
client::BackupResponse backup_response_;
client::RestoreResponse restore_response_;
client::ExposeResponse expose_response_;
};
}; // namespace svr2::core::test
#endif // __SVR2_CORE_CORETEST_CLIENT_H__

View File

@ -0,0 +1,290 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "testingcore.h"
#include <gtest/gtest.h>
#include "util/bytes.h"
#include "replicagroup.h"
#include "testingclient.h"
namespace svr2::core::test {
TestingCore::TestingCore(ReplicaGroup& replica_group)
: replica_group_(replica_group) {
context::Context ctx;
enclaveconfig::InitConfig cfg = replica_group.get_init_config();
cfg.set_initial_timestamp_unix_secs(timer_secs_);
auto [core, err] = Core::Create(&ctx, cfg);
if (err != error::OK) {
LOG(ERROR) << "Could not create core: " << err;
CHECK(false);
}
core_ = std::move(core);
}
error::Error TestingCore::ProcessIncomingMessage() {
error::Error result = error::OK;
if (!active() || input_messages_.empty()) {
return result;
}
// send the commands and other messages to the enclave
LOG(VERBOSE) << "Core " << ID() << " processing first of "
<< input_messages_.size() << " messages";
context::Context ctx;
// take the input message
auto msg = std::move(input_messages_.front());
input_messages_.pop_front();
auto err = core_->Receive(&ctx, msg);
if (err != error::OK) {
// clear the messages and return error
env::test::SentMessages();
return err;
}
// get the responses
auto response_msgs = env::test::SentMessages();
// process according to type
peerid::PeerID to;
PeerMessage peer_msg;
for (auto& response : response_msgs) {
switch (response.inner_case()) {
case EnclaveMessage::kPeerMessage:
peer_msg = std::move(*response.mutable_peer_message());
// read who this message is *to*
to.FromString(peer_msg.peer_id());
// Now reset the peer_id in the message to our ID so the
// recipient knows who it is *from*
ID().ToString(peer_msg.mutable_peer_id());
peer_messages_out_[to].emplace_back(std::move(peer_msg));
break;
case EnclaveMessage::kH2EResponse:
h2e_responses_out_.emplace_back(response.h2e_response());
break;
default:
CHECK(false);
}
}
return error::OK;
}
error::Error TestingCore::ProcessAllIncomingMessages() {
while (!input_messages_.empty()) {
RETURN_IF_ERROR(ProcessIncomingMessage());
}
return error::OK;
}
error::Error TestingCore::ProcessNextH2EResponse() {
auto h2e_response = std::move(h2e_responses_out_.front());
h2e_responses_out_.pop_front();
auto request_id = h2e_response.request_id();
auto cl = open_client_requests_[request_id];
switch (h2e_response.inner_case()) {
case HostToEnclaveResponse::kStatus:
if (error::OK != h2e_response.status()) {
LOG(DEBUG) << ID() << " response for request " << request_id << " error: " << h2e_response.status();
return h2e_response.status();
}
break;
case HostToEnclaveResponse::kNewClientReply:
cl->HandleNewClientReply(h2e_response.new_client_reply());
break;
case HostToEnclaveResponse::kExistingClientReply:
cl->HandleExistingClientReply(h2e_response.existing_client_reply());
break;
case HostToEnclaveResponse::kGetEnclaveStatusReply:
break;
default:
CHECK(false);
}
return error::OK;
}
error::Error TestingCore::ProcessAllH2EResponses() {
while (!h2e_responses_out_.empty()) {
RETURN_IF_ERROR(ProcessNextH2EResponse());
}
return error::OK;
}
error::Error TestingCore::AddPeerMessage(PeerMessage&& peer_message) {
if (state_ == State::ACTIVE || state_ == State::PAUSED_SAVE_MSGS) {
peerid::PeerID other_id;
other_id.FromString(peer_message.peer_id());
LOG(VERBOSE) << " core " << ID() << " receiving message from " << other_id;
::svr2::UntrustedMessage req;
*req.mutable_peer_message() = std::move(peer_message);
input_messages_.emplace_back(std::move(req));
}
return error::OK;
}
error::Error TestingCore::ForwardOutgoingMessages() {
for (auto& [to, msgs] : peer_messages_out_) {
for (auto& msg : msgs) {
RETURN_IF_ERROR(replica_group_.SendMessage(to, msg));
}
}
peer_messages_out_.clear();
return error::OK;
}
error::Error TestingCore::ResetPeer(peerid::PeerID peer_id) {
LOG(VERBOSE) << "resetpeerreq " << core_->ID() << " -> " << peer_id;
UntrustedMessage msg;
auto reset_req = msg.mutable_reset_peer();
peer_id.ToString(reset_req->mutable_peer_id());
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::PingPeer(peerid::PeerID peer_id) {
LOG(VERBOSE) << "pingreq " << core_->ID() << " -> " << peer_id;
UntrustedMessage msg;
auto host = msg.mutable_h2e_request();
host->set_request_id(next_request_id());
peer_id.ToString(host->mutable_ping_peer()->mutable_peer_id());
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::GetEnclaveStatus() {
LOG(VERBOSE) << "getenclavestatus " << core_->ID();
UntrustedMessage msg;
auto host = msg.mutable_h2e_request();
host->set_request_id(next_request_id());
host->set_get_enclave_status(true);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::TimerTick() {
++timer_secs_;
LOG(VERBOSE) << "timertick " << core_->ID() << " secs: " << timer_secs_;
UntrustedMessage msg;
msg.mutable_timer_tick()->set_new_timestamp_unix_secs(timer_secs_);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::CreateNewRaftGroup() {
LOG(VERBOSE) << "createnewraftgroup " << core_->ID();
UntrustedMessage msg;
auto host = msg.mutable_h2e_request();
host->set_request_id(next_request_id());
host->set_create_new_raft_group(true);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::JoinRaft(peerid::PeerID peer_id) {
if (!peer_id.Valid()) {
return error::Peers_InvalidID;
}
LOG(VERBOSE) << "joinraftreq " << core_->ID() << " -> " << peer_id;
UntrustedMessage msg;
auto host = msg.mutable_h2e_request();
host->set_request_id(next_request_id());
auto req = host->mutable_join_raft();
peer_id.ToString(req->mutable_peer_id());
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::RequestVoting() {
LOG(VERBOSE) << "requestvoting " << core_->ID();
UntrustedMessage msg;
auto host = msg.mutable_h2e_request();
host->set_request_id(next_request_id());
host->set_request_voting(true);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::Reconfigure(const enclaveconfig::EnclaveConfig& config) {
LOG(VERBOSE) << "reconfigure " << core_->ID();
config_ = config;
UntrustedMessage msg;
auto host = msg.mutable_h2e_request();
host->set_request_id(next_request_id());
host->mutable_reconfigure()->MergeFrom(config);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::RaftRemoval() {
LOG(VERBOSE) << "raft_removal " << core_->ID();
UntrustedMessage msg;
auto host = msg.mutable_h2e_request();
host->set_request_id(next_request_id());
host->set_request_removal(true);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::DeleteBackup(const std::string& client_authenticated_id) {
LOG(VERBOSE) << "deletebackup " << core_->ID();
UntrustedMessage msg;
auto host = msg.mutable_h2e_request();
host->set_request_id(next_request_id());
client::Request delete_;
delete_.mutable_delete_();
CHECK(delete_.SerializeToString(host->mutable_database_request()->mutable_request()));
host->mutable_database_request()->set_authenticated_id(client_authenticated_id);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
error::Error TestingCore::NewClientRequest(
TestingClient* client, std::string client_authenticated_id) {
LOG(VERBOSE) << "newclient " << core_->ID();
UntrustedMessage msg;
auto h2e_req = msg.mutable_h2e_request();
auto new_client_req = h2e_req->mutable_new_client();
auto request_id = next_request_id();
open_client_requests_[request_id] = client;
h2e_req->set_request_id(request_id);
new_client_req->set_client_authenticated_id(client_authenticated_id);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
// Backup or Restore
error::Error TestingCore::ExistingClientRequest(TestingClient* client,
uint64_t client_id,
std::string data) {
LOG(VERBOSE) << "existingclient " << core_->ID();
UntrustedMessage msg;
auto h2e_req = msg.mutable_h2e_request();
auto existing_client_req = h2e_req->mutable_existing_client();
auto request_id = next_request_id();
open_client_requests_[request_id] = client;
h2e_req->set_request_id(request_id);
existing_client_req->set_client_id(client_id);
existing_client_req->set_data(data);
input_messages_.emplace_back(std::move(msg));
return error::OK;
}
EnclaveReplicaStatus TestingCore::TakeExpectedEnclaveStatusReply() {
auto& h2e_response = h2e_responses_out_[0];
EXPECT_EQ(h2e_response.inner_case(), HostToEnclaveResponse::kGetEnclaveStatusReply);
auto result = std::move(h2e_response.get_enclave_status_reply());
h2e_responses_out_.pop_front();
return result;
}
}; // namespace svr2::core::test

View File

@ -0,0 +1,124 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_CORE_CORETEST_TESTINGCORE_H__
#define __SVR2_CORE_CORETEST_TESTINGCORE_H__
#include <deque>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "core/core.h"
#include "env/test/test.h"
#include "proto/enclaveconfig.pb.h"
#include "proto/error.pb.h"
#include "proto/msgs.pb.h"
#include "util/log.h"
#include "proto/client.pb.h"
namespace svr2::core::test {
class TestingCore;
class ReplicaGroup;
class TestingClient;
using RequestID = uint64_t;
using TestingCoreMap = std::map<peerid::PeerID, TestingCore*>;
using PeerMessageMap = std::map<peerid::PeerID, std::vector<PeerMessage>>;
using OpenClientRequests = std::map<RequestID, TestingClient*>;
/*
This class wraps the basic actions of a `Core` and plays much
of the role the host plays in a real deployment - wrapping requests,
forwarding messages to peers and clients, etc.
*/
class TestingCore {
enum class State { ACTIVE, PAUSED_SAVE_MSGS, PAUSED_DROP_MSGS, STOPPED };
public:
TestingCore(ReplicaGroup& replica_group);
error::Error Init() { return error::OK; }
uint64_t next_request_id() { return ++(next_request_id_); }
peerid::PeerID ID() const { return core_->ID(); }
const std::map<peerid::PeerID, std::vector<PeerMessage>>& peer_messages_out()
const {
return peer_messages_out_;
}
const std::deque<HostToEnclaveResponse>& host_to_enclave_responses() const {
return h2e_responses_out_;
}
std::deque<HostToEnclaveResponse> take_host_to_enclave_responses() {
return std::move(h2e_responses_out_);
}
const std::deque<UntrustedMessage>& input_messages() const {
return input_messages_;
}
bool leader() const { return core_->leader() && active(); }
bool serving() const { return core_->serving() && active(); }
bool voting() const { return core_->voting() && active(); }
bool active() const { return state_ == State::ACTIVE; }
size_t num_voting() const { return core_->num_voting(); }
size_t num_serving() const { return core_->num_members(); }
std::set<peerid::PeerID> all_replicas() const { return core_->all_replicas(); }
void Stop() { state_ = State::STOPPED; }
void Pause(bool drop_msgs) {
state_ = drop_msgs ? State::PAUSED_DROP_MSGS : State::PAUSED_SAVE_MSGS;
}
void Reactivate() { state_ = State::ACTIVE; }
error::Error ProcessIncomingMessage();
error::Error ProcessAllIncomingMessages();
error::Error ForwardOutgoingMessages();
error::Error ProcessNextH2EResponse();
error::Error ProcessAllH2EResponses();
// Host to Enclave commands
error::Error ResetPeer(peerid::PeerID peer_id);
error::Error PingPeer(peerid::PeerID peer_id);
error::Error GetEnclaveStatus();
error::Error TimerTick();
error::Error CreateNewRaftGroup();
error::Error JoinRaft(peerid::PeerID peer_id);
error::Error RequestVoting();
error::Error Reconfigure(const enclaveconfig::EnclaveConfig& config);
error::Error DeleteBackup(const std::string& client_authenticated_id);
error::Error RaftRemoval();
// Peer communication
error::Error AddPeerMessage(PeerMessage&& peer_message);
// Client communication
// handshake
error::Error NewClientRequest(TestingClient* client,
std::string client_authenticated_id);
// Backup or Restore
error::Error ExistingClientRequest(TestingClient* client, uint64_t client_id,
std::string data);
EnclaveReplicaStatus TakeExpectedEnclaveStatusReply();
private:
std::unique_ptr<Core> core_;
ReplicaGroup& replica_group_;
enclaveconfig::EnclaveConfig config_;
std::deque<UntrustedMessage> input_messages_;
std::deque<HostToEnclaveResponse> h2e_responses_out_;
PeerMessageMap peer_messages_out_;
OpenClientRequests open_client_requests_;
uint64_t next_request_id_{0};
uint64_t timer_secs_{1};
State state_{State::ACTIVE};
};
}; // namespace svr2::core::test
#endif // __SVR2_CORE_CORETEST_TESTINGCORE_H__

76
enclave/core/internal.h Normal file
View File

@ -0,0 +1,76 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_CORE_INTERNAL_H__
#define __SVR2_CORE_INTERNAL_H__
#include <mutex>
#include "raft/log.h"
#include "raft/raft.h"
#include "db/db.h"
#include "proto/e2e.pb.h"
#include "proto/msgs.pb.h"
#include "proto/raft.pb.h"
namespace svr2::core::internal {
typedef uint64_t TransactionID;
struct WaitingForFirstConnection {
peerid::PeerID peer;
TransactionID join_tx;
};
struct Loading {
enclaveconfig::RaftGroupConfig group_config;
raft::ReplicaGroup replica_group;
std::unique_ptr<raft::Log> log;
std::unique_ptr<db::DB> db;
std::unique_ptr<raft::Membership> mem;
peerid::PeerID load_from;
TransactionID join_tx;
bool started;
uint64_t replication_id;
uint64_t replication_sequence;
std::string lexigraphically_largest_row_loaded_into_db;
};
struct Loaded {
enclaveconfig::RaftGroupConfig group_config;
std::unique_ptr<raft::Raft> raft;
std::unique_ptr<db::DB> db;
raft::LogIdx db_last_applied_log;
};
struct Raft {
Raft() { ClearState(); }
void ClearState() REQUIRES(mu) {
state = svr2::RAFTSTATE_NO_STATE;
waiting_for_first_connection = {
.peer = peerid::PeerID(),
.join_tx = 0,
};
loading = {
.group_config = enclaveconfig::RaftGroupConfig(),
.replica_group = raft::ReplicaGroup(),
.log = nullptr,
.db = nullptr,
.join_tx = 0,
.started = false,
.replication_sequence = 0,
.lexigraphically_largest_row_loaded_into_db = "",
};
loaded = {
.group_config = enclaveconfig::RaftGroupConfig(),
.raft = nullptr,
.db = nullptr,
.db_last_applied_log = 0,
};
}
mutable util::mutex mu; // protects everything else in this struct.
RaftState state GUARDED_BY(mu);
WaitingForFirstConnection waiting_for_first_connection GUARDED_BY(mu);
Loading loading GUARDED_BY(mu);
Loaded loaded GUARDED_BY(mu);
};
} // namespace svr2::core::internal
#endif // __SVR2_CORE_INTERNAL_H__

2306
enclave/core/tests/core.cc Normal file

File diff suppressed because it is too large Load Diff

27
enclave/db/db.cc Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "db/db.h"
#include "db/db2.h"
#include "db/db3.h"
#include <memory>
namespace svr2::db {
std::unique_ptr<DB> DB::New(enclaveconfig::DatabaseVersion version) {
std::unique_ptr<db::DB> out;
switch (version) {
case enclaveconfig::DATABASE_VERSION_SVR2:
out.reset(new db::DB2());
break;
case enclaveconfig::DATABASE_VERSION_SVR3:
out.reset(new db::DB3());
break;
default:
return nullptr;
}
return out;
}
} // namespace svr2::db

122
enclave/db/db.h Normal file
View File

@ -0,0 +1,122 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_DB_DB_H__
#define __SVR2_DB_DB_H__
#include <map>
#include <array>
#include <google/protobuf/message_lite.h>
#include "proto/error.pb.h"
#include "proto/e2e.pb.h"
#include "proto/msgs.pb.h"
#include "sip/hasher.h"
#include "context/context.h"
#include "util/log.h"
namespace svr2::db {
// DB provides a generic interface for databases, which can be used by both
// SVR2 (db2.*) and SVR3 (db3.*). These two databases take in different
// requests and return different responses, which are packaged in the
// DB::Protocol interface and implemented per-database.
//
// A database uses three objects during its lifecycle:
// - Request: a protobuf created and provided by a (remote) client
// - Log: generated from a `Request` and contains the operation to be performed
// - Respose: returned to the (remote) client detailing the output of the operation
//
// In many cases, the Request and Log will be similar, and often the Request
// is simply embedded into the Log. However, the Log generally contains a
// few other key pieces of information:
// - the database key associated with the request/authenticated_id, if there is one
// - any information (entropy, timestamps, etc) which could differ if recomputed
// across different replicas.
//
// Generally, the lifecycle of a request is:
// - the Request is received by one replica
// - that replica uses it to generate a Log
// - that Log is submitted to Raft for ordering and persistence
// - Raft commits the Log
// - the Log is then applied to the database via the Run method
// - the Run method generates a Response
// - on the replica that received the Request, the Response is returned to the client
class DB {
public:
DELETE_COPY_AND_ASSIGN(DB);
DB() {}
virtual ~DB() {}
// Returns a database based on the passed-in version number.
static std::unique_ptr<DB> New(enclaveconfig::DatabaseVersion version);
typedef google::protobuf::MessageLite Request;
typedef google::protobuf::MessageLite Log;
typedef google::protobuf::MessageLite Response;
// Protocol encapsulates typing requests and responses for clients.
class Protocol {
public:
// RequestPB creates a new request protobuf in the scope of `ctx`
virtual Request* RequestPB(context::Context* ctx) const = 0;
// LogPB creates a new log protobuf in the scope of `ctx`
virtual Log* LogPB(context::Context* ctx) const = 0;
// Given a request, creates a log. Note that this potentially std::move's
// the request into the log, so care should be taken to not use the request
// after calling LogPBFromRequest.
virtual std::pair<Log*, error::Error> LogPBFromRequest(
context::Context* ctx,
Request&& request,
const std::string& authenticated_id) const = 0;
// LogKey returns the database key associated with the given request proto.
virtual const std::string& LogKey(const Log& r) const = 0;
// Validate that a log has the right shape, size, etc.
virtual error::Error ValidateClientLog(const Log& log) const = 0;
// Returns the maximum size of a database row when serialized.
virtual size_t MaxRowSerializedSize() const = 0;
};
// P() returns a pointer to a _static_ Protocol object,
// which will outlast the DB object.
virtual const Protocol* P() const = 0;
// Run a client log request and yield a response.
// The client log should already have been checked with ValidateClientLog;
// failing to do so will CHECK-fail.
// It's assumed that validation happens on Raft log insert, so that
// outputs from the Raft log are already validated.
//
// Output response is valid within the passed-in context.
virtual Response* Run(context::Context* ctx, const Log& log) = 0;
// Get rows from this database in range (exclusive_start, ...], returning
// no more than [size] rows. If it returns <[size] rows, the end of the database
// has been reached. Pass in empty string to start with the first key in
// the database. Returns the key of the largest returned row.
virtual std::pair<std::string, error::Error> RowsAsProtos(
context::Context* ctx,
const std::string& exclusive_start,
size_t size,
google::protobuf::RepeatedPtrField<std::string>* out) const = 0;
// Update this database using the given database row states.
// This will return an error if any of the DatabaseRowStates contain
// rows that already exist within the database. Rows must be lexigraphically
// larger than any existing row in the database. Returns the row key
// of the last row inserted into the database, on success.
virtual std::pair<std::string, error::Error> LoadRowsFromProtos(
context::Context* ctx,
const google::protobuf::RepeatedPtrField<std::string>& rows) = 0;
// Compute a hash of the entire database. This is not designed to
// be useful for security-focussed integrity checking, but should be
// sufficient to verify that replicated data matches up between source
// and destination.
virtual std::array<uint8_t, 32> Hash(context::Context* ctx) const = 0;
// Get the number of backups stored in the database
virtual size_t row_count() const = 0;
};
} // namespace svr2::db
#endif // __SVR2_DB_DB_H__

342
enclave/db/db2.cc Normal file
View File

@ -0,0 +1,342 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "db/db2.h"
#include <algorithm>
#include <sodium/crypto_auth_hmacsha256.h>
#include "util/log.h"
#include "util/bytes.h"
#include "util/hex.h"
#include "util/constant.h"
#include "util/endian.h"
#include "context/context.h"
#include "metrics/metrics.h"
#include "proto/clientlog.pb.h"
namespace svr2::db {
template <class T>
static void CopyArrayToString(const T& array, std::string* out) {
CHECK(array.size() == 0 || sizeof(array[0]) == 1);
out->resize(array.size());
std::copy(array.cbegin(), array.cend(), out->begin());
}
static size_t SMALL_BYTES_FIELD_EXTRA_PROTO_METADATA = 2;
static size_t U16_AS_VARINT_MAX_SIZE = 3;
size_t DB2::Protocol::MaxRowSerializedSize() const {
return
BACKUP_ID_SIZE + SMALL_BYTES_FIELD_EXTRA_PROTO_METADATA +
MAX_DATA_SIZE + SMALL_BYTES_FIELD_EXTRA_PROTO_METADATA +
PIN_SIZE + SMALL_BYTES_FIELD_EXTRA_PROTO_METADATA +
U16_AS_VARINT_MAX_SIZE; // max bytes for TRIES
}
DB::Request* DB2::Protocol::RequestPB(context::Context* ctx) const {
return ctx->Protobuf<client::Request>();
}
DB::Log* DB2::Protocol::LogPB(context::Context* ctx) const {
return ctx->Protobuf<client::Log2>();
}
std::pair<DB::Log*, error::Error> DB2::Protocol::LogPBFromRequest(
context::Context* ctx,
Request&& request,
const std::string& authenticated_id) const {
auto r = dynamic_cast<client::Request*>(&request);
if (r == nullptr) {
return std::make_pair(nullptr, COUNTED_ERROR(DB2_InvalidRequestType));
}
auto log = ctx->Protobuf<client::Log2>();
if (authenticated_id.size() != BACKUP_ID_SIZE) {
return std::make_pair(nullptr, COUNTED_ERROR(DB2_ClientBackupIDSize));
}
log->set_backup_id(authenticated_id);
*log->mutable_req() = std::move(*r);
return std::make_pair(log, error::OK);
}
const std::string& DB2::Protocol::LogKey(const DB::Log& req) const {
auto r = dynamic_cast<const client::Log2*>(&req);
CHECK(r != nullptr);
return r->backup_id();
}
error::Error DB2::Protocol::ValidateClientLog(const DB::Log& req_pb) const {
auto log = dynamic_cast<const client::Log2*>(&req_pb);
if (log == nullptr) { return COUNTED_ERROR(DB2_InvalidRequestType); }
auto req = log->req();
if (log->backup_id().size() != BACKUP_ID_SIZE) { return COUNTED_ERROR(DB2_ClientBackupIDSize); }
switch (req.inner_case()) {
case client::Request::kBackup: {
auto r = req.backup();
if (r.pin().size() != PIN_SIZE) { return COUNTED_ERROR(DB2_ClientPinSize); }
if (r.data().size() > MAX_DATA_SIZE) { return COUNTED_ERROR(DB2_ClientDataSize); }
if (r.data().size() < MIN_DATA_SIZE) { return COUNTED_ERROR(DB2_ClientDataSize); }
if (r.max_tries() > MAX_ALLOWED_MAX_TRIES) { return COUNTED_ERROR(DB2_ClientTriesTooHigh); }
if (r.max_tries() < MIN_ALLOWED_MAX_TRIES) { return COUNTED_ERROR(DB2_ClientTriesZero); }
} break;
case client::Request::kRestore: {
auto r = req.restore();
if (r.pin().size() != PIN_SIZE) { return COUNTED_ERROR(DB2_ClientPinSize); }
} break;
case client::Request::kDelete: {
auto r = req.delete_();
} break;
case client::Request::kExpose: {
auto r = req.expose();
if (r.data().size() > MAX_DATA_SIZE) { return COUNTED_ERROR(DB2_ClientDataSize); }
if (r.data().size() < MIN_DATA_SIZE) { return COUNTED_ERROR(DB2_ClientDataSize); }
} break;
default:
return COUNTED_ERROR(DB2_ClientRequestCase);
}
return error::OK;
}
const DB::Protocol* DB2::P() const {
static DB2::Protocol rr;
return &rr;
}
DB::Response* DB2::Run(context::Context* ctx, const DB::Log& log_pb) {
// We CHECK here because this should have already been validated when it
// was added to the Raft log.
MEASURE_CPU(ctx, cpu_db_client_request);
CHECK(error::OK == P()->ValidateClientLog(log_pb));
auto log = reinterpret_cast<const client::Log2&>(log_pb); // dynamic_cast checked in ValidateClientLog.
BackupID id;
CHECK(log.backup_id().size() == id.size());
std::copy(log.backup_id().begin(), log.backup_id().end(), id.begin());
auto resp = ctx->Protobuf<client::Response>();
switch (log.req().inner_case()) {
case client::Request::kBackup:
Backup(id, log.req().backup(), resp->mutable_backup());
break;
case client::Request::kRestore:
Restore(id, log.req().restore(), resp->mutable_restore());
break;
case client::Request::kDelete:
Delete(id, log.req().delete_(), resp->mutable_delete_());
break;
case client::Request::kExpose:
Expose(id, log.req().expose(), resp->mutable_expose());
break;
default:
LOG(WARNING) << "unsupported request case, returning empty response";
break;
}
return resp;
}
void DB2::Row::Clear(e2e::DB2RowState::State s) {
memset(data.begin(), 0, data.size());
memset(pin.begin(), 0, pin.size());
tries = 0;
data_size = 0;
state = s;
}
void DB2::Backup(const BackupID& id, const client::BackupRequest& req, client::BackupResponse* resp) {
std::map<std::array<uint8_t, BACKUP_ID_SIZE>, Row>::iterator find = rows_.find(id);
if (find == rows_.end()) {
auto e = rows_.emplace(
std::piecewise_construct,
std::forward_as_tuple(std::move(id)),
std::forward_as_tuple());
find = e.first;
GAUGE(db, rows)->Set(rows_.size());
}
Row* row = &find->second;
row->Clear(e2e::DB2RowState::POPULATED);
std::copy(req.data().begin(), req.data().end(), row->data.begin());
row->data_size = req.data().size();
row->tries = req.max_tries();
std::copy(req.pin().begin(), req.pin().end(), row->pin.begin());
resp->set_status(client::BackupResponse::OK);
}
void DB2::Restore(const BackupID& id, const client::RestoreRequest& req, client::RestoreResponse* resp) {
auto find = rows_.find(id);
if (find == rows_.end() || find->second.state != e2e::DB2RowState::AVAILABLE) {
resp->set_status(client::RestoreResponse::MISSING);
return;
}
Row* row = &find->second;
if (util::ConstantTimeEquals(req.pin(), row->pin)) {
resp->set_status(client::RestoreResponse::OK);
resp->set_tries(row->tries);
*resp->mutable_data() = std::string(row->data.begin(), row->data.begin() + row->data_size);
return;
}
if (--row->tries == 0) {
// We Clear before erasing because erasing just removes the entry from the log, and
// we want to actually zero out the secret wherever it is in memory.
row->Clear(e2e::DB2RowState::UNINITIATED);
rows_.erase(find);
resp->set_status(client::RestoreResponse::MISSING);
GAUGE(db, rows)->Set(rows_.size());
return;
}
resp->set_status(client::RestoreResponse::PIN_MISMATCH);
resp->set_tries(row->tries);
}
void DB2::Delete(const BackupID& id, const client::DeleteRequest& req, client::DeleteResponse* resp) {
auto find = rows_.find(id);
if (find == rows_.end()) { return; }
// We Clear before erasing because erasing just removes the entry from the log, and
// we want to actually zero out the secret wherever it is in memory.
find->second.Clear(e2e::DB2RowState::UNINITIATED);
rows_.erase(find);
GAUGE(db, rows)->Set(rows_.size());
}
void DB2::Expose(const BackupID& id, const client::ExposeRequest& req, client::ExposeResponse* resp) {
// Expose provides a 2-phase commit of backups, to avoid client backup
// retries from allowing server operators infinite guesses against the pin.
// Without Expose, the following attack is possible:
// 1. client sends BackupRequest
// 2. server processes BackupRequest
// 3. server operator drops connection to client before BackupResponse is sent
// 4. server operator makes max_tries guesses against backup
// 5. client retries BackupRequest (goto 1)
//
// The Expose proto must contain the secret to make sure that only someone
// that already knows the secret (IE: the client) can expose the backup for
// restores. Otherwise, the following attack is possible:
// 1. client sends BackupRequest
// 2. server processes BackupRequest
// 3. server operator drops connection to client before BackupResponse is sent
// 4. server operator sends ExposeRequest to enclave, which processes it
// 5. server operator makes max_tries guesses against backup
// 6. client retries BackupRequest (goto 1)
auto find = rows_.find(id);
if (find == rows_.end()) {
resp->set_status(client::ExposeResponse::ERROR);
return;
}
Row* row = &find->second;
if (!util::ConstantTimeEqualsPrefix(row->data, req.data(), row->data_size)) {
resp->set_status(client::ExposeResponse::ERROR);
return;
}
switch (row->state) {
case e2e::DB2RowState::POPULATED:
case e2e::DB2RowState::AVAILABLE:
row->state = e2e::DB2RowState::AVAILABLE;
resp->set_status(client::ExposeResponse::OK);
return;
default:
resp->set_status(client::ExposeResponse::ERROR);
return;
}
}
std::pair<std::string, error::Error> DB2::RowsAsProtos(context::Context* ctx, const std::string& exclusive_start, size_t size, google::protobuf::RepeatedPtrField<std::string>* out) const {
MEASURE_CPU(ctx, cpu_db_repl_send);
auto iter = rows_.begin();
if (!exclusive_start.empty()) {
auto [id, err] = BackupIDFromString(exclusive_start);
if (err != error::OK) {
return std::make_pair("", err);
}
iter = rows_.upper_bound(id);
}
auto row = ctx->Protobuf<e2e::DB2RowState>();
std::string last_id;
for (size_t i = 0; i < size && iter != rows_.end(); i++, ++iter) {
row->Clear();
CopyArrayToString(iter->first, row->mutable_backup_id());
CopyArrayToString(iter->second.data, row->mutable_data());
row->mutable_data()->resize(iter->second.data_size);
CopyArrayToString(iter->second.pin, row->mutable_pin());
row->set_tries(iter->second.tries);
row->set_state(iter->second.state);
if (!row->SerializeToString(out->Add())) {
return std::make_pair("", COUNTED_ERROR(DB2_ReplicationInvalidRow));
}
last_id = row->backup_id();
}
LOG(DEBUG) << "DB sending rows in (" << util::PrefixToHex(exclusive_start, 8) << ", " << util::PrefixToHex(last_id, 8) << "]";
return std::make_pair(last_id, error::OK);
}
DB2::Row::Row() : state(e2e::DB2RowState::UNINITIATED), tries(0), data_size(0), data{0}, pin{0} {}
std::pair<std::string, error::Error> DB2::LoadRowsFromProtos(context::Context* ctx, const google::protobuf::RepeatedPtrField<std::string>& rows) {
MEASURE_CPU(ctx, cpu_db_repl_recv);
CHECK(rows.size());
size_t initial_rows = rows_.size();
auto row = ctx->Protobuf<e2e::DB2RowState>();
for (int i = 0; i < rows.size(); i++) {
row->Clear();
if (!row->ParseFromString(rows.Get(i))) {
return std::make_pair("", COUNTED_ERROR(DB2_ReplicationInvalidRow));
}
if (row->tries() > MAX_ALLOWED_MAX_TRIES ||
row->pin().size() != PIN_SIZE ||
row->data().size() < MIN_DATA_SIZE ||
row->data().size() > MAX_DATA_SIZE) {
return std::make_pair("", COUNTED_ERROR(DB2_ReplicationInvalidRow));
}
auto [key, err] = BackupIDFromString(row->backup_id());
if (err != error::OK) {
return std::make_pair("", err);
}
if (rows_.size() && key <= rows_.rbegin()->first) {
return std::make_pair("", COUNTED_ERROR(DB2_ReplicationOutOfOrder));
}
Row r;
r.state = row->state();
std::copy(row->pin().begin(), row->pin().end(), r.pin.begin());
std::copy(row->data().begin(), row->data().end(), r.data.begin());
r.data_size = row->data().size();
r.tries = row->tries();
rows_.emplace_hint(rows_.end(), key, std::move(r));
GAUGE(db, rows)->Set(rows_.size());
}
if (rows_.size() != initial_rows + rows.size()) {
// This ensures that we didn't accidentally attempt to load rows that
// already exist within the DB.
return std::make_pair("", COUNTED_ERROR(DB2_LoadedRowsAlreadyInDB));
}
return std::make_pair(row->backup_id(), error::OK);
}
std::pair<DB2::BackupID, error::Error> DB2::BackupIDFromString(const std::string& s) {
DB2::BackupID out;
if (s.size() != BACKUP_ID_SIZE) {
return std::make_pair(std::move(out), COUNTED_ERROR(DB2_BackupIDSize));
}
std::copy(s.begin(), s.end(), out.data());
return std::make_pair(std::move(out), error::OK);
}
std::array<uint8_t, 32> DB2::Hash(context::Context* ctx) const {
MEASURE_CPU(ctx, cpu_db_hash);
crypto_hash_sha256_state sha;
crypto_hash_sha256_init(&sha);
uint8_t num[8];
util::BigEndian64Bytes(rows_.size(), num);
crypto_hash_sha256_update(&sha, num, sizeof(num));
for (auto iter = rows_.cbegin(); iter != rows_.cend(); ++iter) {
util::BigEndian64Bytes(iter->second.state, num);
crypto_hash_sha256_update(&sha, num, sizeof(num));
crypto_hash_sha256_update(&sha, iter->first.data(), iter->first.size());
util::BigEndian64Bytes(iter->second.tries, num);
crypto_hash_sha256_update(&sha, num, sizeof(num));
crypto_hash_sha256_update(&sha, iter->second.data.data(), iter->second.data_size);
crypto_hash_sha256_update(&sha, iter->second.pin.data(), iter->second.pin.size());
}
std::array<uint8_t, 32> out;
crypto_hash_sha256_final(&sha, out.data());
return out;
}
} // namespace svr2::db

113
enclave/db/db2.h Normal file
View File

@ -0,0 +1,113 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_DB_DB2_H__
#define __SVR2_DB_DB2_H__
#include <map>
#include <array>
#include "proto/error.pb.h"
#include "proto/e2e.pb.h"
#include "sip/hasher.h"
#include "context/context.h"
#include "util/log.h"
#include "db/db.h"
#include "proto/client.pb.h"
namespace svr2::db {
// DB2 implements the DB interface for SVR2.
// DB is a database meant to be driven by a Raft log.
// Raft stores an ordered, consistent list of committed client::Request requests.
// This DB executes those requests as CRUD operations on an underlying ordered map,
// and returns their respective responses.
class DB2 : public DB {
public:
DELETE_COPY_AND_ASSIGN(DB2);
DB2() {}
virtual ~DB2() {}
class Protocol : public DB::Protocol {
public:
virtual Request* RequestPB(context::Context* ctx) const;
virtual Log* LogPB(context::Context* ctx) const;
virtual std::pair<Log*, error::Error> LogPBFromRequest(
context::Context* ctx,
Request&& request,
const std::string& authenticated_id) const;
virtual const std::string& LogKey(const Log& r) const;
virtual error::Error ValidateClientLog(const Log& log) const;
virtual size_t MaxRowSerializedSize() const;
};
virtual const DB::Protocol* P() const;
// Run a client log request and yield a response.
// The client log should already have been checked with ValidateClientLog;
// failing to do so will CHECK-fail.
// It's assumed that validation happens on Raft log insert, so that
// outputs from the Raft log are already validated.
//
// Output response is valid within the passed-in context.
virtual Response* Run(context::Context* ctx, const Log& request);
// Limits on sizes/etc for validation.
static const size_t BACKUP_ID_SIZE = 16;
static const size_t MIN_DATA_SIZE = 16;
static const size_t MAX_DATA_SIZE = 48;
static const size_t PIN_SIZE = 32;
static const uint16_t MAX_ALLOWED_MAX_TRIES = 255;
static const uint16_t MIN_ALLOWED_MAX_TRIES = 1;
// Get rows from this database in range (exclusive_start, ...], returning
// no more than [size] rows. If it returns <[size] rows, the end of the database
// has been reached. Pass in DB::Beginning to start with the first key in
// the database.
virtual std::pair<std::string, error::Error> RowsAsProtos(context::Context* ctx, const std::string& exclusive_start, size_t size, google::protobuf::RepeatedPtrField<std::string>* out) const;
// Update this database using the given database row states.
// This will return an error if any of the DB2RowStates contain
// rows that already exist within the database. Rows must be lexigraphically
// larger than any existing row in the database. Returns the row key
// of the last row inserted into the database, on success.
virtual std::pair<std::string, error::Error> LoadRowsFromProtos(context::Context* ctx, const google::protobuf::RepeatedPtrField<std::string>& rows);
// Compute a hash of the entire database. This is not designed to
// be useful for security-focussed integrity checking, but should be
// sufficient to verify that replicated data matches up between source
// and destination.
virtual std::array<uint8_t, 32> Hash(context::Context* ctx) const;
// Get the number of backups stored in the database
virtual size_t row_count() const { return rows_.size(); }
private:
typedef std::array<uint8_t, BACKUP_ID_SIZE> BackupID;
static std::pair<BackupID, error::Error> BackupIDFromString(const std::string& s);
struct Row {
Row();
e2e::DB2RowState::State state;
uint8_t tries;
uint8_t data_size; // should be MIN_DATA_SIZE <= data_size <= MAX_DATA_SIZE, or 0 if unset
// We use std::array here to avoid lots of extra heap allocations.
// We store slightly more data than necessary if client data is
// smaller than MAX_DATA_SIZE, but we make up for it in at least
// three 64-bit pointers if these were std::string.
std::array<uint8_t, MAX_DATA_SIZE> data;
std::array<uint8_t, PIN_SIZE> pin;
void Clear(e2e::DB2RowState::State s);
};
// Execute each of the three request types.
void Backup(const BackupID& id, const client::BackupRequest& request, client::BackupResponse* resp);
void Restore(const BackupID& id, const client::RestoreRequest& request, client::RestoreResponse* resp);
void Delete(const BackupID& id, const client::DeleteRequest& request, client::DeleteResponse* resp);
void Expose(const BackupID& id, const client::ExposeRequest& request, client::ExposeResponse* resp);
// We use std::map over std::unordered_map because order matters to us.
// We need a consistently ordered keyspace for data transfers between
// replicas.
std::map<BackupID, Row> rows_;
};
} // namespace svr2::db
#endif // __SVR2_DB_DB2_H__

298
enclave/db/db3.cc Normal file
View File

@ -0,0 +1,298 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "db/db3.h"
#include <algorithm>
#include <sodium/crypto_auth_hmacsha256.h>
#include <sodium/crypto_core_ristretto255.h>
#include "util/log.h"
#include "util/bytes.h"
#include "util/hex.h"
#include "util/constant.h"
#include "util/endian.h"
#include "context/context.h"
#include "metrics/metrics.h"
#include "proto/clientlog.pb.h"
namespace svr2::db {
DB::Request* DB3::Protocol::RequestPB(context::Context* ctx) const {
return ctx->Protobuf<client::Request3>();
}
DB::Log* DB3::Protocol::LogPB(context::Context* ctx) const {
return ctx->Protobuf<client::Log3>();
}
std::pair<DB::Log*, error::Error> DB3::Protocol::LogPBFromRequest(
context::Context* ctx,
Request&& request,
const std::string& authenticated_id) const {
auto r = dynamic_cast<client::Request3*>(&request);
if (r == nullptr) {
return std::make_pair(nullptr, COUNTED_ERROR(DB3_RequestInvalid));
}
if (authenticated_id.size() != BACKUP_ID_SIZE) {
return std::make_pair(nullptr, COUNTED_ERROR(DB3_BackupIDSize));
}
auto log = ctx->Protobuf<client::Log3>();
log->set_backup_id(authenticated_id);
*log->mutable_req() = std::move(*r);
if (log->req().inner_case() == client::Request3::kCreate) {
auto [priv, pub] = NewKeys();
log->set_create_privkey(util::ByteArrayToString(priv));
log->set_create_pubkey(util::ByteArrayToString(pub));
}
return std::make_pair(log, error::OK);
}
const std::string& DB3::Protocol::LogKey(const DB::Log& req) const {
auto r = dynamic_cast<const client::Log3*>(&req);
CHECK(r != nullptr);
return r->backup_id();
}
error::Error DB3::Protocol::ValidateClientLog(const DB::Log& log_pb) const {
auto log = dynamic_cast<const client::Log3*>(&log_pb);
if (log == nullptr) { return COUNTED_ERROR(DB3_RequestInvalid); }
if (log->backup_id().size() != BACKUP_ID_SIZE) { return COUNTED_ERROR(DB3_BackupIDSize); }
switch (log->req().inner_case()) {
case client::Request3::kCreate: {
auto r = log->req().create();
if (r.max_tries() < 1 || r.max_tries() > 255) { return COUNTED_ERROR(DB3_MaxTriesOutOfRange); }
if (r.blinded_element().size() != ELEMENT_SIZE) { return COUNTED_ERROR(DB3_BlindedElementSize); }
if (log->create_privkey().size() != sizeof(PrivateKey)) { return COUNTED_ERROR(DB3_LogPrivateKeyInvalid); }
if (log->create_pubkey().size() != sizeof(PublicKey)) { return COUNTED_ERROR(DB3_LogPublicKeyInvalid); }
} break;
case client::Request3::kEvaluate: {
auto r = log->req().evaluate();
if (r.blinded_element().size() != ELEMENT_SIZE) { return COUNTED_ERROR(DB3_BlindedElementSize); }
} break;
case client::Request3::kRemove: {
// nothing to do
} break;
default:
return COUNTED_ERROR(DB3_ToplevelRequestType);
}
return error::OK;
}
const DB::Protocol* DB3::P() const {
static DB3::Protocol rr;
return &rr;
}
size_t DB3::Protocol::MaxRowSerializedSize() const {
const size_t PROTOBUF_SMALL_STRING_EXTRA = 2; // additional bytes for serializing string
const size_t PROTOBUF_SMALL_INT = 2; // bytes for serializing a small integer
return BACKUP_ID_SIZE + PROTOBUF_SMALL_STRING_EXTRA + // backup ID
SCALAR_SIZE + PROTOBUF_SMALL_STRING_EXTRA + // priv key
PROTOBUF_SMALL_INT; // tries
}
DB::Response* DB3::Run(context::Context* ctx, const DB::Log& log_pb) {
MEASURE_CPU(ctx, cpu_db_client_request);
CHECK(P()->ValidateClientLog(log_pb) == error::OK);
auto log = dynamic_cast<const client::Log3*>(&log_pb);
CHECK(log != nullptr);
auto out = ctx->Protobuf<client::Response3>();
auto [id, err] = util::StringToByteArray<BACKUP_ID_SIZE>(log->backup_id());
CHECK(err == error::OK);
switch (log->req().inner_case()) {
case client::Request3::kCreate: {
Create(ctx, id, log->create_privkey(), log->create_pubkey(), log->req().create(), out->mutable_create());
} break;
case client::Request3::kEvaluate: {
Evaluate(ctx, id, log->req().evaluate(), out->mutable_evaluate());
} break;
case client::Request3::kRemove: {
Remove(ctx, id, log->req().remove(), out->mutable_remove());
} break;
default: CHECK(nullptr == "should never reach here, client log already validated");
}
return out;
}
std::pair<std::string, error::Error> DB3::RowsAsProtos(context::Context* ctx, const std::string& exclusive_start, size_t size, google::protobuf::RepeatedPtrField<std::string>* out) const {
MEASURE_CPU(ctx, cpu_db_repl_send);
auto iter = rows_.begin();
if (!exclusive_start.empty()) {
auto [id, err] = util::StringToByteArray<BACKUP_ID_SIZE>(exclusive_start);
if (err != error::OK) {
return std::make_pair("", err);
}
iter = rows_.upper_bound(id);
}
auto row = ctx->Protobuf<e2e::DB3RowState>();
for (size_t i = 0; i < size && iter != rows_.end(); i++, ++iter) {
row->Clear();
row->set_backup_id(util::ByteArrayToString(iter->first));
row->set_priv(util::ByteArrayToString(iter->second.priv));
row->set_tries(iter->second.tries);
if (!row->SerializeToString(out->Add())) {
return std::make_pair("", COUNTED_ERROR(DB3_ReplicationInvalidRow));
}
}
LOG(DEBUG) << "DB sending rows in (" << util::PrefixToHex(exclusive_start, 8) << ", " << util::PrefixToHex(row->backup_id(), 8) << "]";
return std::make_pair(row->backup_id(), error::OK);
}
std::pair<std::string, error::Error> DB3::LoadRowsFromProtos(context::Context* ctx, const google::protobuf::RepeatedPtrField<std::string>& rows) {
MEASURE_CPU(ctx, cpu_db_repl_recv);
size_t initial_rows = rows_.size();
auto row = ctx->Protobuf<e2e::DB3RowState>();
for (int i = 0; i < rows.size(); i++) {
row->Clear();
if (!row->ParseFromString(rows.Get(i))) {
return std::make_pair("", COUNTED_ERROR(DB3_ReplicationInvalidRow));
}
if (row->tries() > MAX_ALLOWED_MAX_TRIES ||
row->tries() < MIN_ALLOWED_MAX_TRIES) {
return std::make_pair("", COUNTED_ERROR(DB3_ReplicationInvalidRow));
}
auto [key, err1] = util::StringToByteArray<BACKUP_ID_SIZE>(row->backup_id());
if (err1 != error::OK) {
return std::make_pair("", err1);
}
if (rows_.size() && key <= rows_.rbegin()->first) {
return std::make_pair("", COUNTED_ERROR(DB3_ReplicationOutOfOrder));
}
auto [priv, err2] = util::StringToByteArray<sizeof(PrivateKey)>(row->priv());
if (err2 != error::OK) {
return std::make_pair("", err2);
}
Row r;
r.tries = row->tries();
r.priv = priv;
rows_.emplace_hint(rows_.end(), key, std::move(r));
GAUGE(db, rows)->Set(rows_.size());
}
if (rows_.size() != initial_rows + rows.size()) {
// This ensures that we didn't accidentally attempt to load rows that
// already exist within the DB.
return std::make_pair("", COUNTED_ERROR(DB3_LoadedRowsAlreadyInDB));
}
return std::make_pair(row->backup_id(), error::OK);
}
std::array<uint8_t, 32> DB3::Hash(context::Context* ctx) const {
MEASURE_CPU(ctx, cpu_db_hash);
crypto_hash_sha256_state sha;
crypto_hash_sha256_init(&sha);
uint8_t num[8];
util::BigEndian64Bytes(rows_.size(), num);
crypto_hash_sha256_update(&sha, num, sizeof(num));
for (auto iter = rows_.cbegin(); iter != rows_.cend(); ++iter) {
crypto_hash_sha256_update(&sha, iter->first.data(), iter->first.size());
util::BigEndian64Bytes(iter->second.tries, num);
crypto_hash_sha256_update(&sha, num, sizeof(num));
crypto_hash_sha256_update(&sha, iter->second.priv.data(), iter->second.priv.size());
}
std::array<uint8_t, 32> out;
crypto_hash_sha256_final(&sha, out.data());
return out;
}
std::pair<DB3::Element, error::Error> DB3::BlindEvaluate(const DB3::PrivateKey& key, const DB3::Element& blinded_element) {
Element out{0};
int ret = 0;
if (0 != (ret = crypto_scalarmult_ristretto255(out.data(), key.data(), blinded_element.data()))) {
LOG(WARNING) << "crypto_scalarmult_ristretto255 error: " << ret;
return std::make_pair(out, COUNTED_ERROR(DB3_ScalarMultFailure));
}
return std::make_pair(out, error::OK);
}
std::pair<DB3::PrivateKey, DB3::PublicKey> DB3::Protocol::NewKeys() {
PrivateKey priv{0};
PublicKey pub{0};
crypto_core_ristretto255_scalar_random(priv.data());
// This will only return non-zero if `priv == 0`, which should never happen.
// TODO: Consider using either a protocol specific or a server specific base point.
CHECK(0 == crypto_scalarmult_ristretto255_base(pub.data(), priv.data()));
return std::make_pair(priv, pub);
}
void DB3::Create(
context::Context* ctx,
const DB3::BackupID& id,
const std::string& privkey,
const std::string& pubkey,
const client::CreateRequest& req,
client::CreateResponse* resp) {
auto [elt, err1] = util::StringToByteArray<ELEMENT_SIZE>(req.blinded_element());
if (err1 != error::OK) {
resp->set_status(client::CreateResponse::INVALID_REQUEST);
return;
}
auto [priv, err2] = util::StringToByteArray<sizeof(PrivateKey)>(privkey);
if (err2 != error::OK) {
resp->set_status(client::CreateResponse::ERROR);
return;
}
auto [pub, err3] = util::StringToByteArray<sizeof(PublicKey)>(pubkey);
if (err3 != error::OK) {
resp->set_status(client::CreateResponse::ERROR);
return;
}
auto [evaluated, err4] = BlindEvaluate(priv, elt);
if (err4 != error::OK) {
resp->set_status(client::CreateResponse::ERROR);
return;
}
rows_[id] = {
.priv = priv,
.tries = (uint8_t) req.max_tries(),
};
GAUGE(db, rows)->Set(rows_.size());
resp->set_evaluated_element(util::ByteArrayToString(evaluated));
resp->set_public_key(util::ByteArrayToString(pub));
resp->set_status(client::CreateResponse::OK);
}
void DB3::Evaluate(
context::Context* ctx,
const DB3::BackupID& id,
const client::EvaluateRequest& req,
client::EvaluateResponse* resp) {
auto [elt, err1] = util::StringToByteArray<ELEMENT_SIZE>(req.blinded_element());
if (err1 != error::OK) {
resp->set_status(client::EvaluateResponse::INVALID_REQUEST);
return;
}
auto find = rows_.find(id);
if (find == rows_.end()) {
resp->set_status(client::EvaluateResponse::MISSING);
return;
}
auto [evaluated, err2] = BlindEvaluate(find->second.priv, elt);
if (err2 != error::OK) {
resp->set_status(client::EvaluateResponse::ERROR);
return;
}
find->second.tries--;
resp->set_tries_remaining(find->second.tries);
if (find->second.tries == 0) {
rows_.erase(find);
GAUGE(db, rows)->Set(rows_.size());
}
resp->set_evaluated_element(util::ByteArrayToString(evaluated));
resp->set_status(client::EvaluateResponse::OK);
}
void DB3::Remove(
context::Context* ctx,
const DB3::BackupID& id,
const client::RemoveRequest& req,
client::RemoveResponse* resp) {
auto find = rows_.find(id);
if (find != rows_.end()) {
rows_.erase(find);
GAUGE(db, rows)->Set(rows_.size());
}
}
} // namespace svr2::db

124
enclave/db/db3.h Normal file
View File

@ -0,0 +1,124 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_DB_DB3_H__
#define __SVR2_DB_DB3_H__
#include <map>
#include <array>
#include "proto/error.pb.h"
#include "proto/e2e.pb.h"
#include "proto/msgs.pb.h"
#include "sip/hasher.h"
#include "context/context.h"
#include "util/log.h"
#include "db/db.h"
#include "proto/client3.pb.h"
#include <sodium/crypto_core_ristretto255.h>
#include <sodium/crypto_scalarmult_ristretto255.h>
namespace svr2::db {
class DB3 : public DB {
public:
DELETE_COPY_AND_ASSIGN(DB3);
DB3() {}
virtual ~DB3() {}
static const size_t BACKUP_ID_SIZE = 16;
static const size_t SCALAR_SIZE = crypto_scalarmult_ristretto255_SCALARBYTES;
static const size_t ELEMENT_SIZE = crypto_scalarmult_ristretto255_BYTES;
typedef std::array<uint8_t, BACKUP_ID_SIZE> BackupID;
typedef std::array<uint8_t, SCALAR_SIZE> Scalar;
typedef std::array<uint8_t, ELEMENT_SIZE> Element;
typedef Scalar PrivateKey;
typedef Element PublicKey;
static const uint16_t MAX_ALLOWED_MAX_TRIES = 255;
static const uint16_t MIN_ALLOWED_MAX_TRIES = 1;
// Protocol encapsulates typing requests and responses for clients.
class Protocol : public DB::Protocol {
public:
virtual DB::Request* RequestPB(context::Context* ctx) const;
virtual DB::Log* LogPB(context::Context* ctx) const;
virtual std::pair<Log*, error::Error> LogPBFromRequest(
context::Context* ctx,
Request&& request,
const std::string& authenticated_id) const;
virtual const std::string& LogKey(const DB::Log& r) const;
virtual error::Error ValidateClientLog(const DB::Log& log) const;
virtual size_t MaxRowSerializedSize() const;
public_for_test:
static std::pair<PrivateKey, PublicKey> NewKeys();
};
// P() returns a pointer to a _static_ Protocol object,
// which will outlast the DB object.
virtual const DB::Protocol* P() const;
// Run a client log request and yield a response.
// The client log should already have been checked with ValidateClientLog;
// failing to do so will CHECK-fail.
// It's assumed that validation happens on Raft log insert, so that
// outputs from the Raft log are already validated.
//
// Output response is valid within the passed-in context.
virtual DB::Response* Run(context::Context* ctx, const DB::Log& request);
// Get rows from this database in range (exclusive_start, ...], returning
// no more than [size] rows. If it returns <[size] rows, the end of the database
// has been reached. Pass in empty string to start with the first key in
// the database. Returns the key of the largest returned row.
virtual std::pair<std::string, error::Error> RowsAsProtos(
context::Context* ctx,
const std::string& exclusive_start,
size_t size,
google::protobuf::RepeatedPtrField<std::string>* out) const;
// Update this database using the given database row states.
// This will return an error if any of the DatabaseRowStates contain
// rows that already exist within the database. Rows must be lexigraphically
// larger than any existing row in the database. Returns the row key
// of the last row inserted into the database, on success.
virtual std::pair<std::string, error::Error> LoadRowsFromProtos(
context::Context* ctx,
const google::protobuf::RepeatedPtrField<std::string>& rows);
// Compute a hash of the entire database. This is not designed to
// be useful for security-focussed integrity checking, but should be
// sufficient to verify that replicated data matches up between source
// and destination.
virtual std::array<uint8_t, 32> Hash(context::Context* ctx) const;
// Get the number of backups stored in the database
virtual size_t row_count() const { return rows_.size(); }
private:
static std::pair<Element, error::Error> BlindEvaluate(const PrivateKey& key, const Element& blinded_element);
struct Row {
PrivateKey priv;
uint8_t tries;
};
std::map<BackupID, Row> rows_;
void Create(
context::Context* ctx,
const BackupID& id,
const std::string& privkey,
const std::string& pubkey,
const client::CreateRequest& req,
client::CreateResponse* resp);
void Evaluate(
context::Context* ctx,
const BackupID& id,
const client::EvaluateRequest& req,
client::EvaluateResponse* resp);
void Remove(
context::Context* ctx,
const BackupID& id,
const client::RemoveRequest& req,
client::RemoveResponse* resp);
};
} // namespace svr2::db
#endif // __SVR2_DB_DB3_H__

247
enclave/db/tests/db2.cc Normal file
View File

@ -0,0 +1,247 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP context
//TESTDEP env
//TESTDEP env/test
//TESTDEP env
//TESTDEP util
//TESTDEP metrics
//TESTDEP proto
//TESTDEP protobuf-lite
//TESTDEP libsodium
#include <gtest/gtest.h>
#include "db/db2.h"
#include "env/env.h"
#include "util/log.h"
#include "util/endian.h"
#include "proto/client.pb.h"
#include "proto/clientlog.pb.h"
#include <memory>
namespace svr2::db {
class DB2Test : public ::testing::Test {
protected:
static void SetUpTestCase() {
env::Init();
}
context::Context ctx;
DB2 db;
};
TEST_F(DB2Test, SingleBackupLifecycle) {
{
client::Log2 log;
auto b = log.mutable_req()->mutable_backup();
log.set_backup_id("BACKUP7890123456");
b->set_data("DATA56789012345678901234567890123456789012345678");
b->set_pin("PIN45678901234567890123456789012");
b->set_max_tries(2);
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::BackupResponse::OK, resp->backup().status());
}
{
client::Log2 log;
auto b = log.mutable_req()->mutable_expose();
log.set_backup_id("BACKUP7890123456");
b->set_data("DATA56789012345678901234567890123456789012345678");
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::ExposeResponse::OK, resp->expose().status());
}
{
client::Log2 log;
auto r = log.mutable_req()->mutable_restore();
log.set_backup_id("BACKUP7890123456");
r->set_pin("PIN45678901234567890123456789012");
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::RestoreResponse::OK, resp->restore().status());
ASSERT_EQ("DATA56789012345678901234567890123456789012345678", resp->restore().data());
ASSERT_EQ(2, resp->restore().tries());
}
{
client::Log2 log;
auto r = log.mutable_req()->mutable_restore();
log.set_backup_id("BACKUP7890123456");
r->set_pin("PIN............................2");
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::RestoreResponse::PIN_MISMATCH, resp->restore().status());
ASSERT_EQ("", resp->restore().data());
ASSERT_EQ(1, resp->restore().tries());
}
{
client::Log2 log;
auto r = log.mutable_req()->mutable_restore();
log.set_backup_id("BACKUP7890123456");
r->set_pin("PIN............................2");
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::RestoreResponse::MISSING, resp->restore().status());
}
}
TEST_F(DB2Test, SmallerData) {
{
client::Log2 log;
auto b = log.mutable_req()->mutable_backup();
log.set_backup_id("BACKUP7890123456");
b->set_data("DATA5678901234567890123456789012"); // 32 bytes
b->set_pin("PIN45678901234567890123456789012");
b->set_max_tries(2);
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::BackupResponse::OK, resp->backup().status());
}
{
client::Log2 log;
auto b = log.mutable_req()->mutable_expose();
log.set_backup_id("BACKUP7890123456");
b->set_data("DATA5678901234567890123456789012"); // 32 bytes
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::ExposeResponse::OK, resp->expose().status());
}
{
client::Log2 log;
auto r = log.mutable_req()->mutable_restore();
log.set_backup_id("BACKUP7890123456");
r->set_pin("PIN45678901234567890123456789012");
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::RestoreResponse::OK, resp->restore().status());
ASSERT_EQ("DATA5678901234567890123456789012", resp->restore().data());
}
}
TEST_F(DB2Test, Delete) {
{
client::Log2 log;
auto b = log.mutable_req()->mutable_backup();
log.set_backup_id("BACKUP7890123456");
b->set_data("DATA5678901234567890123456789012"); // 32 bytes
b->set_pin("PIN45678901234567890123456789012");
b->set_max_tries(2);
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::BackupResponse::OK, resp->backup().status());
}
{
client::Log2 log;
auto b = log.mutable_req()->mutable_expose();
log.set_backup_id("BACKUP7890123456");
b->set_data("DATA5678901234567890123456789012"); // 32 bytes
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::ExposeResponse::OK, resp->expose().status());
}
{
client::Log2 log;
auto d = log.mutable_req()->mutable_delete_();
log.set_backup_id("BACKUP7890123456");
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
}
{
client::Log2 log;
auto r = log.mutable_req()->mutable_restore();
log.set_backup_id("BACKUP7890123456");
r->set_pin("PIN45678901234567890123456789012");
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::RestoreResponse::MISSING, resp->restore().status());
}
}
TEST_F(DB2Test, MultipleRows) {
std::string backup_id("BACKUP789012345.");
std::string data("DATA567890123456789012345678901.");
for (int i = 0; i < 256; i++) {
backup_id[DB2::BACKUP_ID_SIZE-1] = i;
data[31] = i;
{
client::Log2 log;
auto b = log.mutable_req()->mutable_backup();
log.set_backup_id(backup_id);
b->set_data(data); // 32 bytes
b->set_pin("PIN45678901234567890123456789012");
b->set_max_tries(2);
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::BackupResponse::OK, resp->backup().status());
}
{
client::Log2 log;
auto b = log.mutable_req()->mutable_expose();
log.set_backup_id(backup_id);
b->set_data(data); // 32 bytes
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::ExposeResponse::OK, resp->expose().status());
}
}
for (int i = 0; i < 256; i++) {
client::Log2 log;
auto r = log.mutable_req()->mutable_restore();
backup_id[DB2::BACKUP_ID_SIZE-1] = i;
data[31] = i;
log.set_backup_id(backup_id);
r->set_pin("PIN45678901234567890123456789012");
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::RestoreResponse::OK, resp->restore().status());
ASSERT_EQ(data, resp->restore().data());
}
}
TEST_F(DB2Test, HashMatch) {
std::string backup_id("BACKUP789012345.");
std::string data("DATA567890123456789012345678901.");
uint64_t hash = 0;
for (int i = 0; i < 256; i++) {
client::Log2 log;
auto b = log.mutable_req()->mutable_backup();
backup_id[DB2::BACKUP_ID_SIZE-1] = i;
data[31] = i;
log.set_backup_id(backup_id);
b->set_data(data); // 32 bytes
b->set_pin("PIN45678901234567890123456789012");
b->set_max_tries(2);
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::BackupResponse::OK, resp->backup().status());
uint64_t new_hash = util::BigEndian64FromBytes(db.Hash(&ctx).data());
ASSERT_NE(hash, new_hash); // hash changes with every database change.
hash = new_hash;
}
ASSERT_EQ(hash, 784802678439774802ULL);
}
TEST_F(DB2Test, HashMatchBackwards) {
// Make sure that even if we construct the same DB in a different way
// (in this case, by inserting back IDs in reverse of HashMatch), we
// get the same result.
std::string backup_id("BACKUP789012345.");
std::string data("DATA567890123456789012345678901.");
for (int i = 255; i >= 0; i--) {
client::Log2 log;
auto b = log.mutable_req()->mutable_backup();
backup_id[DB2::BACKUP_ID_SIZE-1] = i;
data[31] = i;
log.set_backup_id(backup_id);
b->set_data(data); // 32 bytes
b->set_pin("PIN45678901234567890123456789012");
b->set_max_tries(2);
auto resp = dynamic_cast<client::Response*>(db.Run(&ctx, log));
ASSERT_EQ(client::BackupResponse::OK, resp->backup().status());
}
ASSERT_EQ(util::BigEndian64FromBytes(db.Hash(&ctx).data()), 784802678439774802ULL);
}
} // namespace svr2::db

373
enclave/db/tests/db3.cc Normal file
View File

@ -0,0 +1,373 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP context
//TESTDEP env
//TESTDEP env/test
//TESTDEP env
//TESTDEP util
//TESTDEP metrics
//TESTDEP proto
//TESTDEP protobuf-lite
//TESTDEP libsodium
#include <gtest/gtest.h>
#include "db/db3.h"
#include "env/env.h"
#include "util/log.h"
#include "util/endian.h"
#include "util/bytes.h"
#include "util/hex.h"
#include "proto/client3.pb.h"
#include "proto/clientlog.pb.h"
#include <memory>
#include <sodium/crypto_scalarmult_ristretto255.h>
#include <sodium/crypto_auth_hmacsha512.h>
namespace svr2::db {
class DB3Test : public ::testing::Test {
protected:
static void SetUpTestCase() {
env::Init();
}
context::Context ctx;
DB3 db;
static std::string backup_id;
};
std::string DB3Test::backup_id("BACKUP7890123456");
TEST_F(DB3Test, SingleBackupLifecycle) {
std::string blinded_element;
blinded_element.resize(DB3::ELEMENT_SIZE);
crypto_core_ristretto255_random(
reinterpret_cast<uint8_t*>(blinded_element.data()));
std::string evaluated_element;
int tries = 3;
{
client::Log3 log;
log.set_backup_id(backup_id);
auto b = log.mutable_req()->mutable_create();
b->set_max_tries(3);
b->set_blinded_element(blinded_element);
auto [priv, pub] = DB3::Protocol::NewKeys();
log.set_create_privkey(util::ByteArrayToString(priv));
log.set_create_pubkey(util::ByteArrayToString(pub));
auto resp = dynamic_cast<client::Response3*>(db.Run(&ctx, log));
auto r = resp->create();
ASSERT_EQ(client::CreateResponse::OK, r.status());
evaluated_element = r.evaluated_element();
}
for (int i = 0; i < tries; i++) {
client::Log3 log;
log.set_backup_id(backup_id);
auto b = log.mutable_req()->mutable_evaluate();
b->set_blinded_element(blinded_element);
auto resp = dynamic_cast<client::Response3*>(db.Run(&ctx, log));
auto r = resp->evaluate();
ASSERT_EQ(client::EvaluateResponse::OK, r.status());
EXPECT_EQ(r.tries_remaining(), tries - i - 1);
EXPECT_EQ(r.evaluated_element(), evaluated_element);
}
{
client::Log3 log;
log.set_backup_id(backup_id);
auto b = log.mutable_req()->mutable_evaluate();
b->set_blinded_element(blinded_element);
auto resp = dynamic_cast<client::Response3*>(db.Run(&ctx, log));
auto r = resp->evaluate();
ASSERT_EQ(client::EvaluateResponse::MISSING, r.status());
}
}
TEST_F(DB3Test, Remove) {
std::string blinded_element;
blinded_element.resize(DB3::ELEMENT_SIZE);
crypto_core_ristretto255_random(
reinterpret_cast<uint8_t*>(blinded_element.data()));
std::string evaluated_element;
int tries = 3;
{
client::Log3 log;
log.set_backup_id(backup_id);
auto b = log.mutable_req()->mutable_create();
b->set_max_tries(3);
b->set_blinded_element(blinded_element);
auto [priv, pub] = DB3::Protocol::NewKeys();
log.set_create_privkey(util::ByteArrayToString(priv));
log.set_create_pubkey(util::ByteArrayToString(pub));
auto resp = dynamic_cast<client::Response3*>(db.Run(&ctx, log));
auto r = resp->create();
ASSERT_EQ(client::CreateResponse::OK, r.status());
evaluated_element = r.evaluated_element();
}
{
client::Log3 log;
log.set_backup_id(backup_id);
auto b = log.mutable_req()->mutable_remove();
db.Run(&ctx, log);
}
{
client::Log3 log;
log.set_backup_id(backup_id);
auto b = log.mutable_req()->mutable_evaluate();
b->set_blinded_element(blinded_element);
auto resp = dynamic_cast<client::Response3*>(db.Run(&ctx, log));
auto r = resp->evaluate();
ASSERT_EQ(client::EvaluateResponse::MISSING, r.status());
}
}
// IETF VOPRF v21 test vectors (https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-21.html)
const std::string context_string_prefix{"OPRFV1-"};
const std::string ciphersuite_identifier{"ristretto255-SHA512"};
static const size_t PRIVATE_KEY_SIZE = 32;
static const size_t PUBLIC_KEY_SIZE = 32;
static const size_t SHA512_BLOCK_BYTES = 128;
static const size_t SHA512_OUTPUT_BYTES = 64;
// https://www.rfc-editor.org/rfc/rfc8017
std::string I2OSP(uint64_t x, size_t n) {
std::string X;
X.resize(n);
for(size_t i = 0; i < n; ++i) {
X[n-1-i] = x%256;
x /= 256;
}
return X;
}
/*
def CreateContextString(mode, identifier):
return "OPRFV1-" || I2OSP(mode, 1) || "-" || identifier
*/
std::string context_string() {
auto mode = I2OSP(0x00, 1);
return context_string_prefix + mode + "-" + ciphersuite_identifier;
}
std::string sha512_hash(std::string s) {
crypto_hash_sha512_state sha;
crypto_hash_sha512_init(&sha);
crypto_hash_sha512_update(&sha, reinterpret_cast<uint8_t*>(s.data()), s.size());
std::array<uint8_t, SHA512_OUTPUT_BYTES> out;
crypto_hash_sha512_final(&sha, out.data());
return util::ByteArrayToString(out);
}
std::string strxor(const std::string& lhs, const std::string& rhs) {
CHECK(lhs.size() == rhs.size());
std::string result;
result.resize(rhs.size());
for(size_t i = 0; i < lhs.size(); ++i) {
result[i] = lhs[i] ^ rhs[i];
}
return result;
}
template<size_t N>
bool is_zero(const std::array<uint8_t, N>& arr) {
bool result = true;
for(size_t i = 0; i < N; ++i) {
result = result && (arr[i] == 0);
}
return result;
}
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-12#name-expand_message_xmd
template<size_t N>
std::array<uint8_t, N> ExpandMessageXMD_SHA512(std::string msg, std::string dst) {
auto ell = N / SHA512_OUTPUT_BYTES + ((N%SHA512_OUTPUT_BYTES == 0) ? 0 : 1);
CHECK(ell <= 255);
LOG(DEBUG) << "expand_message_xmd blocks: " << ell;
std::array<uint8_t, N> result{0};
auto dst_prime = dst + I2OSP(dst.size(),1);
auto z_pad = I2OSP(0, SHA512_BLOCK_BYTES);
auto l_i_b_str = I2OSP(N,2);
auto msg_prime = z_pad + msg + l_i_b_str + I2OSP(0,1) + dst_prime;
auto b_0 = sha512_hash(msg_prime);
auto b_1 = sha512_hash(b_0 + I2OSP(1,1) + dst_prime);
auto bytes_to_copy = std::min(b_1.size(), N);
std::copy(b_1.data(), b_1.data()+ bytes_to_copy, result.data());
auto b_last = b_1;
for(size_t i = 2; i <= ell; ++i) {
auto b_next = sha512_hash(
strxor(b_0, b_last)
+ I2OSP(i,1)
+ dst_prime
);
auto bytes_to_copy = std::min(SHA512_OUTPUT_BYTES, N - (i-1)*SHA512_OUTPUT_BYTES);
LOG(DEBUG) << "copying " << bytes_to_copy << " bytes";
std::copy(b_next.data(), b_next.data() + bytes_to_copy, result.data() + (i-1)*SHA512_OUTPUT_BYTES);
b_last = b_next;
}
return result;
}
std::array<uint8_t, PRIVATE_KEY_SIZE> HashToScalar(const std::string& data) {
std::string dst = std::string{"HashToScalar-"} + context_string();
auto uniform_bytes = ExpandMessageXMD_SHA512<64>(data, dst);
std::array<uint8_t, PRIVATE_KEY_SIZE> s;
// TODO: verify that this interprets numbers in little-endian order
crypto_core_ristretto255_scalar_reduce(s.data(), uniform_bytes.data());
return s;
}
std::pair<std::array<uint8_t, PUBLIC_KEY_SIZE>, std::array<uint8_t, PRIVATE_KEY_SIZE>>
DeriveKeyPair(std::string seed, std::string info) {
std::string derive_input = seed + I2OSP(info.size(),2) + info;
size_t counter = 0;
std::array<uint8_t, PRIVATE_KEY_SIZE> sk{0};
std::array<uint8_t, PUBLIC_KEY_SIZE> pk{0};
std::string dst = std::string{"DeriveKeyPair"} + context_string();
while(is_zero(sk)) {
LOG(DEBUG) << "derive key pair attempt " << counter;
CHECK(counter < 255);
auto uniform_bytes =
ExpandMessageXMD_SHA512<64>(derive_input + I2OSP(counter,1), dst);
crypto_core_ristretto255_scalar_reduce(sk.data(), uniform_bytes.data());
counter += 1;
}
CHECK(0 == crypto_scalarmult_ristretto255_base(pk.data(), sk.data()));
return std::make_pair(pk, sk);
}
std::array<uint8_t, PUBLIC_KEY_SIZE> HashToGroup(std::string input) {
std::string dst = std::string{"HashToGroup-"} + context_string();
auto uniform_bytes = ExpandMessageXMD_SHA512<64>(input, dst);
std::array<uint8_t, PUBLIC_KEY_SIZE> result{};
crypto_core_ristretto255_from_hash(result.data(), uniform_bytes.data());
return result;
}
TEST_F(DB3Test, IETF_A_1_1) {
auto seed = util::HexToBytes("a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3");
auto key_info = util::HexToBytes("74657374206b6579");
auto sk_expected = "5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e";
auto cs = context_string();
for(size_t i = 0; i < cs.size(); ++i) {
LOG(DEBUG) << " (" << static_cast<int>(cs[i]) << ") " << cs[i] ;
}
LOG(DEBUG) << cs;
auto [pk, sk] = DeriveKeyPair(seed, key_info);
auto sk_hex = util::BytesToHex(sk.data(), PRIVATE_KEY_SIZE);
EXPECT_EQ(sk_hex, sk_expected);
}
TEST_F(DB3Test, EXPAND_MESSAGE_XMD_1) {
std::string dst{"QUUX-V01-CS02-with-expander-SHA512-256"};
std::string msg{"abc"};
size_t len_in_bytes = 0x80;
auto uniform_bytes = ExpandMessageXMD_SHA512<0x80>(msg, dst);
LOG(DEBUG) << "here";
auto hex = util::BytesToHex(uniform_bytes.data(), uniform_bytes.size());
LOG(DEBUG) << hex;
LOG(DEBUG) << "there";
EXPECT_EQ(util::BytesToHex(uniform_bytes.data(), uniform_bytes.size()), "7f1dddd13c08b543f2e2037b14cefb255b44c83cc397c1786d975653e36a6b11bdd7732d8b38adb4a0edc26a0cef4bb45217135456e58fbca1703cd6032cb1347ee720b87972d63fbf232587043ed2901bce7f22610c0419751c065922b488431851041310ad659e4b23520e1772ab29dcdeb2002222a363f0c2b1c972b3efe1");
}
TEST_F(DB3Test, EXPAND_MESSAGE_XMD_2) {
std::string dst{"QUUX-V01-CS02-with-expander-SHA512-256"};
std::string msg{"abcdef0123456789"};
size_t len_in_bytes = 0x20;
auto uniform_bytes = ExpandMessageXMD_SHA512<0x20>(msg, dst);
LOG(DEBUG) << util::BytesToHex(uniform_bytes.data(), uniform_bytes.size());
EXPECT_EQ(util::BytesToHex(uniform_bytes.data(), uniform_bytes.size()), "087e45a86e2939ee8b91100af1583c4938e0f5fc6c9db4b107b83346bc967f58");
}
TEST_F(DB3Test, IETF_A_1_1_1) {
auto sk = util::HexToBytes("5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e");
auto input = util::HexToBytes("00");
auto blind = util::HexToBytes("64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706");
auto blinded_element_expected = util::HexToBytes("609a0ae68c15a3cf6903766461307e5c8bb2f95e7e6550e1ffa2dc99e412803c");
std::string evaluation_element_hex = "7ec6578ae5120958eb2db1745758ff379e77cb64fe77b0b2d8cc917ea0869c7e";
std::string output_hex = "527759c3d9366f277d8c6020418d96bb393ba2afb20ff90df23fb7708264e2f3ab9135e3bd69955851de4b1f9fe8a0973396719b7912ba9ee8aa7d0b5e24bcf6";
// Compute blinded element
std::array<uint8_t, 32> blinded_element;
std::array<uint8_t, PUBLIC_KEY_SIZE> elt = HashToGroup(input);
auto ret = crypto_scalarmult_ristretto255(blinded_element.data(), reinterpret_cast<uint8_t*>(blind.data()), elt.data());
EXPECT_EQ(util::BytesToHex(blinded_element.data(), PUBLIC_KEY_SIZE), "609a0ae68c15a3cf6903766461307e5c8bb2f95e7e6550e1ffa2dc99e412803c");
// send to server to evaluate
std::string evaluated_element;
int tries = 3;
{
client::Log3 log;
log.set_backup_id(backup_id);
auto b = log.mutable_req()->mutable_create();
b->set_max_tries(3);
b->set_blinded_element(util::ByteArrayToString(blinded_element));
std::array<uint8_t, PUBLIC_KEY_SIZE> pk{};
CHECK(0 == crypto_scalarmult_ristretto255_base(pk.data(), reinterpret_cast<uint8_t*>(sk.data())));
log.set_create_privkey(sk);
log.set_create_pubkey(util::ByteArrayToString(pk));
auto resp = dynamic_cast<client::Response3*>(db.Run(&ctx, log));
auto r = resp->create();
ASSERT_EQ(client::CreateResponse::OK, r.status());
evaluated_element = r.evaluated_element();
auto [ee_data, err] = util::StringToByteArray<PUBLIC_KEY_SIZE>(evaluated_element);
EXPECT_EQ(util::BytesToHex(ee_data.data(), ee_data.size()), evaluation_element_hex);
}
}
TEST_F(DB3Test, IETF_A_1_1_2) {
auto seed = util::HexToBytes("a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3");
auto key_info = util::HexToBytes("74657374206b6579");
auto sk = util::HexToBytes("5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e");
auto input = util::HexToBytes("5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a");
auto blind = util::HexToBytes("64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706");
auto blinded_element_expected = util::HexToBytes("da27ef466870f5f15296299850aa088629945a17d1f5b7f5ff043f76b3c06418");
auto evaluation_element_hex = "b4cbf5a4f1eeda5a63ce7b77c7d23f461db3fcab0dd28e4e17cecb5c90d02c25";
auto output_hex = "f4a74c9c592497375e796aa837e907b1a045d34306a749db9f34221f7e750cb4f2a6413a6bf6fa5e19ba6348eb673934a722a7ede2e7621306d18951e7cf2c73";
// Compute blinded element
std::array<uint8_t, 32> blinded_element;
std::array<uint8_t, PUBLIC_KEY_SIZE> elt = HashToGroup(input);
auto ret = crypto_scalarmult_ristretto255(blinded_element.data(), reinterpret_cast<uint8_t*>(blind.data()), elt.data());
EXPECT_EQ(util::BytesToHex(blinded_element.data(), PUBLIC_KEY_SIZE), "da27ef466870f5f15296299850aa088629945a17d1f5b7f5ff043f76b3c06418");
// send to server to evaluate
std::string evaluated_element;
int tries = 3;
{
client::Log3 log;
log.set_backup_id(backup_id);
auto b = log.mutable_req()->mutable_create();
b->set_max_tries(3);
b->set_blinded_element(util::ByteArrayToString(blinded_element));
std::array<uint8_t, PUBLIC_KEY_SIZE> pk{};
CHECK(0 == crypto_scalarmult_ristretto255_base(pk.data(), reinterpret_cast<uint8_t*>(sk.data())));
log.set_create_privkey(sk);
log.set_create_pubkey(util::ByteArrayToString(pk));
auto resp = dynamic_cast<client::Response3*>(db.Run(&ctx, log));
auto r = resp->create();
ASSERT_EQ(client::CreateResponse::OK, r.status());
evaluated_element = r.evaluated_element();
auto [ee_data, err] = util::StringToByteArray<PUBLIC_KEY_SIZE>(evaluated_element);
EXPECT_EQ(util::BytesToHex(ee_data.data(), ee_data.size()), evaluation_element_hex);
}
}
} // namespace svr2::db

97
enclave/ecalls/ecalls.cc Normal file
View File

@ -0,0 +1,97 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include <stdint.h>
#include <memory>
#include <atomic>
#include <mutex>
#include <cstdlib>
#include "svr2/svr2_t.h"
#include "core/core.h"
#include "proto/error.pb.h"
#include "proto/enclaveconfig.pb.h"
#include "env/env.h"
#include "context/context.h"
#include "util/endian.h"
#include "util/log.h"
#include "metrics/metrics.h"
namespace svr2::ecalls {
namespace {
void SeedWeakRandom() {
LOG(INFO) << "Seeding weak randomness with strong";
// Best-effort seeding of weak randomness from strong.
uint8_t bytes[8];
env::environment->RandomBytes(bytes, sizeof(bytes));
srand(util::BigEndian64FromBytes(bytes));
}
std::unique_ptr<svr2::core::Core> global_core;
// Sadly, we don't appear to have access to std::shared_mutex, so we use
// the next best thing.
enum class GlobalCoreState {
UNINITIATED = 0,
INITIATING = 1,
INITIATED = 2,
};
std::atomic<GlobalCoreState> global_core_state(GlobalCoreState::UNINITIATED);
} // namespace
extern "C" {
int svr2_init(
size_t config_size,
unsigned char* config,
unsigned char* peer_id) {
context::Context ctx;
COUNTER(ecalls, init_calls)->Increment();
GlobalCoreState state_expected = GlobalCoreState::UNINITIATED;
GlobalCoreState state_requested = GlobalCoreState::INITIATING;
if (!global_core_state.compare_exchange_strong(state_expected, state_requested)) {
return COUNTED_ERROR(Core_ReInit);
}
enclaveconfig::InitConfig config_pb;
if (!config_pb.ParseFromArray(config, config_size)) {
global_core_state.store(GlobalCoreState::UNINITIATED);
return COUNTED_ERROR(Core_ConfigProtobufParse);
}
if (config_pb.initial_log_level() != enclaveconfig::LOG_LEVEL_NONE) {
util::SetLogLevel(config_pb.initial_log_level());
}
env::Init(config_pb.group_config().simulated()); // Can be called more than once, but never concurrently.
SeedWeakRandom();
LOG(INFO) << "Creating core";
auto [core, err] = core::Core::Create(&ctx, config_pb);
if (err != error::OK) {
global_core_state.store(GlobalCoreState::UNINITIATED);
return err;
}
global_core = std::move(core);
const auto peer_id_array = global_core->ID().Get();
std::copy(peer_id_array.begin(), peer_id_array.end(), peer_id);
global_core_state.store(GlobalCoreState::INITIATED);
return error::OK;
}
int svr2_input_message(
size_t msg_size,
unsigned char* msg) {
context::Context ctx;
COUNTER(ecalls, host_messages_received)->Increment();
COUNTER(ecalls, host_bytes_received)->IncrementBy(msg_size);
if (global_core_state.load() != GlobalCoreState::INITIATED) {
return COUNTED_ERROR(Core_NoInit);
}
UntrustedMessage* msg_pb = ctx.Protobuf<UntrustedMessage>();
if (!msg_pb->ParseFromArray(msg, msg_size)) {
return COUNTED_ERROR(Core_ReceiveProtobufParse);
}
return global_core->Receive(&ctx, *msg_pb);
}
} // extern "C"
} // namespace svr2::ecalls

80
enclave/env/env.cc vendored Normal file
View File

@ -0,0 +1,80 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "env/env.h"
#include "util/macros.h"
#include <sodium/core.h>
#include <sodium/randombytes.h>
namespace svr2::env {
namespace {
class UnsetEnvironment : public Environment {
public:
virtual ~UnsetEnvironment() {}
virtual std::pair<e2e::Attestation, error::Error> Evidence(const PublicKey& key, const enclaveconfig::RaftGroupConfig& config) const {
CHECK(nullptr == "env::Init not called, environment not initiated");
return std::make_pair(e2e::Attestation(), error::General_Unimplemented);
}
// Given evidence and endorsements, extract the key.
virtual std::pair<PublicKey, error::Error> Attest(
util::UnixSecs now,
const std::string& evidence,
const std::string& endorsements) const {
CHECK(nullptr == "env::Init not called, environment not initiated");
std::array<uint8_t, 32> out = {0};
return std::make_pair(out, error::General_Unimplemented);
}
// Given a string of size N, rewrite all bytes in that string with
// random bytes.
virtual error::Error RandomBytes(void* bytes, size_t size) const {
CHECK(nullptr == "env::Init not called, environment not initiated");
return error::General_Unimplemented;
}
virtual error::Error SendMessage(const std::string& msg) const {
CHECK(nullptr == "env::Init not called, environment not initiated");
return error::General_Unimplemented;
}
virtual void Log(int level, const std::string& msg) const {
CHECK(nullptr == "env::Init not called, environment not initiated");
}
virtual error::Error UpdateEnvStats() const {
CHECK(nullptr == "env::Init not called, environment not initiated");
return error::General_Unimplemented;
}
};
const char* env_randombytes_name() { return "env"; }
uint32_t env_randombytes_uint32() {
uint32_t out;
CHECK(error::OK == environment->RandomBytes(&out, sizeof(out)));
return out;
}
void env_randombytes_bytes(void* const buf, const size_t size) {
CHECK(error::OK == environment->RandomBytes(buf, size));
}
randombytes_implementation sodium_randombytes_impl = {
.implementation_name = env_randombytes_name,
.random = env_randombytes_uint32,
.buf = env_randombytes_bytes,
};
} // namespace
std::unique_ptr<Environment> environment(new UnsetEnvironment());
Environment::Environment() {
}
void Environment::Init() {
// sodium_init returns 0 or 1 on success, -1 on failure.
CHECK(0 == randombytes_set_implementation(&sodium_randombytes_impl));
CHECK(sodium_init() >= 0);
}
} // namespace svr2::env

50
enclave/env/env.h vendored Normal file
View File

@ -0,0 +1,50 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_ENV_ENV_H__
#define __SVR2_ENV_ENV_H__
#include <string>
#include <array>
#include "proto/error.pb.h"
#include "proto/e2e.pb.h"
#include "proto/msgs.pb.h"
#include "util/macros.h"
#include "util/ticks.h"
namespace svr2::env {
typedef std::array<uint8_t, 32> PublicKey;
class Environment {
public:
DELETE_COPY_AND_ASSIGN(Environment);
Environment();
virtual ~Environment() {}
virtual void Init();
// Given a 32-byte key, return evidence of that key (an OpenEnclave report).
virtual std::pair<e2e::Attestation, error::Error> Evidence(const PublicKey& key, const enclaveconfig::RaftGroupConfig& config) const = 0;
// Given evidence and endorsements, extract the key.
virtual std::pair<PublicKey, error::Error> Attest(
util::UnixSecs now,
const std::string& evidence,
const std::string& endorsements) const = 0;
// Given a string of size N, rewrite all bytes in that string with
// random bytes.
virtual error::Error RandomBytes(void* bytes, size_t size) const = 0;
// Send a message from enclave to host. [msg] should be a serialized
// EnclaveMessage.
virtual error::Error SendMessage(const std::string& msg) const = 0;
// Log a message to a logging framework.
virtual void Log(int level, const std::string& msg) const = 0;
// Update env-specific statistics.
virtual error::Error UpdateEnvStats() const = 0;
};
extern std::unique_ptr<Environment> environment;
void Init(bool is_simulated = true);
} // namespace svr2::env
#endif // __SVR2_ENV_ENV_H__

114
enclave/env/nsm/nsm.cc vendored Normal file
View File

@ -0,0 +1,114 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include <sodium/core.h>
#include <nsm.h>
#include <deque>
#include "env/env.h"
#include "util/macros.h"
#include "context/context.h"
#include "socketwrap/socket.h"
#include "proto/nitro.pb.h"
#include "queue/queue.h"
namespace svr2::env {
namespace nsm {
namespace {
static queue::Queue<std::string> output_messages(100);
class Environment : public ::svr2::env::Environment {
public:
DELETE_COPY_AND_ASSIGN(Environment);
Environment() {
nsm_fd_ = nsm_lib_init();
}
virtual ~Environment() {
nsm_lib_exit(nsm_fd_);
}
virtual std::pair<e2e::Attestation, error::Error> Evidence(const PublicKey& key, const enclaveconfig::RaftGroupConfig& config) const {
e2e::Attestation out;
out.mutable_evidence()->resize(4096);
uint32_t evidence_len = out.evidence().size();
std::string config_serialized;
if (!config.SerializeToString(&config_serialized)) {
return std::make_pair(out, error::Env_SerializeCustomClaims);
}
if (ERROR_CODE_SUCCESS != nsm_get_attestation_doc(
nsm_fd_,
reinterpret_cast<const uint8_t*>(config_serialized.data()),
config_serialized.size(),
nullptr,
0,
key.data(),
key.size(),
reinterpret_cast<uint8_t*>(out.mutable_evidence()->data()),
&evidence_len)) {
return std::make_pair(out, error::Env_AttestationFailure);
}
out.mutable_evidence()->resize(evidence_len);
return std::make_pair(out, error::OK);
}
// Given evidence and endorsements, extract the key.
virtual std::pair<PublicKey, error::Error> Attest(
util::UnixSecs now,
const std::string& evidence,
const std::string& endorsements) const {
std::array<uint8_t, 32> out = {0};
return std::make_pair(out, error::General_Unimplemented);
}
// Given a string of size N, rewrite all bytes in that string with
// random bytes.
virtual error::Error RandomBytes(void* bytes, size_t size) const {
uintptr_t received;
uint8_t* u8ptr = reinterpret_cast<uint8_t*>(bytes);
while (size) {
received = size;
if (ERROR_CODE_SUCCESS != nsm_get_random(nsm_fd_, u8ptr, &received)) {
return error::Env_RandomBytes;
}
size -= received;
u8ptr += received;
}
return error::OK;
}
virtual error::Error SendMessage(const std::string& msg) const {
output_messages.Push(msg);
return error::OK;
}
virtual void Log(int level, const std::string& msg) const {
}
virtual error::Error UpdateEnvStats() const {
return error::General_Unimplemented;
}
private:
int32_t nsm_fd_;
};
} // namespace
error::Error SendNsmMessages(socketwrap::Socket* sock) {
while (true) {
context::Context ctx;
for (int i = 0; i < 100; i++) {
auto out = ctx.Protobuf<nitro::OutboundMessage>();
*out->mutable_out() = output_messages.Pop();
RETURN_IF_ERROR(sock->WritePB(&ctx, *out));
}
}
}
} // namespace nsm
void Init(bool is_simulated) {
environment = std::make_unique<::svr2::env::nsm::Environment>();
}
} // namespace svr2::env

17
enclave/env/nsm/nsm.h vendored Normal file
View File

@ -0,0 +1,17 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_ENV_NSM_NSM_H__
#define __SVR2_ENV_NSM_NSM_H__
#include "socketwrap/socket.h"
#include "proto/error.pb.h"
namespace svr2::env::nsm {
// Send all outstanding messages, in order, up to the host.
error::Error SendNsmMessages(socketwrap::Socket* sock);
} // namespace svr2::env::nsm
#endif // __SVR2_ENV_NSM_NSM_H__

272
enclave/env/sgx/sgx.cc vendored Normal file
View File

@ -0,0 +1,272 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include <openenclave/attestation/attester.h>
#include <openenclave/attestation/custom_claims.h>
#include <openenclave/attestation/sgx/evidence.h>
#include <openenclave/attestation/verifier.h>
#include <openenclave/bits/evidence.h>
#include <openenclave/enclave.h>
#include <openenclave/advanced/mallinfo.h>
#include <memory>
#include "attestation/attestation.h"
#include "env/env.h"
#include "metrics/metrics.h"
#include "svr2/svr2_t.h"
#include "util/constant.h"
#include "util/log.h"
namespace svr2::env {
namespace sgx {
static const char* unattested_evidence_prefix = "UNATTESTED EVIDENCE:";
static const char* custom_claim_pk = "pk";
static const char* custom_claim_config = "config";
class Environment : public ::svr2::env::Environment {
public:
DELETE_COPY_AND_ASSIGN(Environment);
Environment(bool simulated) : ::svr2::env::Environment(), simulated_(simulated) {
if (!simulated_) {
CHECK(OE_OK == oe_attester_initialize());
CHECK(OE_OK == oe_verifier_initialize());
CHECK(error::OK == GetMRENCLAVE());
}
}
virtual ~Environment() {
if (!simulated_) {
oe_attester_shutdown();
oe_verifier_shutdown();
}
}
virtual std::pair<e2e::Attestation, error::Error> Evidence(
const PublicKey& key, const enclaveconfig::RaftGroupConfig& config) const {
e2e::Attestation attestation;
if (simulated_) {
attestation.set_evidence(
unattested_evidence_prefix +
std::string(reinterpret_cast<const char*>(key.data()), key.size()));
return std::make_pair(attestation, error::OK);
}
std::string serialized_config;
if (!config.SerializeToString(&serialized_config)) {
return std::make_pair(e2e::Attestation(), COUNTED_ERROR(Env_SerializeConfigForEvidence));
}
uint8_t* custom_claims_buffer = NULL;
size_t custom_claims_buffer_size = 0;
oe_claim_t custom_claims[] = {
{
.name = const_cast<char*>(custom_claim_pk),
.value = const_cast<uint8_t*>(key.data()),
.value_size = key.size(),
},
{
.name = const_cast<char*>(custom_claim_config),
.value = reinterpret_cast<uint8_t*>(serialized_config.data()),
.value_size = serialized_config.size(),
},
};
if (OE_OK != oe_serialize_custom_claims(custom_claims, sizeof(custom_claims) / sizeof(custom_claims[0]),
&custom_claims_buffer,
&custom_claims_buffer_size)) {
return std::make_pair(e2e::Attestation(),
COUNTED_ERROR(Env_SerializeCustomClaims));
}
std::unique_ptr<uint8_t, oe_result_t (*)(uint8_t*)> free_cc(
custom_claims_buffer, oe_free_serialized_custom_claims);
uint8_t* evidence_buffer = NULL;
size_t evidence_buffer_size = 0;
uint8_t* endorsements_buffer = NULL;
size_t endorsements_buffer_size = 0;
if (OE_OK != oe_get_evidence(&attestation::sgx_remote_uuid, 0, custom_claims_buffer,
custom_claims_buffer_size, NULL, 0,
&evidence_buffer, &evidence_buffer_size,
&endorsements_buffer,
&endorsements_buffer_size)) {
return std::make_pair(e2e::Attestation(), error::Env_GetEvidence);
}
std::unique_ptr<uint8_t, oe_result_t (*)(uint8_t*)> free_evidence(
evidence_buffer, oe_free_evidence);
std::unique_ptr<uint8_t, oe_result_t (*)(uint8_t*)> free_endorsements(
endorsements_buffer, oe_free_endorsements);
std::string evidence((char*)evidence_buffer, evidence_buffer_size);
std::string endorsements((char*)endorsements_buffer,
endorsements_buffer_size);
attestation.set_evidence(evidence);
attestation.set_endorsements(endorsements);
return std::make_pair(attestation, error::OK);
}
virtual error::Error RandomBytes(void* bytes, size_t size) const {
CHECK(size > 0);
if (OE_OK != oe_random(bytes, size)) {
return COUNTED_ERROR(Env_RandomBytes);
}
return error::OK;
}
virtual std::pair<PublicKey, error::Error> Attest(
util::UnixSecs now,
const std::string& evidence,
const std::string& endorsements) const {
PublicKey out = {0};
if (simulated_) {
if (evidence.size() != strlen(unattested_evidence_prefix) + out.size() ||
evidence.substr(0, strlen(unattested_evidence_prefix)) !=
unattested_evidence_prefix) {
return std::make_pair(out, error::Env_AttestationFailure);
}
memcpy(out.data(), evidence.data() + strlen(unattested_evidence_prefix),
out.size());
return std::make_pair(out, error::OK);
}
const uint8_t* evidence_data =
reinterpret_cast<const uint8_t*>(evidence.data());
const uint8_t* endorsements_data =
reinterpret_cast<const uint8_t*>(endorsements.data());
oe_claim_t* claims = nullptr;
size_t claims_length = 0;
oe_datetime_t now_datetime;
SecsToOEDatetime(now, &now_datetime);
oe_policy_t policy = {
.type = OE_POLICY_ENDORSEMENTS_TIME,
.policy = &now_datetime,
.policy_size = sizeof(now_datetime),
};
auto verify_err = oe_verify_evidence(
&attestation::sgx_remote_uuid, evidence_data, evidence.size(), endorsements_data,
endorsements.size(), &policy, 1, &claims, &claims_length);
if (OE_OK != verify_err) {
LOG(ERROR) << "oe_verify_evidence failed with code " << verify_err;
return std::make_pair(out, error::Env_AttestationFailure);
}
auto free_claims_known_size = [claims_length](oe_claim_t* ptr) {
return oe_free_claims(ptr, claims_length);
};
std::unique_ptr<oe_claim_t, decltype(free_claims_known_size)> free_claims(
claims, free_claims_known_size);
// evidence is verified, now check individual fields
error::Error err = ValidateStandardClaims(claims, claims_length);
if (error::OK != err) {
return std::make_pair(out, err);
}
err = attestation::ReadKeyFromVerifiedClaims(claims, claims_length, out);
return std::make_pair(out, err);
}
virtual error::Error SendMessage(const std::string& msg) const {
if (OE_OK !=
svr2_output_message(
msg.size(), const_cast<uint8_t*>(
reinterpret_cast<const uint8_t*>(msg.data())))) {
return COUNTED_ERROR(Env_SendMessage);
}
return error::OK;
}
virtual void Log(int level, const std::string& msg) const {
oe_log_ocall(level, msg.c_str());
}
virtual error::Error UpdateEnvStats() const {
oe_mallinfo_t info;
if (OE_OK != oe_allocator_mallinfo(&info)) {
return COUNTED_ERROR(Env_MallinfoFailure);
}
GAUGE(env, total_heap_size)->Set(info.max_total_heap_size);
GAUGE(env, allocated_heap_size)->Set(info.current_allocated_heap_size);
GAUGE(env, peak_heap_size)->Set(info.peak_allocated_heap_size);
return error::OK;
}
private:
bool simulated_;
std::string expected_mrenclave_;
error::Error GetMRENCLAVE() {
auto [attestation, err] = Evidence(PublicKey{0}, enclaveconfig::RaftGroupConfig());
if (err != error::OK) {
return err;
}
auto [claims, claims_length] = attestation::VerifyAndReadClaims(
attestation.evidence(), attestation.endorsements());
auto free_claims_known_size = [claims_length=claims_length](oe_claim_t* ptr) {
return oe_free_claims(ptr, claims_length);
};
std::unique_ptr<oe_claim_t, decltype(free_claims_known_size)> free_claims(
claims, free_claims_known_size);
// read the MRENCLAVE - this is our MRENCLAVE and we expect all peers to
// have the same value OE_CLAIM_UNIQUE_ID retrieves MRENCLAVE on SGX
const oe_claim_t* claim;
if ((claim = attestation::FindClaim(claims, claims_length,
OE_CLAIM_UNIQUE_ID)) == nullptr) {
return COUNTED_ERROR(Env_AttestationFailure);
}
expected_mrenclave_ = std::string(
reinterpret_cast<const char*>(claim->value), claim->value_size);
return error::OK;
}
error::Error ValidateStandardClaims(oe_claim_t* claims,
size_t claims_length) const {
const oe_claim_t* claim;
// OE_CLAIM_UNIQUE_ID is MRENCLAVE for SGX
if ((claim = attestation::FindClaim(claims, claims_length,
OE_CLAIM_UNIQUE_ID)) == nullptr) {
return COUNTED_ERROR(Env_MissingMRENCLAVE);
}
auto actual_mrenclave = std::string(
reinterpret_cast<const char*>(claim->value), claim->value_size);
// Don't need constant time, but we have it so we use it.
if (!util::ConstantTimeEquals(actual_mrenclave, expected_mrenclave_)) {
return COUNTED_ERROR(Env_WrongMRENCLAVE);
}
return error::OK;
}
static void SecsToOEDatetime(util::UnixSecs secs, oe_datetime_t* dt) {
// Mostly copied from oe_datetime_now in OpenEnclave's common/datetime.c.
// Unfortunately, they expose the ability to get from "now", but not
// from an arbitrary timestamp.
CHECK(dt != nullptr);
struct tm timeinfo;
gmtime_r(&secs, &timeinfo);
dt->year = (uint32_t)timeinfo.tm_year + 1900;
dt->month = (uint32_t)timeinfo.tm_mon + 1;
dt->day = (uint32_t)timeinfo.tm_mday;
dt->hours = (uint32_t)timeinfo.tm_hour;
dt->minutes = (uint32_t)timeinfo.tm_min;
dt->seconds = (uint32_t)timeinfo.tm_sec;
}
};
} // namespace sgx
void Init(bool is_simulated) {
environment = std::make_unique<::svr2::env::sgx::Environment>(is_simulated);
environment->Init();
}
} // namespace svr2::env

94
enclave/env/test/test.cc vendored Normal file
View File

@ -0,0 +1,94 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "env/test/test.h"
#include "env/env.h"
#include "util/mutex.h"
#include <sys/random.h>
#include <string.h>
#include <atomic>
#include <mutex>
namespace svr2::env {
namespace test {
static const char* evidence_prefix = "EVIDENCE:";
static volatile std::atomic<uint32_t> random_gen;
class Environment : public ::svr2::env::Environment {
public:
DELETE_COPY_AND_ASSIGN(Environment);
Environment() : ::svr2::env::Environment() {}
virtual ~Environment() {}
virtual std::pair<e2e::Attestation, error::Error> Evidence(const PublicKey& key, const enclaveconfig::RaftGroupConfig& config) const {
e2e::Attestation attestation;
attestation.set_evidence(evidence_prefix + std::string(reinterpret_cast<const char*>(key.data()), key.size()));
return std::make_pair(attestation, error::OK);
}
virtual error::Error RandomBytes(void* bytes, size_t size) const {
// We could do this reading in a while loop, but we expect it should be fine.
// Rewrite this if tests fail because of it.
CHECK(size > 0);
uint8_t* ptr = reinterpret_cast<uint8_t*>(bytes);
for (size_t i = 0; i < size; i++) {
uint32_t next = std::atomic_fetch_add(&random_gen, 1U);
// This keeps the sequence of bytes relatively non-repeating for the first 4GB.
*ptr++ = (uint8_t)(next ^ (next >> 8) ^ (next >> 16) ^ (next >> 24));
}
return error::OK;
}
virtual std::pair<PublicKey, error::Error> Attest(
util::UnixSecs now,
const std::string& evidence,
const std::string& endorsements) const {
PublicKey out = {0};
if (evidence.size() != strlen(evidence_prefix) + out.size()
|| evidence.substr(0, strlen(evidence_prefix)) != evidence_prefix) {
return std::make_pair(out, error::Env_AttestationFailure);
}
memcpy(out.data(), evidence.data() + strlen(evidence_prefix), out.size());
return std::make_pair(out, error::OK);
}
virtual error::Error SendMessage(const std::string& msg) const {
util::unique_lock ul(mu_);
EnclaveMessage m;
CHECK(m.ParseFromString(msg));
sent_messages_.push_back(std::move(m));
return error::OK;
}
virtual void Log(int level, const std::string& msg) const {
fprintf(stderr, "%s\n", msg.c_str());
}
std::vector<EnclaveMessage> SentMessages() {
util::unique_lock ul(mu_);
return std::move(sent_messages_);
}
virtual error::Error UpdateEnvStats() const {
return error::OK;
}
private:
mutable util::mutex mu_;
mutable std::vector<EnclaveMessage> sent_messages_ GUARDED_BY(mu_);
};
std::vector<EnclaveMessage> SentMessages() {
Environment* e = dynamic_cast<Environment*>(::svr2::env::environment.get());
CHECK(e != nullptr);
return e->SentMessages();
}
} // namespace test
void Init(bool is_simulated) {
environment = std::make_unique<::svr2::env::test::Environment>();
environment->Init();
}
} // namespace svr2::env

17
enclave/env/test/test.h vendored Normal file
View File

@ -0,0 +1,17 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_ENV_TEST_TEST_H__
#define __SVR2_ENV_TEST_TEST_H__
#include <vector>
#include <string>
#include "proto/msgs.pb.h"
namespace svr2::env::test {
std::vector<EnclaveMessage> SentMessages();
} // namespace svr2::env::test
#endif // __SVR2_ENV_TEST_TEST_H__

28
enclave/env/test/tests/testrand.cc vendored Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP util
//TESTDEP env
//TESTDEP env/test
//TESTDEP proto
//TESTDEP protobuf-lite
//TESTDEP libsodium
#include <gtest/gtest.h>
#include "env/env.h"
#include "util/log.h"
#include "util/hex.h"
namespace svr2::env {
TEST(EnvTest, Random) {
Init();
uint8_t got[260];
ASSERT_EQ(error::OK, environment->RandomBytes(got, sizeof(got)));
LOG(INFO) << "Bytes: " << util::BytesToHex(got, 8);
uint8_t expect_first[] = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17};
ASSERT_EQ(0, memcmp(got, expect_first, sizeof(expect_first)));
}
} // namespace svr2::env

25
enclave/find_header.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
#
# Given a compiler and a header file, return what directory that header is located in.
#
set -e
if [[ $# != 2 ]]; then
echo 1>&2 "Usage: $0 <compiler> <header>"
exit 1
fi
COMPILER=$1
HEADER=$2
LISTING=""
"$COMPILER" -E -x c++ - -v </dev/null 2>&1 | while read line
do
if [[ $line == "#include <...> search starts here:" ]]; then
LISTING=1
elif [[ $line == "End of search list." ]]; then
exit 1
elif [[ $LISTING != "" ]]; then
if ls "$line/$HEADER" >/dev/null 2>/dev/null; then
echo "$line"
exit 0
fi
fi
done

1
enclave/googletest Submodule

@ -0,0 +1 @@
Subproject commit 3026483ae575e2de942db5e760cf95e973308dd5

View File

@ -0,0 +1,52 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "groupclock/groupclock.h"
#include <algorithm>
#include <vector>
#include "util/log.h"
namespace svr2::groupclock {
void Clock::SetLocalTime(util::UnixSecs secs) {
local_.store(secs);
}
void Clock::SetRemoteTime(context::Context* ctx, const peerid::PeerID& peer, util::UnixSecs secs) {
ACQUIRE_LOCK(mu_, ctx, lock_groupclock);
remotes_[peer] = secs;
}
util::UnixSecs Clock::GetTime(context::Context* ctx, const std::set<peerid::PeerID>& remotes) const {
std::vector<util::UnixSecs> secs(1 /* local_ */ + remotes.size());
ACQUIRE_LOCK(mu_, ctx, lock_groupclock);
auto set_iter = remotes.begin();
auto map_iter = remotes_.begin();
secs[0] = local_.load();
size_t secs_size = 1;
while (set_iter != remotes.end() && map_iter != remotes_.end()) {
const peerid::PeerID& set_peer = *set_iter;
const peerid::PeerID& map_peer = map_iter->first;
if (set_peer < map_peer) {
++set_iter;
} else if (map_peer < set_peer) {
++map_iter;
} else {
secs[secs_size++] = map_iter->second;
++set_iter;
++map_iter;
}
}
secs.resize(secs_size);
// `secs` now contains a list of my timestamp and the timestamps of all
// peers in `remotes` that we've received a timestamp from. Get the median.
std::sort(secs.begin(), secs.end());
return secs[secs.size()/2];
}
util::UnixSecs Clock::GetLocalTime() const {
return local_.load();
}
} // namespace svr2::groupclock

View File

@ -0,0 +1,37 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_GROUPCLOCK_GROUPCLOCK_H__
#define __SVR2_GROUPCLOCK_GROUPCLOCK_H__
#include <stdint.h>
#include <atomic>
#include <mutex>
#include "util/macros.h"
#include "util/mutex.h"
#include "util/ticks.h"
#include "peerid/peerid.h"
#include "context/context.h"
namespace svr2::groupclock {
// Clock that returns time based on times reported from a group of
// peers. The reported time will be the median of all reported times.
class Clock {
public:
DELETE_COPY_AND_ASSIGN(Clock);
Clock() : local_(0) {};
void SetLocalTime(util::UnixSecs secs);
void SetRemoteTime(context::Context* ctx, const peerid::PeerID& peer, util::UnixSecs secs) EXCLUDES(mu_);
util::UnixSecs GetTime(context::Context* ctx, const std::set<peerid::PeerID>& remotes) const EXCLUDES(mu_);
util::UnixSecs GetLocalTime() const;
private:
mutable util::mutex mu_;
std::atomic<util::UnixSecs> local_;
std::map<peerid::PeerID, util::UnixSecs> remotes_ GUARDED_BY(mu_);
};
} // namespace svr2::groupclock
#endif

View File

@ -0,0 +1,55 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP peerid
//TESTDEP sip
//TESTDEP sender
//TESTDEP context
//TESTDEP env
//TESTDEP env/test
//TESTDEP util
//TESTDEP metrics
//TESTDEP proto
//TESTDEP protobuf-lite
//TESTDEP libsodium
#include <gtest/gtest.h>
#include "groupclock/groupclock.h"
#include "env/env.h"
#include "context/context.h"
namespace svr2::groupclock {
class ClockTest : public ::testing::Test {
protected:
static void SetUpTestCase() {
env::Init();
}
context::Context ctx;
};
TEST_F(ClockTest, BasicUsage) {
Clock c;
EXPECT_EQ(0, c.GetTime(&ctx, std::set<peerid::PeerID>{}));
c.SetLocalTime(1000);
EXPECT_EQ(1000, c.GetTime(&ctx, std::set<peerid::PeerID>{}));
peerid::PeerID p1((uint8_t[32]){1});
peerid::PeerID p2((uint8_t[32]){2});
peerid::PeerID p3((uint8_t[32]){3});
peerid::PeerID p4((uint8_t[32]){4});
c.SetRemoteTime(&ctx, p1, 1001);
c.SetRemoteTime(&ctx, p2, 1002);
c.SetRemoteTime(&ctx, p3, 1003);
c.SetRemoteTime(&ctx, p4, 1004);
EXPECT_EQ(1001, c.GetTime(&ctx, std::set<peerid::PeerID>{p1}));
EXPECT_EQ(1001, c.GetTime(&ctx, std::set<peerid::PeerID>{p1, p2}));
EXPECT_EQ(1002, c.GetTime(&ctx, std::set<peerid::PeerID>{p1, p2, p3}));
EXPECT_EQ(1002, c.GetTime(&ctx, std::set<peerid::PeerID>{p1, p2, p3, p4}));
c.SetLocalTime(1005);
EXPECT_EQ(1003, c.GetTime(&ctx, std::set<peerid::PeerID>{p1, p2, p3, p4}));
c.SetRemoteTime(&ctx, p1, 1004);
EXPECT_EQ(1004, c.GetTime(&ctx, std::set<peerid::PeerID>{p1, p2, p3, p4}));
}
} // namespace svr2::groupclock

1
enclave/gtest/gtest-all.cc Symbolic link
View File

@ -0,0 +1 @@
../googletest/googletest/src/gtest-all.cc

1
enclave/gtest/gtest_main.cc Symbolic link
View File

@ -0,0 +1 @@
../googletest/googletest/src/gtest_main.cc

25
enclave/hmac/hmac.cc Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "hmac/hmac.h"
#include <sodium/crypto_auth_hmacsha256.h>
#include <string.h>
namespace svr2::hmac {
std::array<uint8_t, 32> Sha256(const std::string& input) {
crypto_hash_sha256_state sha;
crypto_hash_sha256_init(&sha);
crypto_hash_sha256_update(&sha, reinterpret_cast<const unsigned char*>(input.data()), input.size());
std::array<uint8_t, 32> out;
crypto_hash_sha256_final(&sha, out.data());
return out;
}
std::array<uint8_t, 32> HmacSha256(const std::array<uint8_t, 32>& key, const std::string& input) {
std::array<uint8_t, 32> out;
crypto_auth_hmacsha256(out.data(), reinterpret_cast<const unsigned char*>(input.data()), input.size(), key.data());
return out;
}
} // namespace svr2::hmac

17
enclave/hmac/hmac.h Normal file
View File

@ -0,0 +1,17 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_HMAC_HMAC_H__
#define __SVR2_HMAC_HMAC_H__
#include <array>
#include <string>
namespace svr2::hmac {
std::array<uint8_t, 32> Sha256(const std::string& input);
std::array<uint8_t, 32> HmacSha256(const std::array<uint8_t, 32>& key, const std::string& input);
} // namespace svr2::hmac
#endif // __SVR2_HMAC_HMAC_H__

View File

@ -0,0 +1,44 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP hmac
//TESTDEP noise-c
//TESTDEP libsodium
#include <array>
#include <string>
#include <gtest/gtest.h>
#include "hmac/hmac.h"
namespace svr2::hmac {
class HmacTest : public ::testing::Test {
};
TEST_F(HmacTest, BasicUsage) {
std::array<uint8_t, 32> key = {
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'1', '2'};
std::array<uint8_t, 32> out = HmacSha256(key, "abc");
// Python3:
// >>> import base64
// >>> import hmac
// >>> import hashlib
// >>> base64.b16encode(hmac.digest(b'12345678901234567890123456789012', b'abc', hashlib.sha256))
// b'26B7F4C64769835D3F654DC635D5362988C270883270E1EFD65372B5F3100BAF'
std::array<uint8_t, 32> expected = {
0x26, 0xB7, 0xF4, 0xC6, 0x47, 0x69, 0x83, 0x5D,
0x3F, 0x65, 0x4D, 0xC6, 0x35, 0xD5, 0x36, 0x29,
0x88, 0xC2, 0x70, 0x88, 0x32, 0x70, 0xE1, 0xEF,
0xD6, 0x53, 0x72, 0xB5, 0xF3, 0x10, 0x0B, 0xAF,
};
EXPECT_EQ(out, expected);
}
} // namespace svr2::hmac

1
enclave/libsodium Submodule

@ -0,0 +1 @@
Subproject commit fd5cbe9e696c1b886e45f3111dd099d51b12de6e

114
enclave/metrics/counters.h Normal file
View File

@ -0,0 +1,114 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// This file contains all counter metrics used within SVR2.
//
// They're created with the macro CREATE_COUNTER, which takes arguments:
// * ns - namespace of the counter (generally, module name)
// * varname - name of the variable used to reference this counter, must be
// unique within the namespace (ns)
// * name - name of the exported variable (actually, "ns.name")
// * tags - set of tags associated with this variable, either empty `({})`, or
// an initializer list `({{"foo", "bar"}, {"baz", "blah"}})` for tags
// foo=bar, baz=blah. Must be wrapped in parens.
//
// Once these counters are created here, they're used with the incantation:
// COUNTER(ns, varname)->CounterFunction();
// IE:
// COUNTER(sender, enclave_messages_sent)->IncrementBy(3);
//
// All counters created here will be exported to the host, even if they are
// zero. This differs from error counts, which are exported only if non-zero.
CREATE_COUNTER(ecalls, host_messages_received, host_messages_received, ({}))
CREATE_COUNTER(ecalls, host_bytes_received, host_bytes_received, ({}))
CREATE_COUNTER(ecalls, init_calls, init_calls, ({}))
CREATE_COUNTER(sender, enclave_messages_sent, enclave_messages_sent, ({}))
CREATE_COUNTER(sender, enclave_bytes_sent, enclave_bytes_sent, ({}))
CREATE_COUNTER(core, host_requests_received, msgs_received, ({{"type", "host_request"}}))
CREATE_COUNTER(core, peer_msgs_received, msgs_received, ({{"type", "peer_message"}}))
CREATE_COUNTER(core, timer_ticks_received, msgs_received, ({{"type", "timer_tick"}}))
CREATE_COUNTER(core, invalid_msgs_received, msgs_received, ({{"type", "invalid"}}))
CREATE_COUNTER(core, new_client_success, new_clients, ({{"outcome", "success"}}))
CREATE_COUNTER(core, new_client_failure, new_clients, ({{"outcome", "failure"}}))
CREATE_COUNTER(core, log_transactions_success, log_transactions, ({{"outcome", "success"}}))
CREATE_COUNTER(core, log_transactions_cancelled, log_transactions, ({{"outcome", "cancelled"}}))
CREATE_COUNTER(core, host_delete_success, host_delete, ({{"outcome", "success"}}))
CREATE_COUNTER(core, host_delete_failure, host_delete, ({{"outcome", "failure"}}))
CREATE_COUNTER(core, client_transaction_success, client_transaction, ({{"outcome", "success"}}))
CREATE_COUNTER(core, client_transaction_cancelled, client_transaction, ({{"outcome", "cancelled"}}))
CREATE_COUNTER(core, client_transaction_error, client_transaction, ({{"outcome", "error"}}))
CREATE_COUNTER(core, client_transaction_invalid, client_transaction, ({{"outcome", "invalid"}}))
CREATE_COUNTER(core, client_transaction_dne, client_transaction, ({{"outcome", "dne"}}))
CREATE_COUNTER(core, client_transaction_encrypterr, client_transaction, ({{"outcome", "encrypterr"}}))
CREATE_COUNTER(core, raft_log_applied, raft_log_applied, ({}))
CREATE_COUNTER(client, created, created, ({}))
CREATE_COUNTER(client, closed, closed, ({}))
CREATE_COUNTER(client, new_dh_state, new_dh_state, ({}))
CREATE_COUNTER(client, key_rotate_success, key_rotate, ({{"outcome", "success"}}))
CREATE_COUNTER(client, key_rotate_failure, key_rotate, ({{"outcome", "failure"}}))
CREATE_COUNTER(client, attestation_refresh_success, attestation_refresh, ({{"outcome", "success"}}))
CREATE_COUNTER(client, attestation_refresh_failure, attestation_refresh, ({{"outcome", "failure"}}))
CREATE_COUNTER(peers, attestation_refresh_success, attestation_refresh, ({{"outcome", "success"}}))
CREATE_COUNTER(peers, attestation_refresh_failure, attestation_refresh, ({{"outcome", "failure"}}))
CREATE_COUNTER(raft, logs_committed, logs_committed, ({}))
CREATE_COUNTER(raft, logs_promised, logs_promised, ({}))
CREATE_COUNTER(raft, vote_requests_received, msgs_received, ({{"type", "vote_request"}}))
CREATE_COUNTER(raft, vote_responses_received, msgs_received, ({{"type", "vote_response"}}))
CREATE_COUNTER(raft, append_requests_received, msgs_received, ({{"type", "append_request"}}))
CREATE_COUNTER(raft, append_responses_received, msgs_received, ({{"type", "append_response"}}))
CREATE_COUNTER(raft, timeout_nows_received, msgs_received, ({{"type", "timeout_now"}}))
CREATE_COUNTER(raft, invalid_requests_received, msgs_received, ({{"type", "invalid"}}))
CREATE_COUNTER(raft, term_updated, term_updated, ({}))
CREATE_COUNTER(raft, term_increments, term_increments, ({}))
CREATE_COUNTER(raft, logs_append_success, logs_appended, ({{"outcome", "success"}}))
CREATE_COUNTER(raft, logs_append_failure, logs_appended, ({{"outcome", "failure"}}))
CREATE_COUNTER(raft, election_timeouts, election_timeouts, ({}))
CREATE_COUNTER(timeout, timeouts_created, timeouts_created, ({}))
CREATE_COUNTER(timeout, timeouts_run, timeouts_completed, ({{"outcome", "run"}}))
CREATE_COUNTER(timeout, timeouts_cancelled, timeouts_completed, ({{"outcome", "cancelled"}}))
CREATE_COUNTER(context, cpu_uncategorized, cpu, ({{"in", "uncategorized"}, {"action", "uncategorized"}}))
CREATE_COUNTER(context, cpu_client_encrypt, cpu, ({{"in", "client"}, {"action", "encrypt"}}))
CREATE_COUNTER(context, cpu_client_decrypt, cpu, ({{"in", "client"}, {"action", "decrypt"}}))
CREATE_COUNTER(context, cpu_client_hs_start, cpu, ({{"in", "client"}, {"action", "hs_start"}}))
CREATE_COUNTER(context, cpu_client_hs_finish, cpu, ({{"in", "client"}, {"action", "hs_finish"}}))
CREATE_COUNTER(context, cpu_peer_encrypt, cpu, ({{"in", "peer"}, {"action", "encrypt"}}))
CREATE_COUNTER(context, cpu_peer_decrypt, cpu, ({{"in", "peer"}, {"action", "decrypt"}}))
CREATE_COUNTER(context, cpu_peer_connect, cpu, ({{"in", "peer"}, {"action", "connect"}}))
CREATE_COUNTER(context, cpu_peer_connect2, cpu, ({{"in", "peer"}, {"action", "connect2"}}))
CREATE_COUNTER(context, cpu_peer_accept, cpu, ({{"in", "peer"}, {"action", "accept"}}))
CREATE_COUNTER(context, cpu_db_client_request, cpu, ({{"in", "db"}, {"action", "client_request"}}))
CREATE_COUNTER(context, cpu_db_repl_send, cpu, ({{"in", "db"}, {"action", "repl_send"}}))
CREATE_COUNTER(context, cpu_db_repl_recv, cpu, ({{"in", "db"}, {"action", "repl_recv"}}))
CREATE_COUNTER(context, cpu_db_hash, cpu, ({{"in", "db"}, {"action", "hash"}}))
CREATE_COUNTER(context, cpu_core_client_msg, cpu, ({{"in", "core"}, {"action", "client_msg"}}))
CREATE_COUNTER(context, cpu_core_peer_msg, cpu, ({{"in", "core"}, {"action", "peer_msg"}}))
CREATE_COUNTER(context, cpu_core_host_msg, cpu, ({{"in", "core"}, {"action", "host_msg"}}))
CREATE_COUNTER(context, cpu_core_raft_msg, cpu, ({{"in", "core"}, {"action", "raft_msg"}}))
CREATE_COUNTER(context, cpu_core_e2e_txn_req, cpu, ({{"in", "core"}, {"action", "e2e_txn_req"}}))
CREATE_COUNTER(context, cpu_core_e2e_txn_resp, cpu, ({{"in", "core"}, {"action", "e2e_txn_resp"}}))
CREATE_COUNTER(context, cpu_core_repl_send, cpu, ({{"in", "core"}, {"action", "repl_send"}}))
CREATE_COUNTER(context, cpu_core_repl_recv, cpu, ({{"in", "core"}, {"action", "repl_recv"}}))
CREATE_COUNTER(context, cpu_core_committed_logs, cpu, ({{"in", "core"}, {"action", "committed_logs"}}))
CREATE_COUNTER(context, cpu_core_timer_tick, cpu, ({{"in", "core"}, {"action", "timer_tick"}}))
CREATE_COUNTER(context, cpu_test_database_entries, cpu, ({{"in", "core"}, {"action", "test_database_entries"}}))
CREATE_COUNTER(context, lock_core_raft, cpu, ({{"in", "core"}, {"action", "lock"}, {"lock", "core_raft"}}))
CREATE_COUNTER(context, lock_core_log_txns, cpu, ({{"in", "core"}, {"action", "lock"}, {"lock", "core_log_txns"}}))
CREATE_COUNTER(context, lock_core_e2e_txns, cpu, ({{"in", "core"}, {"action", "lock"}, {"lock", "core_e2e_txns"}}))
CREATE_COUNTER(context, lock_core_config, cpu, ({{"in", "core"}, {"action", "lock"}, {"lock", "core_config"}}))
CREATE_COUNTER(context, lock_groupclock, cpu, ({{"in", "groupclock"}, {"action", "lock"}, {"lock", "groupclock"}}))
CREATE_COUNTER(context, lock_timeout, cpu, ({{"in", "timeout"}, {"action", "lock"}, {"lock", "timeout"}}))
CREATE_COUNTER(context, lock_peermanager, cpu, ({{"in", "peer"}, {"action", "lock"}, {"lock", "peermanager"}}))
CREATE_COUNTER(context, lock_peer, cpu, ({{"in", "peer"}, {"action", "lock"}, {"lock", "peer"}}))
CREATE_COUNTER(context, lock_clientmanager, cpu, ({{"in", "client"}, {"action", "lock"}, {"lock", "clientmanager"}}))
CREATE_COUNTER(context, lock_client, cpu, ({{"in", "client"}, {"action", "lock"}, {"lock", "client"}}))
CREATE_COUNTER(context, lock_test, cpu, ({{"in", "test"}}))
CREATE_COUNTER(context, lock_socket_read, socket, ({{"in", "socket"}, {"action", "lock"}, {"lock", "read"}}))
CREATE_COUNTER(context, lock_socket_write, socket, ({{"in", "socket"}, {"action", "lock"}, {"lock", "write"}}))

51
enclave/metrics/gauges.h Normal file
View File

@ -0,0 +1,51 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// This file contains all gauge metrics used within SVR2.
//
// They're created with the macro CREATE_GAUGE, which takes arguments:
// * ns - namespace of the gauge (generally, module name)
// * varname - name of the variable used to reference this gauge, must be
// unique within the namespace (ns). Also the exported name.
//
// Once these gauges are created here, they're used with the incantation:
// GAUGE(ns, varname)->GaugeFunction();
// IE:
// GAUGE(sender, enclave_messages_sent)->Set(12);
//
// Gauges are only exported after their first Set call, to avoid sending up
// spurious invalid values to metrics. If Clear is called, they will no longer
// be exported.
CREATE_GAUGE(raft, role)
CREATE_GAUGE(raft, is_voting)
CREATE_GAUGE(raft, current_term)
CREATE_GAUGE(raft, commit_index)
CREATE_GAUGE(raft, promise_index)
CREATE_GAUGE(raft, log_oldest_stored_log_index)
CREATE_GAUGE(raft, log_last_log_term)
CREATE_GAUGE(raft, log_last_log_index)
CREATE_GAUGE(raft, log_size)
CREATE_GAUGE(raft, log_total_size)
CREATE_GAUGE(raft, log_entries)
CREATE_GAUGE(core, raft_state)
CREATE_GAUGE(core, last_index_applied_to_db)
CREATE_GAUGE(core, current_local_time)
CREATE_GAUGE(core, current_groupclock_time)
CREATE_GAUGE(peers, peers)
CREATE_GAUGE(client, clients)
CREATE_GAUGE(db, rows)
CREATE_GAUGE(timeout, timeouts)
CREATE_GAUGE(test, test1)
CREATE_GAUGE(test, test2)
CREATE_GAUGE(env, total_heap_size)
CREATE_GAUGE(env, allocated_heap_size)
CREATE_GAUGE(env, peak_heap_size)

100
enclave/metrics/metrics.cc Normal file
View File

@ -0,0 +1,100 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "metrics/metrics.h"
namespace svr2::metrics {
namespace {
static std::atomic<uint64_t> recorded_errors[error::Error_ARRAYSIZE] = {0};
} // namespace
MetricsPB AllAsPB() {
MetricsPB out;
for (int i = 0; i < error::Error_ARRAYSIZE; i++) {
if (error::Error_IsValid(i)) {
uint64_t v = recorded_errors[i].load();
if (v > 0) {
U64PB* counter = out.add_counters();
counter->set_name("errors");
(*counter->mutable_tags())["error"] = error::Error_Name(i);
counter->set_v(v);
}
}
}
for (int i = 0; i < COUNTERS_ARRAY_SIZE; i++) {
internal::counters[i].AddToMetrics(&out);
}
for (int i = 0; i < GAUGES_ARRAY_SIZE; i++) {
internal::gauges[i].AddToMetrics(&out);
}
return out;
}
void ClearAllForTest() {
for (int i = 0; i < error::Error_ARRAYSIZE; i++) {
recorded_errors[i].store(0);
}
for (int i = 0; i < COUNTERS_ARRAY_SIZE; i++) {
internal::counters[i].Clear();
}
}
Counter::Counter(const std::string& name, std::map<std::string, std::string>&& tags)
: name_(name), tags_(tags) {}
void Counter::IncrementBy(uint64_t v) {
v_.fetch_add(v);
}
void Counter::AddToMetrics(MetricsPB* pb) {
auto c = pb->add_counters();
c->set_name(name_);
c->set_v(v_.load());
for (auto iter = tags_.cbegin(); iter != tags_.cend(); ++iter) {
(*c->mutable_tags())[iter->first] = iter->second;
}
}
void Counter::Clear() {
v_.store(0);
}
Gauge::Gauge(const std::string& name)
: v_(UINT64_MAX), name_(name) {}
void Gauge::Set(uint64_t v) {
v_.store(v);
}
void Gauge::AddToMetrics(MetricsPB* pb) {
uint64_t v = v_.load();
if (v == UINT64_MAX) { return; }
auto c = pb->add_gauges();
c->set_name(name_);
c->set_v(v);
}
void Gauge::Clear() {
v_.store(UINT64_MAX);
}
namespace internal {
error::Error RecordError(error::Error e) {
recorded_errors[e].fetch_add(1);
return e;
}
Counter counters[COUNTERS_ARRAY_SIZE] = {
#define CREATE_COUNTER(ns, varname, name, tags) Counter(#ns "." #name, std::map<std::string, std::string>tags),
#include "counters.h"
#undef CREATE_COUNTER
};
Gauge gauges[GAUGES_ARRAY_SIZE] = {
#define CREATE_GAUGE(ns, name) Gauge(#ns "." #name),
#include "gauges.h"
#undef CREATE_GAUGE
};
} // namespace internal
} // namespace svr2::metrics

92
enclave/metrics/metrics.h Normal file
View File

@ -0,0 +1,92 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_METRICS_METRICS_H__
#define __SVR2_METRICS_METRICS_H__
#include <string>
#include <atomic>
#include "proto/metrics.pb.h"
#include "proto/error.pb.h"
namespace svr2::metrics {
// Export all global metrics as a single protobuf.
MetricsPB AllAsPB();
// Return all global metrics to an initial state. For testing only.
void ClearAllForTest();
// A counter provides a simple, atomic counter object that monotonically increases.
// We do not protect against overflows, but given that this is a 64-bit value, they
// would be pretty impressive.
class Counter {
public:
Counter(const std::string& name, std::map<std::string, std::string>&& tags);
void IncrementBy(uint64_t v);
inline void Increment() { IncrementBy(1); }
private:
friend MetricsPB AllAsPB();
friend void ClearAllForTest();
void AddToMetrics(MetricsPB* pb);
void Clear();
std::atomic<uint64_t> v_;
const std::string name_;
const std::map<std::string, std::string> tags_;
};
// A gauge provides a simple, atomic gauge object that can be set to arbitrary
// values. We save UINT64_MAX as a special invalid value.
class Gauge {
public:
Gauge(const std::string& name);
void Set(uint64_t v);
void Clear();
private:
friend MetricsPB AllAsPB();
friend void ClearAllForTest();
void AddToMetrics(MetricsPB* pb);
std::atomic<uint64_t> v_;
const std::string name_;
};
// We use the somewhat tricky counters.h/gauges.h file to generate a set of metricss
// that are both accessible to the rest of the code and iterable by this code.
// In short, we use a CREATE_COUNTER/CREATE_GAUGE macros, which we define/include/undef,
// both here and in metrics.cc, to generate the header and source parts of the metrics.
enum Counters {
#define CREATE_COUNTER(ns, varname, name, tags) CTR__##ns##__##varname,
#include "counters.h"
#undef CREATE_COUNTER
COUNTERS_ARRAY_SIZE,
};
enum Gauges {
#define CREATE_GAUGE(ns, name) GAG__##ns##__##name,
#include "gauges.h"
#undef CREATE_GAUGE
GAUGES_ARRAY_SIZE,
};
namespace internal {
error::Error RecordError(error::Error);
extern Counter counters[COUNTERS_ARRAY_SIZE];
extern Gauge gauges[GAUGES_ARRAY_SIZE];
} // namespace internal
} // namespace svr2::metrics
// COUNTER(ns, name) returns a pointer to a metrics::Counter based on the
// counter namespace/name as created in counters.h.
#define COUNTER(ns, name) (&::svr2::metrics::internal::counters[::svr2::metrics::CTR__##ns##__##name])
// GAUGE(ns, name) returns a pointer to a metrics::Gauge based on the
// gauge namespace/name as created in gauges.h.
#define GAUGE(ns, name) (&::svr2::metrics::internal::gauges[::svr2::metrics::GAG__##ns##__##name])
// COUNTED_ERROR counts an error within metrics, returning that same error.
// It's generally used like:
// return COUNTED_ERROR(Foo_Bar);
#define COUNTED_ERROR(x) ::svr2::metrics::internal::RecordError(error::x)
#endif // __SVR2_METRICS_METRICS_H__

View File

@ -0,0 +1,122 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP metrics
//TESTDEP proto
//TESTDEP protobuf-lite
#include <gtest/gtest.h>
#include "proto/error.pb.h"
#include "metrics/metrics.h"
namespace svr2::metrics {
class MetricsTest : public ::testing::Test {
protected:
void SetUp() {
ClearAllForTest();
}
const int FindCounter(const MetricsPB& pb, const std::string& name, const std::map<std::string, std::string>& tags) {
for (int i = 0; i < pb.counters_size(); i++) {
auto c = pb.counters(i);
if (name != c.name() || tags.size() != c.tags().size()) {
continue;
}
bool tags_equal = true;
for (auto iter = tags.cbegin(); iter != tags.cend() && tags_equal; ++iter) {
if (c.tags().count(iter->first) == 0 ||
c.tags().at(iter->first) != iter->second) {
tags_equal = false;
break;
}
}
if (!tags_equal) { continue; }
return i;
}
return -1;
}
const int FindGauge(const MetricsPB& pb, const std::string& name) {
for (int i = 0; i < pb.gauges_size(); i++) {
auto c = pb.gauges(i);
if (name == c.name()) { return i; }
}
return -1;
}
};
error::Error ReturnsGeneralUnimplemented() {
return COUNTED_ERROR(General_Unimplemented);
}
error::Error ReturnsCoreReInit() {
return COUNTED_ERROR(Core_ReInit);
}
TEST_F(MetricsTest, CountsReturnedErrors) {
for (int i = 0; i < 3; i++) {
ReturnsGeneralUnimplemented();
}
MetricsPB got = AllAsPB();
ASSERT_EQ(got.counters_size(), 1 + COUNTERS_ARRAY_SIZE);
auto c = got.counters(0);
ASSERT_EQ(c.v(), 3);
ASSERT_EQ(c.tags().at("error"), "General_Unimplemented");
for (int i = 0; i < 5; i++) {
ReturnsCoreReInit();
}
got = AllAsPB();
ASSERT_EQ(got.counters_size(), 2 + COUNTERS_ARRAY_SIZE);
c = got.counters(0);
ASSERT_EQ(c.v(), 3);
ASSERT_EQ(c.tags().at("error"), "General_Unimplemented");
c = got.counters(1);
ASSERT_EQ(c.v(), 5);
ASSERT_EQ(c.tags().at("error"), "Core_ReInit");
}
TEST_F(MetricsTest, Counters) {
COUNTER(core, peer_msgs_received)->Increment();
COUNTER(core, peer_msgs_received)->Increment();
COUNTER(core, peer_msgs_received)->Increment();
MetricsPB got = AllAsPB();
int i = FindCounter(got, "core.msgs_received", {{"type", "peer_message"}});
ASSERT_GE(i, 0);
auto c = got.counters(i);
ASSERT_EQ(c.name(), "core.msgs_received");
ASSERT_EQ(c.tags().size(), 1);
ASSERT_EQ(c.tags().at("type"), "peer_message");
ASSERT_EQ(c.v(), 3);
}
TEST_F(MetricsTest, Gauges) {
MetricsPB got = AllAsPB();
ASSERT_EQ(got.gauges_size(), 0);
GAUGE(test, test1)->Set(123);
got = AllAsPB();
ASSERT_EQ(got.gauges_size(), 1);
EXPECT_EQ(got.gauges(0).name(), "test.test1");
EXPECT_EQ(got.gauges(0).v(), 123);
GAUGE(test, test2)->Set(234);
GAUGE(test, test1)->Set(345);
got = AllAsPB();
ASSERT_EQ(got.gauges_size(), 2);
int t1 = FindGauge(got, "test.test1");
int t2 = FindGauge(got, "test.test2");
ASSERT_GE(t1, 0);
ASSERT_GE(t2, 0);
auto g1 = got.gauges(t1);
auto g2 = got.gauges(t2);
EXPECT_EQ(g1.name(), "test.test1");
EXPECT_EQ(g1.v(), 345);
EXPECT_EQ(g2.name(), "test.test2");
EXPECT_EQ(g2.v(), 234);
GAUGE(test, test1)->Clear();
got = AllAsPB();
ASSERT_EQ(got.gauges_size(), 1);
EXPECT_EQ(got.gauges(0).name(), "test.test2");
EXPECT_EQ(got.gauges(0).v(), 234);
}
} // namespace svr2::metrics

View File

@ -0,0 +1,144 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include <sys/socket.h>
#include <linux/vm_sockets.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include "env/env.h"
#include "core/core.h"
#include "context/context.h"
#include "proto/enclaveconfig.pb.h"
#include "util/log.h"
#include "util/bytes.h"
#include "proto/nitro.pb.h"
#include "socketwrap/socket.h"
#include "env/nsm/nsm.h"
#include "queue/queue.h"
namespace svr2 {
#define RETURN_ERRNO_ERROR_IF(x, err) do { \
if ((x)) { \
int e = errno; \
LOG(ERROR) << "(" << #x << ") evaluated to false, errno(" << e << "): " << strerror(e); \
return COUNTED_ERROR(err); \
} \
} while (0)
// To simplify our server, this function creates the appropriate
// AF_VSOCK, binds it, listens, accepts, then returns the accepted
// file descriptor, closing the listener. We know that if this
// socket dies, we stop serving, so there's no need to create an
// accept loop.
error::Error AcceptSocket(int* afd) {
int fd;
RETURN_ERRNO_ERROR_IF(
0 >= (fd = socket(AF_VSOCK, SOCK_STREAM, 0)),
Nitro_SocketCreation);
struct sockaddr_vm my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.svm_family = AF_VSOCK;
my_addr.svm_port = VMADDR_PORT_ANY;
my_addr.svm_cid = VMADDR_CID_ANY;
RETURN_ERRNO_ERROR_IF(
0 != bind(fd, (struct sockaddr *) &my_addr, sizeof(my_addr)),
Nitro_SocketBind);
RETURN_ERRNO_ERROR_IF(
0 != listen(fd, 2),
Nitro_SocketListen);
*afd = 0;
while (*afd <= 0) {
struct sockaddr_vm remote_addr;
socklen_t remote_len = sizeof(remote_addr);
*afd = accept4(fd, reinterpret_cast<struct sockaddr*>(&remote_addr), &remote_len, SOCK_CLOEXEC);
RETURN_ERRNO_ERROR_IF(
*afd <= 0 && errno != EINTR && errno != ECONNABORTED,
Nitro_SocketAccept);
}
shutdown(fd, SHUT_RDWR);
close(fd);
return error::OK;
}
error::Error RunServerThread(core::Core* core, socketwrap::Socket* sock) {
while (true) {
context::Context ctx;
auto in = ctx.Protobuf<nitro::InboundMessage>();
RETURN_IF_ERROR(sock->ReadPB(&ctx, in));
if (in->inner_case() != nitro::InboundMessage::kMsg) {
return COUNTED_ERROR(Nitro_InboundNotMessage);
}
auto msg = ctx.Protobuf<UntrustedMessage>();
if (!msg->ParseFromString(in->mutable_msg()->data())) {
return COUNTED_ERROR(Nitro_InboundMessageParse);
}
auto status = core->Receive(&ctx, *msg);
auto out = ctx.Protobuf<nitro::OutboundMessage>();
auto out_msg = out->mutable_msg();
out_msg->set_id(in->msg().id());
out_msg->set_status(status);
RETURN_IF_ERROR(sock->WritePB(&ctx, *out));
}
}
// Read an init message from a socket and use it to create a new core object.
std::pair<std::unique_ptr<core::Core>, error::Error> InitCore(socketwrap::Socket* sock) {
context::Context ctx;
auto init = ctx.Protobuf<nitro::InboundMessage>();
if (error::Error err = sock->ReadPB(&ctx, init); err != error::OK) {
return std::make_pair(nullptr, err);
}
if (init->inner_case() != nitro::InboundMessage::kInit) {
return std::make_pair(nullptr, COUNTED_ERROR(Nitro_InboundNotInit));
}
auto [core_ptr, err] = core::Core::Create(
&ctx,
init->init());
if (err == error::OK) {
auto out = ctx.Protobuf<nitro::OutboundMessage>();
core_ptr->ID().ToString(out->mutable_init()->mutable_peer_id());
err = sock->WritePB(&ctx, *out);
}
return std::make_pair(std::move(core_ptr), err);
}
// Run a server, returning an error when it dies.
error::Error RunServer() {
int fd;
RETURN_IF_ERROR(AcceptSocket(&fd));
socketwrap::Socket sock(fd);
auto sockp = &sock;
std::vector<std::thread> threads;
threads.emplace_back([sockp]{
LOG(FATAL) << env::nsm::SendNsmMessages(sockp);
});
auto [c, err] = InitCore(&sock);
RETURN_IF_ERROR(err);
auto cp = c.get();
for (size_t i = 0; i < 32 /* chosen by random dice roll */; i++) {
threads.emplace_back([cp, sockp]{
LOG(FATAL) << RunServerThread(cp, sockp);
});
}
for (size_t i = 0; i < threads.size(); i++) {
threads[i].join();
}
return error::OK; // unreachable
}
error::Error Run() {
env::Init();
return RunServer();
}
} // namespace svr2
int main(int argc, char** argv) {
LOG(FATAL) << svr2::Run();
return -1;
}

1
enclave/noise-c Submodule

@ -0,0 +1 @@
Subproject commit 354193847d04475e474a89dbb11b6434e1d9cbca

73
enclave/noise/noise.cc Normal file
View File

@ -0,0 +1,73 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "noise/noise.h"
#include <string.h>
#include "util/log.h"
#include "metrics/metrics.h"
namespace svr2::noise {
static size_t max_message_size = 65535;
std::pair<std::string, error::Error> Encrypt(NoiseCipherState* cs, const std::string& plaintext) {
std::string ciphertext;
size_t mac_size = noise_cipherstate_get_mac_length(cs);
size_t max_encrypt_size = max_message_size - mac_size;
size_t orig_size = plaintext.size();
// We need to fit our plaintext into some number of Noise output packets.
// Each of those packets cannot be larger than max_message_size, and must
// contain some amount of ciphertext along with Noise's added MAC.
// Thus, we have to add some amount of size equivilent to a multiple of
// mac_size to the size of plaintext to get the final size of *ciphertext.
// Examples of input sizes and output sizes, around the max_message_size
// boundary, are:
// size == 1 : add mac_size * 1 -> [cleartext(1B)][mac]
// size == max_message_size - mac_size : add mac_size * 1 -> [cleartext(max_msg_sizeB)][mac]
// size == max_message_size - mac_size + 1 : add mac_size * 2 -> [cleartext(max_msg_sizeB)][mac1][cleartext(1B)][mac2]
size_t num_macs = orig_size / max_encrypt_size + 1;
if (orig_size % max_encrypt_size == 0 && num_macs > 1) num_macs--;
size_t macs_size = mac_size * num_macs;
size_t final_size = orig_size + macs_size;
ciphertext.resize(final_size, 0);
size_t plaintext_start = 0;
for (size_t start = 0; start < final_size; start += max_message_size) {
size_t plaintext_size = std::min(max_encrypt_size, plaintext.size() - plaintext_start);
memcpy(StrU8Ptr(&ciphertext) + start, StrU8Ptr(plaintext) + plaintext_start, plaintext_size);
plaintext_start += plaintext_size;
NoiseBuffer buf;
noise_buffer_set_inout(buf, StrU8Ptr(&ciphertext) + start, plaintext_size, plaintext_size + mac_size);
if (NOISE_ERROR_NONE != noise_cipherstate_encrypt(cs, &buf)) {
return std::make_pair("", COUNTED_ERROR(Peers_Encrypt));
}
}
return std::make_pair(ciphertext, error::OK);
}
std::pair<std::string, error::Error> Decrypt(NoiseCipherState* cs, const std::string& ciphertext) {
std::string plaintext(ciphertext.size(), 0);
size_t plaintext_start = 0;
// Data comes in as [ciphertext][mac][ciphertext][mac].
for (size_t start = 0; start < ciphertext.size(); start += max_message_size) {
size_t size = std::min(max_message_size, ciphertext.size() - start);
memcpy(StrU8Ptr(&plaintext) + plaintext_start, StrU8Ptr(ciphertext) + start, size);
NoiseBuffer buf;
noise_buffer_set_inout(buf, StrU8Ptr(&plaintext) + plaintext_start, size, size);
if (NOISE_ERROR_NONE != noise_cipherstate_decrypt(cs, &buf)) {
return std::make_pair("", COUNTED_ERROR(Peers_Decrypt));
}
plaintext_start += buf.size;
}
plaintext.resize(plaintext_start, 0);
return std::make_pair(plaintext, error::OK);
}
DHState CloneDHState(const DHState& s) {
NoiseDHState* sp = nullptr;
auto dh_id = noise_dhstate_get_dh_id(s.get());
CHECK(NOISE_ERROR_NONE == noise_dhstate_new_by_id(&sp, dh_id));
CHECK(NOISE_ERROR_NONE == noise_dhstate_copy(sp, s.get()));
return WrapDHState(sp);
}
} // namespace svr2::noise

76
enclave/noise/noise.h Normal file
View File

@ -0,0 +1,76 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_NOISE_NOISE_H__
#define __SVR2_NOISE_NOISE_H__
#include <string>
#include <noise/protocol/buffer.h>
#include <noise/protocol/handshakestate.h>
#include <noise/protocol/dhstate.h>
#include <noise/protocol/cipherstate.h>
#include <noise/protocol/randstate.h>
#include "proto/error.pb.h"
// This module provides simple RAII wrappers around noise-c pointers.
// The pointers are exposed publicly as .state, allowing use of noise_* functions
// directly on them, but with the guarantee that when the *State objects fall
// out of scope, the correct noise_*_free function will be called on them.
#include "util/macros.h"
namespace svr2::noise {
const size_t HANDSHAKE_INIT_SIZE = 64;
inline uint8_t* StrU8Ptr(std::string* s) {
return reinterpret_cast<uint8_t*>(s->data());
}
inline const uint8_t* StrU8Ptr(const std::string& s) {
return reinterpret_cast<const uint8_t*>(s.data());
}
inline NoiseBuffer BufferOutputFromString(std::string* s) {
NoiseBuffer b;
noise_buffer_set_output(b, StrU8Ptr(s), s->size());
return b;
}
inline NoiseBuffer BufferInputFromString(std::string* s) {
NoiseBuffer b;
noise_buffer_set_input(b, StrU8Ptr(s), s->size());
return b;
}
inline NoiseBuffer BufferInoutFromString(std::string* s, size_t substr) {
CHECK(substr <= s->size());
NoiseBuffer b;
noise_buffer_set_inout(b, StrU8Ptr(s), substr, s->size());
return b;
}
typedef std::unique_ptr<NoiseHandshakeState, int(*)(NoiseHandshakeState*)> HandshakeState;
inline HandshakeState WrapHandshakeState(NoiseHandshakeState* s) {
return HandshakeState(s, noise_handshakestate_free);
}
typedef std::unique_ptr<NoiseDHState, int(*)(NoiseDHState*)> DHState;
inline DHState WrapDHState(NoiseDHState* s) {
return DHState(s, noise_dhstate_free);
}
DHState CloneDHState(const DHState& s);
typedef std::unique_ptr<NoiseCipherState, int(*)(NoiseCipherState*)> CipherState;
inline CipherState WrapCipherState(NoiseCipherState* s) {
return CipherState(s, noise_cipherstate_free);
}
// Encrypt the given string.
std::pair<std::string, error::Error> Encrypt(NoiseCipherState* cs, const std::string& plaintext);
// Decrypt the given string.
std::pair<std::string, error::Error> Decrypt(NoiseCipherState* cs, const std::string& ciphertext);
} // namespace svr2::noise
#endif // __SVR2_NOISE_NOISE_H__

View File

@ -0,0 +1,100 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP noise
//TESTDEP noise-c
//TESTDEP noisewrap
//TESTDEP env
//TESTDEP util
//TESTDEP env/test
//TESTDEP env
//TESTDEP metrics
//TESTDEP proto
//TESTDEP protobuf-lite
//TESTDEP libsodium
#include <gtest/gtest.h>
#include <noise/protocol/cipherstate.h>
#include "noise/noise.h"
#include "env/env.h"
#include "util/log.h"
#include "proto/error.pb.h"
#include "util/cpu.h"
namespace svr2::noise {
class CipherStateTest : public ::testing::Test {
protected:
static void SetUpTestCase() {
env::Init();
}
void EncryptDecrypt(const std::string& plaintext, std::string* ciphertext_out, int type) {
std::array<uint8_t, 32> key = {1};
NoiseCipherState* s1n;
NoiseCipherState* s2n;
ASSERT_EQ(NOISE_ERROR_NONE, noise_cipherstate_new_by_id(&s1n, type));
ASSERT_EQ(NOISE_ERROR_NONE, noise_cipherstate_init_key(s1n, key.data(), key.size()));
ASSERT_EQ(NOISE_ERROR_NONE, noise_cipherstate_new_by_id(&s2n, type));
ASSERT_EQ(NOISE_ERROR_NONE, noise_cipherstate_init_key(s2n, key.data(), key.size()));
noise::CipherState s1 = noise::WrapCipherState(s1n);
noise::CipherState s2 = noise::WrapCipherState(s2n);
auto [ciphertext, enc_err] = noise::Encrypt(s1n, plaintext);
ASSERT_EQ(error::OK, enc_err);
auto [computed_plaintext, dec_err] = noise::Decrypt(s2n, ciphertext);
ASSERT_EQ(error::OK, dec_err);
ASSERT_EQ(plaintext, computed_plaintext);
ciphertext_out->swap(ciphertext);
}
};
TEST_F(CipherStateTest, EncryptDecrypt) {
std::string ciphertext;
EncryptDecrypt("", &ciphertext, NOISE_CIPHER_CHACHAPOLY);
ASSERT_EQ(16, ciphertext.size());
EncryptDecrypt("a", &ciphertext, NOISE_CIPHER_CHACHAPOLY);
ASSERT_EQ(17, ciphertext.size());
EncryptDecrypt("this is a test of the emergency broadcast system", &ciphertext, NOISE_CIPHER_CHACHAPOLY);
std::string s;
s.resize(65535-16, 'a');
EncryptDecrypt(s, &ciphertext, NOISE_CIPHER_CHACHAPOLY);
ASSERT_EQ(ciphertext.size(), 65535);
s.resize(65535-15, 'a');
EncryptDecrypt(s, &ciphertext, NOISE_CIPHER_CHACHAPOLY);
ASSERT_EQ(ciphertext.size(), 65535-15+32);
s.resize((65535-16)*10, 'a');
EncryptDecrypt(s, &ciphertext, NOISE_CIPHER_CHACHAPOLY);
ASSERT_EQ(ciphertext.size(), 65535*10);
}
TEST_F(CipherStateTest, BenchmarkChaChaPoly) {
std::string plaintext;
std::string ciphertext;
plaintext.resize(1 << 20, 'a');
auto start = util::asm_rdtsc();
int times = 100;
for (int i = 0; i < times; i++) {
EncryptDecrypt(plaintext, &ciphertext, NOISE_CIPHER_CHACHAPOLY);
}
LOG(INFO) << "took " << ((util::asm_rdtsc() - start) * 1.0 / (times * plaintext.size())) << " cycles/byte";
}
TEST_F(CipherStateTest, BenchmarkAesGcm) {
std::string plaintext;
std::string ciphertext;
plaintext.resize(1 << 20, 'a');
auto start = util::asm_rdtsc();
int times = 100;
for (int i = 0; i < times; i++) {
EncryptDecrypt(plaintext, &ciphertext, NOISE_CIPHER_AESGCM);
}
LOG(INFO) << "took " << ((util::asm_rdtsc() - start) * 1.0 / (times * plaintext.size())) << " cycles/byte";
}
} // namespace svr2::noise

View File

@ -0,0 +1,33 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP noise-c
//TESTDEP noisewrap
//TESTDEP util
//TESTDEP env
//TESTDEP env/test
//TESTDEP proto
//TESTDEP protobuf-lite
//TESTDEP libsodium
#include <stdio.h>
#include <gtest/gtest.h>
#include "env/env.h"
#include "util/log.h"
#include <noise/protocol/randstate.h>
#include <noise/protocol/constants.h>
#include "util/hex.h"
namespace svr2 {
TEST(NoiseWrap, RandomnessIsWrappedDeterministically) {
svr2::env::Init();
std::array<uint8_t, 8> out;
ASSERT_EQ(NOISE_ERROR_NONE, noise_randstate_generate_simple(out.data(), out.size()));
LOG(INFO) << "RAND: " << util::ToHex(out);
uint8_t expect[8] = {0x4f, 0x6f, 0xa8, 0x48, 0x32, 0xaa, 0x7d, 0x32};
ASSERT_EQ(0, memcmp(out.data(), expect, 8));
}
} // namespace svr2

15
enclave/noisewrap/wrap.cc Normal file
View File

@ -0,0 +1,15 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include "env/env.h"
#include "proto/error.pb.h"
#include "util/macros.h"
extern "C" {
// Wrap Noise's call to get randomness so it uses our enclave's random generator.
void __wrap_noise_rand_bytes(void* bytes, size_t size) {
CHECK(::svr2::error::OK == ::svr2::env::environment->RandomBytes(bytes, size));
}
} // extern "C"

49
enclave/peerid/peerid.cc Normal file
View File

@ -0,0 +1,49 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#include <ostream>
#include <functional>
#include "peerid/peerid.h"
#include "sip/halfsiphash.h"
#include "util/log.h"
#include "metrics/metrics.h"
#include "util/hex.h"
namespace svr2::peerid {
static std::array<uint8_t, 32> zero_id = {0};
size_t PeerIDHasher::operator()(const PeerID& id) const {
return Hash(id.id_.data(), id.id_.size());
}
PeerID::PeerID(const uint8_t array[32]) {
std::copy(array, array+32, id_.begin());
}
PeerID::PeerID() : id_({0}) {}
error::Error PeerID::FromString(const std::string& s) {
if (s.size() != id_.size()) {
return COUNTED_ERROR(Peers_InvalidID);
}
std::copy(s.begin(), s.end(), id_.begin());
return error::OK;
}
bool PeerID::Valid() const {
// https://cr.yp.to/ecdh.html#validate
return id_ != zero_id;
}
void PeerID::ToString(std::string* s) const {
s->resize(32, 0);
std::copy(id_.begin(), id_.end(), s->begin());
}
std::string PeerID::DebugString() const {
return util::PrefixToHex(id_, 4);
}
std::ostream& operator<<(std::ostream& os, const PeerID& peer_id) {
os << peer_id.DebugString();
return os;
}
} // namespace svr2::peerid

54
enclave/peerid/peerid.h Normal file
View File

@ -0,0 +1,54 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
#ifndef __SVR2_PEERID_PEERID_H__
#define __SVR2_PEERID_PEERID_H__
#include <string>
#include <array>
#include <memory>
#include "context/context.h"
#include "proto/error.pb.h"
#include "sip/hasher.h"
namespace svr2::peerid {
class PeerID;
class PeerIDHasher : public sip::Hasher {
public:
size_t operator()(const PeerID& id) const;
};
class PeerID {
public:
PeerID(PeerID&& moved) = default;
PeerID(const PeerID& copied) = default;
PeerID& operator=(const PeerID& other) = default;
PeerID(); // all zeros, invalid
PeerID(const uint8_t array[32]);
error::Error FromString(const std::string& s);
void ToString(std::string* s) const;
const std::array<uint8_t, 32>& Get() const { return id_; }
bool Valid() const;
bool operator==(const PeerID& other) const { return id_ == other.id_; }
bool operator!=(const PeerID& other) const { return id_ != other.id_; }
bool operator<(const PeerID& other) const { return id_ < other.id_; }
std::string DebugString() const;
std::string AsString() const { std::string out; ToString(&out); return out; }
// Prints DebugString() to an ostream. Overload is acceptable because
// PeerID represents a value and DebugString() does not expose any implementation
// details of the object (https://google.github.io/styleguide/cppguide.html#Streams)
friend std::ostream& operator<<(std::ostream& os, const PeerID& peer_id);
private:
std::array<uint8_t, 32> id_;
friend class PeerIDHasher;
};
} // namespace svr2::peerid
#endif // __SVR2_PEERID_PEERID_H__

View File

@ -0,0 +1,107 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//TESTDEP gtest
//TESTDEP sip
//TESTDEP sender
//TESTDEP env
//TESTDEP env/test
//TESTDEP util
//TESTDEP context
//TESTDEP metrics
//TESTDEP proto
//TESTDEP protobuf-lite
//TESTDEP libsodium
#include <gtest/gtest.h>
#include "peers/peers.h"
#include "env/env.h"
#include "util/log.h"
#include "proto/e2e.pb.h"
#include <memory>
#include <iostream>
#include <array>
namespace svr2::peerid {
class PeerIDTest : public ::testing::Test {
protected:
static void SetUpTestCase() {
env::Init();
}
};
TEST_F(PeerIDTest, Valid) {
PeerID id;
ASSERT_FALSE(id.Valid());
std::string more_valid = "12345678901234567890123456789012";
ASSERT_EQ(error::OK, id.FromString(more_valid));
ASSERT_TRUE(id.Valid());
}
TEST_F(PeerIDTest, FromString) {
PeerID id;
std::string valid = "12345678901234567890123456789012";
ASSERT_EQ(error::OK, id.FromString(valid));
std::array<uint8_t, 32> expected = {
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2',
};
ASSERT_EQ(expected, id.Get());
ASSERT_NE(error::OK, id.FromString("badstring"));
// We can set the string to invalid (all zeros), and FromString will still succeed.
ASSERT_EQ(error::OK, id.FromString(std::string("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 32)));
}
TEST_F(PeerIDTest, FromArray) {
uint8_t in[32] = {1};
PeerID id(in);
std::array<uint8_t, 32> expected = {
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
ASSERT_EQ(expected, id.Get());
}
TEST_F(PeerIDTest, Equality) {
PeerID id1, id2;
std::string valid = "12345678901234567890123456789012";
std::string valid2 = "00045678901234567890123456789012";
ASSERT_EQ(error::OK, id1.FromString(valid));
ASSERT_EQ(error::OK, id2.FromString(valid));
ASSERT_TRUE(id1 == id2);
ASSERT_EQ(error::OK, id2.FromString(valid2));
ASSERT_FALSE(id1 == id2);
ASSERT_EQ(error::OK, id1.FromString(valid2));
ASSERT_TRUE(id1 == id2);
}
TEST_F(PeerIDTest, DebugString) {
PeerID id;
ASSERT_EQ(id.DebugString(), "00000000");
uint8_t in[32] = {1, 2, 3};
id = PeerID(in);
ASSERT_EQ(id.DebugString(), "01020300");
}
TEST_F(PeerIDTest, Copy) {
PeerID id1;
std::string valid = "12345678901234567890123456789012";
ASSERT_EQ(error::OK, id1.FromString(valid));
PeerID id2 = id1;
ASSERT_TRUE(id1 == id2);
}
TEST_F(PeerIDTest, Mapping) {
std::unordered_map<PeerID, uint8_t, PeerIDHasher> map;
for (uint8_t i = 1; i <= 10; i++) {
uint8_t in[32] = {i};
map[PeerID(in)] = i;
}
for (uint8_t i = 1; i <= 10; i++) {
uint8_t in[32] = {i};
ASSERT_EQ(map[PeerID(in)], i);
}
}
} // namespace svr2::peerid

Some files were not shown because too many files have changed in this diff Show More