Key transparency server
This commit is contained in:
commit
a3732f0c03
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
||||
.git
|
||||
.github
|
||||
filter-key-updates/target
|
||||
cmd/kt-server/kt-server
|
||||
cmd/kt-client/kt-client
|
||||
cmd/kt-stress/kt-stress
|
||||
cmd/generate-keys/generate-keys
|
||||
example/db
|
||||
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@ -0,0 +1,21 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
ij_go_add_parentheses_for_single_import = true
|
||||
ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true
|
||||
ij_go_group_stdlib_imports = true
|
||||
ij_go_import_sorting = goimports
|
||||
ij_go_local_group_mode = project
|
||||
ij_go_local_package_prefixes = "github.com/signalapp/keytransparency"
|
||||
ij_go_move_all_imports_in_one_declaration = true
|
||||
ij_go_move_all_stdlib_imports_in_one_group = true
|
||||
56
.github/workflows/push.yml
vendored
Normal file
56
.github/workflows/push.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
name: Build and push Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout main project
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_IAM_ROLE }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
|
||||
- name: Login to ECR
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ${{ vars.ECR_REGISTRY }}
|
||||
|
||||
- name: Set up Go
|
||||
id: setup-go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 21
|
||||
cache: 'maven'
|
||||
|
||||
- name: Test Go
|
||||
run: go test ./...
|
||||
|
||||
- name: Build and push the Docker image of the KT server
|
||||
run: |
|
||||
docker build . --file docker/Dockerfile --build-arg GO_VERSION=${{ steps.setup-go.outputs.go-version }} \
|
||||
--tag "${{ vars.ECR_REGISTRY }}/${{ vars.ECR_REPO }}:${GITHUB_REF_NAME}"
|
||||
docker push "${{ vars.ECR_REGISTRY }}/${{ vars.ECR_REPO }}:${GITHUB_REF_NAME}"
|
||||
|
||||
- name: Build and push filter key updates lambda artifact to S3
|
||||
working-directory: ./filter-key-updates
|
||||
run: |
|
||||
./mvnw -e -B deploy -Djgitver.use-version=${{ github.ref_name }} \
|
||||
-DbucketName=${{ vars.S3_BUCKET }} \
|
||||
-DbucketKey=${{ vars.S3_BUCKET_KEY }}
|
||||
36
.github/workflows/test.yml
vendored
Normal file
36
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: CI
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout main project
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Build Go
|
||||
run: go build ./...
|
||||
|
||||
- name: Test Go
|
||||
run: go test ./...
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 21
|
||||
cache: 'maven'
|
||||
|
||||
- name: Build and test Java
|
||||
run: ./mvnw verify
|
||||
working-directory: filter-key-updates
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
example/db/
|
||||
.idea
|
||||
cmd/kt-server/kt-server
|
||||
cmd/kt-client/kt-client
|
||||
cmd/kt-stress/kt-stress
|
||||
cmd/generate-keys/generate-keys
|
||||
619
LICENSE
Normal file
619
LICENSE
Normal file
@ -0,0 +1,619 @@
|
||||
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
|
||||
148
README.md
Normal file
148
README.md
Normal file
@ -0,0 +1,148 @@
|
||||
key-transparency-server
|
||||
========================
|
||||
|
||||
In an end-to-end encrypted messaging ecosystem, users must have some way to obtain the public keys of the users they wish to message.
|
||||
This typically happens by users uploading a mapping of their public identifier (for example, their phone number)
|
||||
to their public key to a central directory or service, and other users querying this service.
|
||||
However, this requires users to trust that the service operator will behave honestly and not tamper with the keys.
|
||||
|
||||
_Key transparency_ is a mechanism to ensure that a service operator cannot do this without being detected.
|
||||
It does so by sequentially recording updates to the mappings, where an update is either an insert of a new mapping
|
||||
from a search key to a value, or an update of an existing mapping.
|
||||
In this context, "search key" refers to something like "n:+18005550101", and the value it maps to could be the public key associated with that phone number.
|
||||
|
||||
Every update—also referred to as a log entry—is appended to the _key transparency log_,
|
||||
a globally consistent, cryptographically-protected, append-only log.
|
||||
Although the key transparency log is an append-only construct, callers will need to update existing search keys
|
||||
from time to time (e.g. if they lose their phone and wind up generating a new public key).
|
||||
The log allows for updates by appending new log entries that supersede previous entries for the same search key.
|
||||
|
||||
This repo defines a key transparency server that maintains such a log for Signal, as well as associated testing and development tools.
|
||||
It uses this [protocol][ietf-protocol] in the IETF's datatracker, although it
|
||||
accommodates Signal's specific application requirements and therefore contains some modifications.
|
||||
|
||||
[ietf-protocol]: https://datatracker.ietf.org/doc/draft-ietf-keytrans-protocol/
|
||||
|
||||
Repo Overview
|
||||
-------------
|
||||
Key transparency code:
|
||||
- `tree`: Defines the data structures used in the key transparency log, along with the logic to interact with them and generate proofs for various operations.
|
||||
|
||||
Operating the log:
|
||||
- `cmd/kt-server`: Defines the server logic for interacting with the key transparency log.
|
||||
- `db`: Defines various database and cache implementations.
|
||||
- `filter-key-updates`: Defines the lambda that reads account changes from a DynamoDB stream and writes relevant updates to a Kinesis stream.
|
||||
|
||||
Tools/testing:
|
||||
- `cmd/kt-client`: A tool for running various operations against a local or remote key transparency server.
|
||||
- `cmd/kt-stress`: A tool for stress testing a key transparency server.
|
||||
- `cmd/generate-auditing-test-vectors`: Generates test vectors for an auditor implementation.
|
||||
- `cmd/generate-keys`: Generates a new set of private keys needed to run the key transparency server.
|
||||
|
||||
Key Transparency Services
|
||||
-------------------------
|
||||
|
||||
The responsibilities of the key transparency server are split into three services:
|
||||
|
||||
- `KeyTransparencyQueryService` is a read-only server. It contains the `Search` and `Monitor` endpoints that clients use to query the log.
|
||||
- `KeyTransparencyService` is a read-and-write server. It contains the audit endpoints and internally also writes updates to the log.
|
||||
- `KeyTransparencyTestService` is a write-only server. It contains the `Update` endpoint and only exists for local testing and development use by `kt-client` and `kt-stress`.
|
||||
|
||||
All three services communicate via [gRPC](https://grpc.io/). Their definitions can be found in the `cmd/kt-server/pb` directory.
|
||||
|
||||
Data Structures
|
||||
---------------
|
||||
The key transparency log consists of two main types of data structures: a _log tree_ and _prefix trees_.
|
||||
|
||||
Log entries are stored as leaves in the log tree, and prefix trees are used to facilitate
|
||||
efficient lookups of a specific mapping in the large and ever-growing log tree.
|
||||
The transparency log uses various cryptographic techniques to append to the log tree and update prefix trees in a verifiable manner.
|
||||
|
||||
More details about each data structure and how they're used can be found in the [protocol][ietf-protocol].
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
To run all tests:
|
||||
|
||||
```go
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Quickstart
|
||||
----------
|
||||
You can run a key transparency server locally using [LevelDB](https://github.com/google/leveldb) as a backing database.
|
||||
To do so, first run the `generate-keys` command to generate a new set of private keys:
|
||||
|
||||
```shell
|
||||
go run github.com/signalapp/keytransparency/cmd/generate-keys
|
||||
```
|
||||
|
||||
Copy-paste the keys into `example/config.yaml`. Then run the read-only, audit, and test servers locally (all three are required for
|
||||
`kt-client` to have full functionality):
|
||||
|
||||
```shell
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-server -config ./example/config.yaml
|
||||
```
|
||||
|
||||
You can now access metrics and the transparency
|
||||
servers at the configured addresses. You can interact with the transparency log
|
||||
via `kt-client`:
|
||||
|
||||
```shell
|
||||
# Add an ACI to the log
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-client update aci \
|
||||
<UUID> <base64_encoded_aci_identity_key>
|
||||
|
||||
# Add an E164 to the log. Be sure to use the same ACI as a previous update.
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-client update aci \
|
||||
<e164_formatted_number> <UUID>
|
||||
|
||||
# Search for the ACI and provide the value it's mapped to
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-client search \
|
||||
<UUID> <base64_encoded_aci_identity_key>
|
||||
|
||||
# Look up the distinguished key
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-client distinguished
|
||||
```
|
||||
|
||||
To look up a username hash, you must also provide the ACI and ACI identity key:
|
||||
```shell
|
||||
# Search for an E164
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-client \
|
||||
-username_hash <base64url_encoded_username_hash> \
|
||||
search <UUID> <base64_encoded_aci_identity_key>
|
||||
|
||||
```
|
||||
|
||||
To look up an E164, you must provide the ACI, the ACI identity key, and an unidentified access key.
|
||||
If using the `mock` AccountDB configuration, the default `-uak` value matches the one used by the mock implementation and can be left out.
|
||||
```shell
|
||||
# Search for an E164
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-client \
|
||||
-e164 <e164_formatted_number> -uak <base64_encoded_uak> \
|
||||
search <UUID> <base64_encoded_aci_identity_key>
|
||||
```
|
||||
|
||||
To time the latency of a search or monitor request:
|
||||
```shell
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-client -sample-size 50 -num-samples 10 \
|
||||
search-timing <UUID> <base64_encoded_aci_identity_key>
|
||||
|
||||
# Note that this will make a search request first so that it can compute the commitment index
|
||||
# necessary for a monitor request
|
||||
go run github.com/signalapp/keytransparency/cmd/kt-client -sample-size 50 -num-samples 10 \
|
||||
monitor-timing <UUID> <base64_encoded_aci_identity_key>
|
||||
```
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
- The `docs/` folder contains details on the format of the data stored in the database.
|
||||
- [Key Transparency Architecture](https://datatracker.ietf.org/doc/draft-ietf-keytrans-architecture/) describes
|
||||
the various deployment modes of key transparency and how to apply it to messaging and other applications.
|
||||
|
||||
You can also read these academic papers for background:
|
||||
|
||||
- [Merkle^2: A Low-Latency Transparency Log System](https://eprint.iacr.org/2021/453)
|
||||
- [CONIKS: Bringing Key Transparency to End Users](https://eprint.iacr.org/2014/1004)
|
||||
243
cmd/generate-auditing-test-vectors/main.go
Normal file
243
cmd/generate-auditing-test-vectors/main.go
Normal file
@ -0,0 +1,243 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
//go:generate protoc -I ./pb -I ../../tree/transparency/pb --go_out=pb --go_opt=paths=source_relative --go-grpc_out=pb --go-grpc_opt=paths=source_relative vectors.proto
|
||||
|
||||
// Command generate-auditing-test-vectors outputs a file in the current working
|
||||
// directory that contains test vectors for an auditor implementation.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/generate-auditing-test-vectors/pb"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
transparency_test "github.com/signalapp/keytransparency/tree/transparency/test"
|
||||
)
|
||||
|
||||
func random() []byte {
|
||||
out := make([]byte, 16)
|
||||
if _, err := rand.Read(out); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
var output []*pb.TestVectors_ShouldFailTestVector
|
||||
|
||||
// first proof type must be newTree
|
||||
{
|
||||
tree, _, _, _ := transparency_test.NewTree(nil, transparency.ContactMonitoring)
|
||||
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
|
||||
updates, _, _ := tree.Audit(0, 1000)
|
||||
updates = updates[1:]
|
||||
|
||||
output = append(output, &pb.TestVectors_ShouldFailTestVector{
|
||||
Description: "first proof type must be newTree",
|
||||
Updates: updates,
|
||||
})
|
||||
}
|
||||
|
||||
// first proof type must be a real update
|
||||
{
|
||||
tree, _, _, _ := transparency_test.NewTree(nil, transparency.ContactMonitoring)
|
||||
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
|
||||
updates, _, _ := tree.Audit(0, 1000)
|
||||
updates[0].Real = false
|
||||
|
||||
output = append(output, &pb.TestVectors_ShouldFailTestVector{
|
||||
Description: "first proof type must be real update",
|
||||
Updates: updates,
|
||||
})
|
||||
}
|
||||
|
||||
// newTree proof cannot be given for a non-empty tree
|
||||
{
|
||||
tree, _, _, _ := transparency_test.NewTree(nil, transparency.ContactMonitoring)
|
||||
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
|
||||
updates, _, _ := tree.Audit(0, 1000)
|
||||
updates[1] = updates[0]
|
||||
|
||||
output = append(output, &pb.TestVectors_ShouldFailTestVector{
|
||||
Description: "newTree proof cannot be given for a non-empty tree",
|
||||
Updates: updates,
|
||||
})
|
||||
}
|
||||
|
||||
// differentKey must match old root
|
||||
{
|
||||
tree, _, _, _ := transparency_test.NewTree(nil, transparency.ContactMonitoring)
|
||||
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
|
||||
updates, _, _ := tree.Audit(0, 1000)
|
||||
updates[2].Proof.Proof.(*tpb.AuditorProof_DifferentKey_).DifferentKey.OldSeed[0] ^= 1
|
||||
|
||||
output = append(output, &pb.TestVectors_ShouldFailTestVector{
|
||||
Description: "differentKey must match old root",
|
||||
Updates: updates,
|
||||
})
|
||||
}
|
||||
|
||||
// sameKey must match old root
|
||||
{
|
||||
tree, _, _, _ := transparency_test.NewTree(nil, transparency.ContactMonitoring)
|
||||
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
newKey := random()
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: newKey, Value: random()})
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: newKey, Value: random()})
|
||||
|
||||
updates, _, _ := tree.Audit(0, 1000)
|
||||
updates[2].Proof.Proof.(*tpb.AuditorProof_SameKey_).SameKey.Position += 1
|
||||
|
||||
output = append(output, &pb.TestVectors_ShouldFailTestVector{
|
||||
Description: "sameKey must match old root",
|
||||
Updates: updates,
|
||||
})
|
||||
}
|
||||
|
||||
// proof may not be sameKey if update type is fake
|
||||
{
|
||||
tree, _, _, _ := transparency_test.NewTree(nil, transparency.ContactMonitoring)
|
||||
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: random(), Value: random()})
|
||||
newKey := random()
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: newKey, Value: random()})
|
||||
tree.UpdateSimple(&tpb.UpdateRequest{SearchKey: newKey, Value: random()})
|
||||
|
||||
updates, _, _ := tree.Audit(0, 1000)
|
||||
updates[2].Real = false
|
||||
|
||||
output = append(output, &pb.TestVectors_ShouldFailTestVector{
|
||||
Description: "proof may not be sameKey if update type is fake",
|
||||
Updates: updates,
|
||||
})
|
||||
}
|
||||
|
||||
// happy path
|
||||
var successVector *pb.TestVectors_ShouldSucceedTestVector
|
||||
{
|
||||
tree, store, _, _ := transparency_test.NewTree(nil, transparency.ContactMonitoring)
|
||||
transparency_test.RandomTree(tree, store, 10, []int{3}, []int{4, 9})
|
||||
updates, _, _ := tree.Audit(0, 1000)
|
||||
|
||||
vector := &pb.TestVectors_ShouldSucceedTestVector{}
|
||||
for i, update := range updates {
|
||||
root, err := tree.GetLogTree().GetRoot(uint64(i + 1))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
vector.Updates = append(vector.Updates, &pb.TestVectors_ShouldSucceedTestVector_UpdateAndHash{
|
||||
Update: update,
|
||||
LogRoot: root,
|
||||
})
|
||||
}
|
||||
successVector = vector
|
||||
}
|
||||
|
||||
// signature
|
||||
var signatureVector *pb.TestVectors_SignatureTestVector
|
||||
{
|
||||
auditorName := "example-auditor"
|
||||
_, _, config, _ := transparency_test.NewTree(nil, transparency.ThirdPartyAuditing)
|
||||
auditorPub, auditorPriv, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
config.AuditorKeys = map[string]ed25519.PublicKey{
|
||||
auditorName: auditorPub,
|
||||
}
|
||||
|
||||
head, signatureInput, _ := transparency.SignNewAuditorHead(auditorPriv, config.Public(), 1337, make([]byte, 32), auditorName)
|
||||
|
||||
// Encode auditor public and private key for Java.
|
||||
auditorPub, err = x509.MarshalPKIXPublicKey(auditorPub)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
auditorPriv, err = x509.MarshalPKCS8PrivateKey(auditorPriv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sigKey, err := x509.MarshalPKIXPublicKey(config.SigKey.Public())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
vrfKey, err := x509.MarshalPKIXPublicKey(ed25519.PublicKey(config.VrfKey.Public().([]byte)))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
signatureVector = &pb.TestVectors_SignatureTestVector{
|
||||
AuditorPrivKey: auditorPriv,
|
||||
|
||||
DeploymentMode: uint32(config.Mode),
|
||||
SigPubKey: sigKey,
|
||||
AuditorPubKey: auditorPub,
|
||||
VrfPubKey: vrfKey,
|
||||
|
||||
TreeSize: 1337,
|
||||
Timestamp: head.Timestamp,
|
||||
Root: make([]byte, 32),
|
||||
|
||||
Signature: head.Signature,
|
||||
SignatureInput: signatureInput,
|
||||
}
|
||||
}
|
||||
|
||||
fmtProof := func(proof *tpb.AuditorProof) string {
|
||||
switch p := proof.Proof.(type) {
|
||||
case *tpb.AuditorProof_NewTree_:
|
||||
return "newTree{}"
|
||||
case *tpb.AuditorProof_DifferentKey_:
|
||||
return fmt.Sprintf("differentKey{copath: %v, old_seed: %x}", len(p.DifferentKey.Copath), p.DifferentKey.OldSeed)
|
||||
case *tpb.AuditorProof_SameKey_:
|
||||
return fmt.Sprintf("sameKey{copath: %v, ctr: %v, pos: %v}", len(p.SameKey.Copath), p.SameKey.Counter, p.SameKey.Position)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
for _, updates := range output {
|
||||
for _, update := range updates.Updates {
|
||||
fmt.Printf("real=%v index=%x seed=%x commitment = %x proof = %v\n", update.Real, update.Index, update.Seed, update.Commitment, fmtProof(update.Proof))
|
||||
}
|
||||
fmt.Println("----")
|
||||
}
|
||||
|
||||
raw, err := proto.Marshal(&pb.TestVectors{
|
||||
ShouldFail: output,
|
||||
ShouldSucceed: successVector,
|
||||
Signature: signatureVector,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
} else if err := os.WriteFile("./kt_test_vectors.pb", raw, 0777); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
485
cmd/generate-auditing-test-vectors/pb/vectors.pb.go
Normal file
485
cmd/generate-auditing-test-vectors/pb/vectors.pb.go
Normal file
@ -0,0 +1,485 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v5.28.3
|
||||
// source: vectors.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
pb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type TestVectors struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ShouldFail []*TestVectors_ShouldFailTestVector `protobuf:"bytes,1,rep,name=should_fail,json=shouldFail,proto3" json:"should_fail,omitempty"`
|
||||
ShouldSucceed *TestVectors_ShouldSucceedTestVector `protobuf:"bytes,2,opt,name=should_succeed,json=shouldSucceed,proto3" json:"should_succeed,omitempty"`
|
||||
Signature *TestVectors_SignatureTestVector `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
func (x *TestVectors) Reset() {
|
||||
*x = TestVectors{}
|
||||
mi := &file_vectors_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TestVectors) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TestVectors) ProtoMessage() {}
|
||||
|
||||
func (x *TestVectors) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_vectors_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TestVectors.ProtoReflect.Descriptor instead.
|
||||
func (*TestVectors) Descriptor() ([]byte, []int) {
|
||||
return file_vectors_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *TestVectors) GetShouldFail() []*TestVectors_ShouldFailTestVector {
|
||||
if x != nil {
|
||||
return x.ShouldFail
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors) GetShouldSucceed() *TestVectors_ShouldSucceedTestVector {
|
||||
if x != nil {
|
||||
return x.ShouldSucceed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors) GetSignature() *TestVectors_SignatureTestVector {
|
||||
if x != nil {
|
||||
return x.Signature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestVectors_ShouldFailTestVector struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"`
|
||||
Updates []*pb.AuditorUpdate `protobuf:"bytes,2,rep,name=updates,proto3" json:"updates,omitempty"`
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldFailTestVector) Reset() {
|
||||
*x = TestVectors_ShouldFailTestVector{}
|
||||
mi := &file_vectors_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldFailTestVector) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TestVectors_ShouldFailTestVector) ProtoMessage() {}
|
||||
|
||||
func (x *TestVectors_ShouldFailTestVector) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_vectors_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TestVectors_ShouldFailTestVector.ProtoReflect.Descriptor instead.
|
||||
func (*TestVectors_ShouldFailTestVector) Descriptor() ([]byte, []int) {
|
||||
return file_vectors_proto_rawDescGZIP(), []int{0, 0}
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldFailTestVector) GetDescription() string {
|
||||
if x != nil {
|
||||
return x.Description
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldFailTestVector) GetUpdates() []*pb.AuditorUpdate {
|
||||
if x != nil {
|
||||
return x.Updates
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestVectors_ShouldSucceedTestVector struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Updates []*TestVectors_ShouldSucceedTestVector_UpdateAndHash `protobuf:"bytes,1,rep,name=updates,proto3" json:"updates,omitempty"`
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector) Reset() {
|
||||
*x = TestVectors_ShouldSucceedTestVector{}
|
||||
mi := &file_vectors_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TestVectors_ShouldSucceedTestVector) ProtoMessage() {}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_vectors_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TestVectors_ShouldSucceedTestVector.ProtoReflect.Descriptor instead.
|
||||
func (*TestVectors_ShouldSucceedTestVector) Descriptor() ([]byte, []int) {
|
||||
return file_vectors_proto_rawDescGZIP(), []int{0, 1}
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector) GetUpdates() []*TestVectors_ShouldSucceedTestVector_UpdateAndHash {
|
||||
if x != nil {
|
||||
return x.Updates
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestVectors_SignatureTestVector struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
AuditorPrivKey []byte `protobuf:"bytes,8,opt,name=auditor_priv_key,json=auditorPrivKey,proto3" json:"auditor_priv_key,omitempty"`
|
||||
DeploymentMode uint32 `protobuf:"varint,1,opt,name=deployment_mode,json=deploymentMode,proto3" json:"deployment_mode,omitempty"`
|
||||
SigPubKey []byte `protobuf:"bytes,2,opt,name=sig_pub_key,json=sigPubKey,proto3" json:"sig_pub_key,omitempty"`
|
||||
AuditorPubKey []byte `protobuf:"bytes,9,opt,name=auditor_pub_key,json=auditorPubKey,proto3" json:"auditor_pub_key,omitempty"`
|
||||
VrfPubKey []byte `protobuf:"bytes,3,opt,name=vrf_pub_key,json=vrfPubKey,proto3" json:"vrf_pub_key,omitempty"`
|
||||
TreeSize uint64 `protobuf:"varint,4,opt,name=tree_size,json=treeSize,proto3" json:"tree_size,omitempty"`
|
||||
Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
|
||||
Root []byte `protobuf:"bytes,6,opt,name=root,proto3" json:"root,omitempty"`
|
||||
Signature []byte `protobuf:"bytes,7,opt,name=signature,proto3" json:"signature,omitempty"`
|
||||
SignatureInput []byte `protobuf:"bytes,10,opt,name=signature_input,json=signatureInput,proto3" json:"signature_input,omitempty"`
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) Reset() {
|
||||
*x = TestVectors_SignatureTestVector{}
|
||||
mi := &file_vectors_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TestVectors_SignatureTestVector) ProtoMessage() {}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_vectors_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TestVectors_SignatureTestVector.ProtoReflect.Descriptor instead.
|
||||
func (*TestVectors_SignatureTestVector) Descriptor() ([]byte, []int) {
|
||||
return file_vectors_proto_rawDescGZIP(), []int{0, 2}
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetAuditorPrivKey() []byte {
|
||||
if x != nil {
|
||||
return x.AuditorPrivKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetDeploymentMode() uint32 {
|
||||
if x != nil {
|
||||
return x.DeploymentMode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetSigPubKey() []byte {
|
||||
if x != nil {
|
||||
return x.SigPubKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetAuditorPubKey() []byte {
|
||||
if x != nil {
|
||||
return x.AuditorPubKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetVrfPubKey() []byte {
|
||||
if x != nil {
|
||||
return x.VrfPubKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetTreeSize() uint64 {
|
||||
if x != nil {
|
||||
return x.TreeSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetTimestamp() int64 {
|
||||
if x != nil {
|
||||
return x.Timestamp
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetRoot() []byte {
|
||||
if x != nil {
|
||||
return x.Root
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetSignature() []byte {
|
||||
if x != nil {
|
||||
return x.Signature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors_SignatureTestVector) GetSignatureInput() []byte {
|
||||
if x != nil {
|
||||
return x.SignatureInput
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestVectors_ShouldSucceedTestVector_UpdateAndHash struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Update *pb.AuditorUpdate `protobuf:"bytes,1,opt,name=update,proto3" json:"update,omitempty"`
|
||||
LogRoot []byte `protobuf:"bytes,2,opt,name=log_root,json=logRoot,proto3" json:"log_root,omitempty"`
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector_UpdateAndHash) Reset() {
|
||||
*x = TestVectors_ShouldSucceedTestVector_UpdateAndHash{}
|
||||
mi := &file_vectors_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector_UpdateAndHash) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TestVectors_ShouldSucceedTestVector_UpdateAndHash) ProtoMessage() {}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector_UpdateAndHash) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_vectors_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TestVectors_ShouldSucceedTestVector_UpdateAndHash.ProtoReflect.Descriptor instead.
|
||||
func (*TestVectors_ShouldSucceedTestVector_UpdateAndHash) Descriptor() ([]byte, []int) {
|
||||
return file_vectors_proto_rawDescGZIP(), []int{0, 1, 0}
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector_UpdateAndHash) GetUpdate() *pb.AuditorUpdate {
|
||||
if x != nil {
|
||||
return x.Update
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TestVectors_ShouldSucceedTestVector_UpdateAndHash) GetLogRoot() []byte {
|
||||
if x != nil {
|
||||
return x.LogRoot
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_vectors_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_vectors_proto_rawDesc = []byte{
|
||||
0x0a, 0x0d, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
|
||||
0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x22, 0x83, 0x07, 0x0a, 0x0b, 0x54, 0x65, 0x73, 0x74, 0x56, 0x65, 0x63, 0x74,
|
||||
0x6f, 0x72, 0x73, 0x12, 0x42, 0x0a, 0x0b, 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x5f, 0x66, 0x61,
|
||||
0x69, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x56,
|
||||
0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x53, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x46, 0x61, 0x69,
|
||||
0x6c, 0x54, 0x65, 0x73, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0a, 0x73, 0x68, 0x6f,
|
||||
0x75, 0x6c, 0x64, 0x46, 0x61, 0x69, 0x6c, 0x12, 0x4b, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x75, 0x6c,
|
||||
0x64, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x24, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x53, 0x68,
|
||||
0x6f, 0x75, 0x6c, 0x64, 0x53, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x54, 0x65, 0x73, 0x74, 0x56,
|
||||
0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0d, 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x53, 0x75, 0x63,
|
||||
0x63, 0x65, 0x65, 0x64, 0x12, 0x3e, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72,
|
||||
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x56, 0x65,
|
||||
0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54,
|
||||
0x65, 0x73, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61,
|
||||
0x74, 0x75, 0x72, 0x65, 0x1a, 0x6f, 0x0a, 0x14, 0x53, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x46, 0x61,
|
||||
0x69, 0x6c, 0x54, 0x65, 0x73, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x20, 0x0a, 0x0b,
|
||||
0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x35,
|
||||
0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||
0x1b, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x41,
|
||||
0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x07, 0x75, 0x70,
|
||||
0x64, 0x61, 0x74, 0x65, 0x73, 0x1a, 0xc8, 0x01, 0x0a, 0x17, 0x53, 0x68, 0x6f, 0x75, 0x6c, 0x64,
|
||||
0x53, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x54, 0x65, 0x73, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f,
|
||||
0x72, 0x12, 0x4c, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
|
||||
0x28, 0x0b, 0x32, 0x32, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73,
|
||||
0x2e, 0x53, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x53, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x54, 0x65,
|
||||
0x73, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41,
|
||||
0x6e, 0x64, 0x48, 0x61, 0x73, 0x68, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x1a,
|
||||
0x5f, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x6e, 0x64, 0x48, 0x61, 0x73, 0x68,
|
||||
0x12, 0x33, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x1b, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e,
|
||||
0x41, 0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x06, 0x75,
|
||||
0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x5f, 0x72, 0x6f, 0x6f,
|
||||
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x52, 0x6f, 0x6f, 0x74,
|
||||
0x1a, 0xe6, 0x02, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54, 0x65,
|
||||
0x73, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x75, 0x64, 0x69,
|
||||
0x74, 0x6f, 0x72, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01,
|
||||
0x28, 0x0c, 0x52, 0x0e, 0x61, 0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x69, 0x76, 0x4b,
|
||||
0x65, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74,
|
||||
0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x64, 0x65, 0x70,
|
||||
0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x73,
|
||||
0x69, 0x67, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
|
||||
0x52, 0x09, 0x73, 0x69, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0f, 0x61,
|
||||
0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x09,
|
||||
0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x50, 0x75, 0x62,
|
||||
0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0b, 0x76, 0x72, 0x66, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b,
|
||||
0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x76, 0x72, 0x66, 0x50, 0x75, 0x62,
|
||||
0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65,
|
||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x74, 0x72, 0x65, 0x65, 0x53, 0x69, 0x7a, 0x65,
|
||||
0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x05, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x12,
|
||||
0x0a, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x72, 0x6f,
|
||||
0x6f, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18,
|
||||
0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65,
|
||||
0x12, 0x27, 0x0a, 0x0f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x69, 0x6e,
|
||||
0x70, 0x75, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x73, 0x69, 0x67, 0x6e, 0x61,
|
||||
0x74, 0x75, 0x72, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74,
|
||||
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x61, 0x70,
|
||||
0x70, 0x2f, 0x6b, 0x65, 0x79, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63,
|
||||
0x79, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x2d, 0x61,
|
||||
0x75, 0x64, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x76, 0x65, 0x63,
|
||||
0x74, 0x6f, 0x72, 0x73, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_vectors_proto_rawDescOnce sync.Once
|
||||
file_vectors_proto_rawDescData = file_vectors_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_vectors_proto_rawDescGZIP() []byte {
|
||||
file_vectors_proto_rawDescOnce.Do(func() {
|
||||
file_vectors_proto_rawDescData = protoimpl.X.CompressGZIP(file_vectors_proto_rawDescData)
|
||||
})
|
||||
return file_vectors_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_vectors_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_vectors_proto_goTypes = []any{
|
||||
(*TestVectors)(nil), // 0: TestVectors
|
||||
(*TestVectors_ShouldFailTestVector)(nil), // 1: TestVectors.ShouldFailTestVector
|
||||
(*TestVectors_ShouldSucceedTestVector)(nil), // 2: TestVectors.ShouldSucceedTestVector
|
||||
(*TestVectors_SignatureTestVector)(nil), // 3: TestVectors.SignatureTestVector
|
||||
(*TestVectors_ShouldSucceedTestVector_UpdateAndHash)(nil), // 4: TestVectors.ShouldSucceedTestVector.UpdateAndHash
|
||||
(*pb.AuditorUpdate)(nil), // 5: transparency.AuditorUpdate
|
||||
}
|
||||
var file_vectors_proto_depIdxs = []int32{
|
||||
1, // 0: TestVectors.should_fail:type_name -> TestVectors.ShouldFailTestVector
|
||||
2, // 1: TestVectors.should_succeed:type_name -> TestVectors.ShouldSucceedTestVector
|
||||
3, // 2: TestVectors.signature:type_name -> TestVectors.SignatureTestVector
|
||||
5, // 3: TestVectors.ShouldFailTestVector.updates:type_name -> transparency.AuditorUpdate
|
||||
4, // 4: TestVectors.ShouldSucceedTestVector.updates:type_name -> TestVectors.ShouldSucceedTestVector.UpdateAndHash
|
||||
5, // 5: TestVectors.ShouldSucceedTestVector.UpdateAndHash.update:type_name -> transparency.AuditorUpdate
|
||||
6, // [6:6] is the sub-list for method output_type
|
||||
6, // [6:6] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_vectors_proto_init() }
|
||||
func file_vectors_proto_init() {
|
||||
if File_vectors_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_vectors_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 5,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_vectors_proto_goTypes,
|
||||
DependencyIndexes: file_vectors_proto_depIdxs,
|
||||
MessageInfos: file_vectors_proto_msgTypes,
|
||||
}.Build()
|
||||
File_vectors_proto = out.File
|
||||
file_vectors_proto_rawDesc = nil
|
||||
file_vectors_proto_goTypes = nil
|
||||
file_vectors_proto_depIdxs = nil
|
||||
}
|
||||
46
cmd/generate-auditing-test-vectors/pb/vectors.proto
Normal file
46
cmd/generate-auditing-test-vectors/pb/vectors.proto
Normal file
@ -0,0 +1,46 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "github.com/signalapp/keytransparency/cmd/generate-auditing-test-vectors/pb";
|
||||
import "transparency.proto";
|
||||
|
||||
message TestVectors {
|
||||
message ShouldFailTestVector {
|
||||
string description = 1;
|
||||
repeated transparency.AuditorUpdate updates = 2;
|
||||
}
|
||||
|
||||
message ShouldSucceedTestVector {
|
||||
message UpdateAndHash {
|
||||
transparency.AuditorUpdate update = 1;
|
||||
bytes log_root = 2;
|
||||
}
|
||||
repeated UpdateAndHash updates = 1;
|
||||
}
|
||||
|
||||
message SignatureTestVector {
|
||||
bytes auditor_priv_key = 8;
|
||||
|
||||
uint32 deployment_mode = 1;
|
||||
bytes sig_pub_key = 2;
|
||||
bytes auditor_pub_key = 9;
|
||||
bytes vrf_pub_key = 3;
|
||||
|
||||
uint64 tree_size = 4;
|
||||
int64 timestamp = 5;
|
||||
bytes root = 6;
|
||||
|
||||
bytes signature = 7;
|
||||
bytes signature_input = 10;
|
||||
|
||||
// next = 11
|
||||
}
|
||||
|
||||
repeated ShouldFailTestVector should_fail = 1;
|
||||
ShouldSucceedTestVector should_succeed = 2;
|
||||
SignatureTestVector signature = 3;
|
||||
}
|
||||
42
cmd/generate-keys/main.go
Normal file
42
cmd/generate-keys/main.go
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Command generate-keys outputs fresh cryptographic keys for a Key Transparency Server.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
signingKey := make([]byte, ed25519.SeedSize)
|
||||
if _, err := rand.Read(signingKey); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Signing Key: %x\n", signingKey)
|
||||
|
||||
vrfPriv := make([]byte, ed25519.SeedSize)
|
||||
if _, err := rand.Read(vrfPriv); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("VRF Private Key: %x\n", vrfPriv)
|
||||
|
||||
prefixAesKey := make([]byte, 32)
|
||||
if _, err := rand.Read(prefixAesKey); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Prefix Aes Key: %x\n", prefixAesKey)
|
||||
|
||||
openingKey := make([]byte, 32)
|
||||
if _, err := rand.Read(openingKey); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Opening Key: %x\n", openingKey)
|
||||
}
|
||||
360
cmd/internal/config/config.go
Normal file
360
cmd/internal/config/config.go
Normal file
@ -0,0 +1,360 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/signalapp/keytransparency/crypto/vrf"
|
||||
edvrf "github.com/signalapp/keytransparency/crypto/vrf/ed25519"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// envstr is a string in the YAML config file that expands environment variables
|
||||
// when parsed.
|
||||
type envstr string
|
||||
|
||||
func (es *envstr) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var s string
|
||||
if err := unmarshal(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
*es = envstr(os.ExpandEnv(s))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es envstr) String() string { return string(es) }
|
||||
|
||||
// Config specifies the file format of config files.
|
||||
type Config struct {
|
||||
KtServiceConfig *ServiceConfig `yaml:"kt"`
|
||||
KtQueryServiceConfig *ServiceConfig `yaml:"kt-query"`
|
||||
KtTestServiceConfig *ServiceConfig `yaml:"kt-test"`
|
||||
|
||||
LogOutputFile envstr `yaml:"log-output"`
|
||||
MetricsAddr envstr `yaml:"metrics-addr"`
|
||||
DatadogAddr envstr `yaml:"datadog-addr"`
|
||||
HealthAddr envstr `yaml:"health-addr"`
|
||||
|
||||
APIConfig *APIConfig `yaml:"api"`
|
||||
StreamConfig *StreamConfig `yaml:"stream"`
|
||||
DatabaseConfig *DatabaseConfig `yaml:"db"`
|
||||
AccountDB string `yaml:"account-db"`
|
||||
CacheConfig *CacheConfig `yaml:"cache"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
PrefixSize int `yaml:"prefix-size"`
|
||||
LogSize int `yaml:"log-size"`
|
||||
TopSize int `yaml:"top-size"`
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
ServerAddr envstr `yaml:"server-addr"`
|
||||
// a map of headers to a list of authorized values. at least one header to value mapping must be present on client requests
|
||||
AuthorizedHeaders map[string][]string `yaml:"authorized-headers"`
|
||||
// a map of header values to auditor name. each key in this map should match a value in the AuthorizedHeaders map.
|
||||
HeaderValueToAuditorName map[string]string `yaml:"header-value-to-auditor-name"`
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
SigningKey envstr `yaml:"signing-key"` // 32 byte hex-encoded seed for the signing private key.
|
||||
signingKey ed25519.PrivateKey
|
||||
|
||||
VRFKey envstr `yaml:"vrf-key"` // PEM encoded VRF private key.
|
||||
vrfKey vrf.PrivateKey
|
||||
|
||||
PrefixAesKey envstr `yaml:"prefix-key"` // 32 random hex-encoded bytes.
|
||||
prefixAesKey []byte
|
||||
|
||||
OpeningKey envstr `yaml:"opening-key"` // 32 random hex-encoded bytes.
|
||||
openingKey []byte
|
||||
|
||||
FakeUpdates *FakeUpdates `yaml:"fake"`
|
||||
|
||||
// A map of auditor name to its hex-encoded public signature key.
|
||||
AuditorConfigs map[string]envstr `yaml:"auditors"`
|
||||
auditorConfigs map[string]ed25519.PublicKey
|
||||
|
||||
Distinguished time.Duration `yaml:"distinguished"`
|
||||
|
||||
// Minimum latency for a search request
|
||||
MinimumSearchDelay time.Duration `yaml:"min-search-delay"`
|
||||
// Minimum latency for a monitor request
|
||||
MinimumMonitorDelay time.Duration `yaml:"min-monitor-delay"`
|
||||
// What percent of the minimum delay to use for determining the jitter range
|
||||
JitterPercent int `yaml:"jitter-percent"`
|
||||
}
|
||||
|
||||
func (config *APIConfig) TreeConfig() *transparency.PrivateConfig {
|
||||
mode := transparency.ContactMonitoring
|
||||
if config.auditorConfigs != nil {
|
||||
mode = transparency.ThirdPartyAuditing
|
||||
}
|
||||
return &transparency.PrivateConfig{
|
||||
Mode: mode,
|
||||
SigKey: config.signingKey,
|
||||
AuditorKeys: config.auditorConfigs,
|
||||
VrfKey: config.vrfKey,
|
||||
PrefixAesKey: config.prefixAesKey,
|
||||
OpeningKey: config.openingKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (config *APIConfig) NewTree(tx db.TransparencyStore) (*transparency.Tree, error) {
|
||||
return transparency.NewTree(config.TreeConfig(), tx)
|
||||
}
|
||||
|
||||
// FakeUpdates specifies how often to make fake updates. Updates are made such
|
||||
// that there are `count` updates total every `interval` of time.
|
||||
type FakeUpdates struct {
|
||||
Count int `yaml:"count"`
|
||||
Interval time.Duration `yaml:"interval"`
|
||||
}
|
||||
|
||||
type StreamConfig struct {
|
||||
Name envstr `yaml:"name"`
|
||||
// If Name is provided but TableName is not, backfill will not be attempted.
|
||||
TableName envstr `yaml:"table"`
|
||||
InitialHorizon time.Duration `yaml:"initial-horizon"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
// LevelDB
|
||||
File string `yaml:"file"`
|
||||
|
||||
// DynamoDB
|
||||
Table envstr `yaml:"table"`
|
||||
Parallel int `yaml:"parallel"`
|
||||
}
|
||||
|
||||
func (config *DatabaseConfig) Validate() error {
|
||||
if config == nil {
|
||||
return fmt.Errorf("field not provided: db")
|
||||
}
|
||||
|
||||
level := config.File != ""
|
||||
dynamo := config.Table != "" && config.Parallel != 0
|
||||
|
||||
if !level && !dynamo {
|
||||
return fmt.Errorf("no database connection information provided")
|
||||
} else if level && dynamo {
|
||||
return fmt.Errorf("can not provide both leveldb and dynamodb connections")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config *DatabaseConfig) Connect() (db.TransparencyStore, error) {
|
||||
if config.File != "" {
|
||||
return db.NewLDBTransparencyStore(config.File)
|
||||
}
|
||||
return db.NewDynamoDBTransparencyStore(config.Table.String(), config.Parallel)
|
||||
}
|
||||
|
||||
func (c *Config) ConnectAccountDB() (db.AccountDB, error) {
|
||||
if c.AccountDB == "mock" {
|
||||
return &db.MockAccountDB{}, nil
|
||||
}
|
||||
return db.NewAccountDB(c.AccountDB)
|
||||
}
|
||||
|
||||
func Read(filename string) (*Config, error) {
|
||||
// Read from file and parse.
|
||||
raw, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var parsed Config
|
||||
if err := yaml.Unmarshal(raw, &parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check that all required fields are populated.
|
||||
if parsed.KtQueryServiceConfig != nil {
|
||||
if parsed.KtQueryServiceConfig.ServerAddr == "" {
|
||||
return nil, fmt.Errorf("field not provided for service kt-query: server-addr")
|
||||
}
|
||||
if parsed.APIConfig.MinimumSearchDelay == 0 {
|
||||
return nil, fmt.Errorf("field not provided for service kt-query: min-search-delay")
|
||||
}
|
||||
if parsed.APIConfig.MinimumMonitorDelay == 0 {
|
||||
return nil, fmt.Errorf("field not provided for service kt-query: min-monitor-delay")
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.KtServiceConfig != nil {
|
||||
if parsed.KtServiceConfig.ServerAddr == "" {
|
||||
return nil, fmt.Errorf("field not provided for service kt: server-addr")
|
||||
}
|
||||
if parsed.KtServiceConfig.AuthorizedHeaders == nil || len(parsed.KtServiceConfig.AuthorizedHeaders) == 0 {
|
||||
return nil, fmt.Errorf("field not provided for service kt: authorized-headers")
|
||||
}
|
||||
if parsed.KtServiceConfig.HeaderValueToAuditorName == nil || len(parsed.KtServiceConfig.HeaderValueToAuditorName) == 0 {
|
||||
return nil, fmt.Errorf("field not provided for service kt: header-value-to-auditor-name")
|
||||
}
|
||||
if parsed.APIConfig.AuditorConfigs == nil || len(parsed.APIConfig.AuditorConfigs) == 0 {
|
||||
return nil, fmt.Errorf("field not provided for service kt: auditors")
|
||||
}
|
||||
// Ensure every header value maps to an auditor name
|
||||
for _, values := range parsed.KtServiceConfig.AuthorizedHeaders {
|
||||
for _, value := range values {
|
||||
if len(parsed.KtServiceConfig.HeaderValueToAuditorName[value]) == 0 {
|
||||
return nil, fmt.Errorf("header value %s has no associated auditor name", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure every auditor name maps to a public key
|
||||
for _, auditorName := range parsed.KtServiceConfig.HeaderValueToAuditorName {
|
||||
if len(parsed.APIConfig.AuditorConfigs[auditorName]) == 0 {
|
||||
return nil, fmt.Errorf("auditor %s has no associated public key", auditorName)
|
||||
}
|
||||
}
|
||||
if parsed.APIConfig.Distinguished == 0 {
|
||||
return nil, fmt.Errorf("field not provided for service kt: distinguished")
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.KtTestServiceConfig != nil {
|
||||
if parsed.KtTestServiceConfig.ServerAddr == "" {
|
||||
return nil, fmt.Errorf("field not provided for service kt-test: server-addr")
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.KtServiceConfig == nil && parsed.KtQueryServiceConfig == nil && parsed.KtTestServiceConfig == nil {
|
||||
return nil, fmt.Errorf("at least one server-addr field must be provided")
|
||||
} else if parsed.MetricsAddr == "" {
|
||||
return nil, fmt.Errorf("field not provided: metrics-addr")
|
||||
} else if parsed.HealthAddr == "" {
|
||||
return nil, fmt.Errorf("field not provided: health-addr")
|
||||
} else if parsed.APIConfig == nil {
|
||||
return nil, fmt.Errorf("field not provided: api")
|
||||
} else if parsed.APIConfig.SigningKey == "" {
|
||||
return nil, fmt.Errorf("field not provided: api.signing-key")
|
||||
} else if parsed.APIConfig.VRFKey == "" {
|
||||
return nil, fmt.Errorf("field not provided: api.vrf-key")
|
||||
} else if parsed.APIConfig.PrefixAesKey == "" {
|
||||
return nil, fmt.Errorf("field not provided: api.prefix-key")
|
||||
} else if parsed.APIConfig.OpeningKey == "" {
|
||||
return nil, fmt.Errorf("field not provided: api.opening-key")
|
||||
} else if err := parsed.DatabaseConfig.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if parsed.APIConfig.FakeUpdates != nil {
|
||||
if parsed.APIConfig.FakeUpdates.Count == 0 {
|
||||
return nil, fmt.Errorf("field not provided: api.fake.count")
|
||||
} else if parsed.APIConfig.FakeUpdates.Interval == 0 {
|
||||
return nil, fmt.Errorf("field not provided: api.fake.interval")
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.StreamConfig != nil {
|
||||
if parsed.StreamConfig.Name == "" {
|
||||
return nil, fmt.Errorf("field not provided: stream.name")
|
||||
} else if parsed.StreamConfig.InitialHorizon == 0 {
|
||||
return nil, fmt.Errorf("field not provided: stream.initial-horizon")
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.APIConfig.JitterPercent < 0 || parsed.APIConfig.JitterPercent > 100 {
|
||||
return nil, fmt.Errorf("jitter percent must be between 0 and 100")
|
||||
}
|
||||
|
||||
// Parse cryptographic keys.
|
||||
seed, err := hex.DecodeString(parsed.APIConfig.SigningKey.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse signing key: %v", err)
|
||||
} else if len(seed) != ed25519.SeedSize {
|
||||
return nil, fmt.Errorf("signing key is wrong size: wanted=%v, got=%v", ed25519.SeedSize, len(seed))
|
||||
}
|
||||
parsed.APIConfig.signingKey = ed25519.NewKeyFromSeed(seed)
|
||||
|
||||
vrfKey, err := hex.DecodeString(parsed.APIConfig.VRFKey.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse vrf key: %v", err)
|
||||
}
|
||||
parsed.APIConfig.vrfKey, err = edvrf.NewVRFSigner(vrfKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse vrf key: %v", err)
|
||||
}
|
||||
|
||||
prefixAesKey, err := hex.DecodeString(parsed.APIConfig.PrefixAesKey.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parsed prefix seed: %v", err)
|
||||
} else if len(prefixAesKey) != 32 {
|
||||
return nil, fmt.Errorf("prefix AES key is wrong size: wanted=%v, got=%v", 32, len(prefixAesKey))
|
||||
}
|
||||
parsed.APIConfig.prefixAesKey = prefixAesKey
|
||||
|
||||
openingKey, err := hex.DecodeString(parsed.APIConfig.OpeningKey.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parsed opening key: %v", err)
|
||||
} else if len(openingKey) != 32 {
|
||||
return nil, fmt.Errorf("opening key is wrong size: wanted=%v, got=%v", 32, len(openingKey))
|
||||
}
|
||||
parsed.APIConfig.openingKey = openingKey
|
||||
|
||||
parsed.APIConfig.auditorConfigs = map[string]ed25519.PublicKey{}
|
||||
for auditorName, publicKey := range parsed.APIConfig.AuditorConfigs {
|
||||
pubKey, err := hex.DecodeString(publicKey.String())
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse auditor public key: %v for auditor %s", err, auditorName)
|
||||
} else if len(pubKey) != ed25519.PublicKeySize {
|
||||
return nil, fmt.Errorf("auditor public key is wrong size: wanted=%v, got=%v for auditor %s", ed25519.PublicKeySize, len(pubKey), auditorName)
|
||||
}
|
||||
parsed.APIConfig.auditorConfigs[auditorName] = pubKey
|
||||
}
|
||||
|
||||
// If unspecified, use default cache sizes
|
||||
if parsed.CacheConfig == nil {
|
||||
parsed.CacheConfig = &CacheConfig{
|
||||
PrefixSize: 20000,
|
||||
LogSize: 2000,
|
||||
TopSize: 2000,
|
||||
}
|
||||
}
|
||||
|
||||
if parsed.CacheConfig.PrefixSize == 0 {
|
||||
parsed.CacheConfig.PrefixSize = 20000
|
||||
}
|
||||
|
||||
if parsed.CacheConfig.LogSize == 0 {
|
||||
parsed.CacheConfig.LogSize = 2000
|
||||
}
|
||||
|
||||
if parsed.CacheConfig.TopSize == 0 {
|
||||
parsed.CacheConfig.TopSize = 2000
|
||||
}
|
||||
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
func (c *Config) SetLogOutput() error {
|
||||
if c.LogOutputFile != "" {
|
||||
f, err := os.OpenFile(c.LogOutputFile.String(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open log file: %v", err)
|
||||
}
|
||||
log.Default().SetOutput(io.MultiWriter(os.Stderr, f))
|
||||
go func() {
|
||||
for range time.Tick(time.Second) {
|
||||
f.Sync()
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
12
cmd/internal/util/constants.go
Normal file
12
cmd/internal/util/constants.go
Normal file
@ -0,0 +1,12 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package util
|
||||
|
||||
const (
|
||||
AciPrefix = 'a'
|
||||
NumberPrefix = 'n'
|
||||
UsernameHashPrefix = 'u'
|
||||
)
|
||||
53
cmd/internal/util/logger.go
Normal file
53
cmd/internal/util/logger.go
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
var instance *Logger
|
||||
|
||||
func SetLoggerInstance(l *zerolog.Logger) {
|
||||
instance = &Logger{l}
|
||||
}
|
||||
|
||||
func Log() *Logger {
|
||||
if instance == nil {
|
||||
instance = _defaultLogger()
|
||||
instance.Warnf("default logger in use. SetLoggerInstance() should be called first")
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
func _defaultLogger() *Logger {
|
||||
zeroLogLogger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Timestamp().Logger()
|
||||
|
||||
return &Logger{&zeroLogLogger}
|
||||
}
|
||||
|
||||
func (l *Logger) Infof(format string, v ...interface{}) {
|
||||
l.logger.Info().Msgf(format, v...)
|
||||
}
|
||||
|
||||
func (l *Logger) Warnf(format string, v ...interface{}) {
|
||||
l.logger.Warn().Msgf(format, v...)
|
||||
}
|
||||
|
||||
func (l *Logger) Errorf(format string, v ...interface{}) {
|
||||
l.logger.Error().Msgf(format, v...)
|
||||
}
|
||||
|
||||
func (l *Logger) Fatalf(format string, v ...interface{}) {
|
||||
l.logger.Fatal().Msgf(format, v...)
|
||||
}
|
||||
45
cmd/kt-client/audit.go
Normal file
45
cmd/kt-client/audit.go
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
)
|
||||
|
||||
func handleAudit(client pb.KeyTransparencyServiceClient) {
|
||||
if flag.Arg(1) == "" {
|
||||
log.Fatal("No starting position given. Usage: kt-client audit <start> <limit>")
|
||||
} else if flag.Arg(2) == "" {
|
||||
log.Fatal("No entry limit given. Usage: kt-client audit <start> <limit>")
|
||||
}
|
||||
start, err := strconv.ParseUint(flag.Arg(1), 10, 64)
|
||||
checkErr("parsing starting position for audit request", err)
|
||||
|
||||
limit, err := strconv.ParseUint(flag.Arg(2), 10, 64)
|
||||
checkErr("parsing entry limit for audit request", err)
|
||||
|
||||
res, err := client.Audit(context.Background(), &pb.AuditRequest{Start: start, Limit: limit})
|
||||
checkErr("audit request", err)
|
||||
|
||||
p.Println("Updates:")
|
||||
for _, update := range res.Updates {
|
||||
p.Printf(" - real=%-5v index=%-64x seed=%x commitment=%x\n", update.Real, update.Index, update.Seed, update.Commitment)
|
||||
}
|
||||
if len(res.Updates) == 0 {
|
||||
p.Println(" (None)")
|
||||
}
|
||||
p.Println()
|
||||
if res.More {
|
||||
p.Printf("More available, starting at: %v\n", start+limit)
|
||||
} else {
|
||||
p.Println("Reached end of log.")
|
||||
}
|
||||
}
|
||||
40
cmd/kt-client/distinguished.go
Normal file
40
cmd/kt-client/distinguished.go
Normal file
@ -0,0 +1,40 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
)
|
||||
|
||||
func handleDistinguished(client pb.KeyTransparencyQueryServiceClient) {
|
||||
req := new(pb.DistinguishedRequest)
|
||||
if *last >= 0 {
|
||||
x := uint64(*last)
|
||||
req.Last = &x
|
||||
}
|
||||
res, err := client.Distinguished(context.Background(), req)
|
||||
checkErr("distinguished request", err)
|
||||
|
||||
printFullTreeHead(res.TreeHead)
|
||||
p.Printf("Distinguished search response: \n")
|
||||
p.Printf("VRF: %x\n\n", res.Distinguished.VrfProof)
|
||||
printSearchProof(res.Distinguished.Search)
|
||||
p.Printf("Opening: %x\n", res.Distinguished.Opening)
|
||||
p.Printf("Value: %s\n\n", res.Distinguished.Value.Value)
|
||||
|
||||
if *configFile == "" {
|
||||
p.Printf("Verification skipped\n")
|
||||
} else {
|
||||
if err := transparency.VerifySearch(newStore(), createTreeSearchRequest([]byte("distinguished")), createTreeSearchResponse(res.Distinguished, res.TreeHead)); err != nil {
|
||||
p.Printf("Verification failed: %v\n", err)
|
||||
} else {
|
||||
p.Printf("Verification successful\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
166
cmd/kt-client/main.go
Normal file
166
cmd/kt-client/main.go
Normal file
@ -0,0 +1,166 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Command kt-client is a test/example client used for interacting with a
|
||||
// key transparency server.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/message"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
var (
|
||||
p = message.NewPrinter(message.MatchLanguage("en"))
|
||||
|
||||
ktQueryServerAddr = flag.String("query-addr", "localhost:8080", "Address of read-only server.")
|
||||
ktServerAddr = flag.String("kt-addr", "localhost:8082", "Address of read-write server.")
|
||||
testServerAddr = flag.String("test-addr", "localhost:8081", "Address of test server.")
|
||||
configFile = flag.String("config", "", "(Optional) Location of server config file.")
|
||||
|
||||
usernameHash = flag.String("username-hash", "", "Base64url encoded username hash")
|
||||
e164 = flag.String("e164", "", "E164-formatted phone number. Must be preceded with a '+'. E.g. +14155550101")
|
||||
uak = flag.String("uak", "", "Standard base64 encoded unidentified access key")
|
||||
timingNumSamples = flag.Int("num-samples", 5, "Number of samples to use for measuring timing of a query request")
|
||||
timingSampleSize = flag.Int("sample-size", 100, "Number of requests per sample to use for measuring timing of a query request")
|
||||
|
||||
last = flag.Int("last", -1, "(Optional) Size of tree when last observed, or -1 for none.")
|
||||
)
|
||||
|
||||
func consistency(x *int) *tpb.Consistency {
|
||||
if *x == -1 {
|
||||
return &tpb.Consistency{}
|
||||
} else if *x < -1 {
|
||||
log.Fatal("Flag value may not be less than -1.")
|
||||
}
|
||||
y := uint64(*x)
|
||||
return &tpb.Consistency{Last: &y}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC)
|
||||
flag.Parse()
|
||||
|
||||
configs, err := readConfig(*configFile)
|
||||
checkErr("parsing config", err)
|
||||
|
||||
ktQueryConn, err := grpc.NewClient(*ktQueryServerAddr, getClientOptions(configs.KtQueryServiceConfig)...)
|
||||
checkErr("instantiating kt query client", err)
|
||||
|
||||
defer ktQueryConn.Close()
|
||||
ktQueryClient := pb.NewKeyTransparencyQueryServiceClient(ktQueryConn)
|
||||
|
||||
testConn, err := grpc.NewClient(*testServerAddr, getClientOptions(configs.KtTestServiceConfig)...)
|
||||
checkErr("instantiating kt test client", err)
|
||||
|
||||
defer testConn.Close()
|
||||
testClient := pb.NewKeyTransparencyTestServiceClient(testConn)
|
||||
|
||||
ktConn, err := grpc.NewClient(*ktServerAddr, getClientOptions(configs.KtServiceConfig)...)
|
||||
checkErr("instantiating kt client", err)
|
||||
|
||||
defer ktConn.Close()
|
||||
|
||||
ktClient := pb.NewKeyTransparencyServiceClient(ktConn)
|
||||
|
||||
switch flag.Arg(0) {
|
||||
case "distinguished":
|
||||
handleDistinguished(ktQueryClient)
|
||||
case "search":
|
||||
handleSearch(ktQueryClient)
|
||||
case "search-timing":
|
||||
handleSearchTiming(ktQueryClient)
|
||||
case "update":
|
||||
handleUpdate(testClient)
|
||||
case "monitor":
|
||||
handleMonitor(ktQueryClient)
|
||||
case "monitor-timing":
|
||||
handleMonitorTiming(ktQueryClient)
|
||||
case "audit":
|
||||
handleAudit(ktClient)
|
||||
default:
|
||||
log.Fatal("Unexpected operation requested. Allowed arguments: search, update, audit, config")
|
||||
}
|
||||
}
|
||||
|
||||
func readConfig(configFile string) (*config.Config, error) {
|
||||
if configFile == "" {
|
||||
return &config.Config{}, nil
|
||||
}
|
||||
|
||||
cfg, err := config.Read(configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getClientOptions(config *config.ServiceConfig) []grpc.DialOption {
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
}
|
||||
if config == nil || len(config.AuthorizedHeaders) == 0 {
|
||||
return opts
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
headers := []string{}
|
||||
for header, values := range config.AuthorizedHeaders {
|
||||
for _, value := range values {
|
||||
headers = append(append(headers, header), value)
|
||||
}
|
||||
}
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, headers...)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}))
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func round(d time.Duration) time.Duration {
|
||||
if d > time.Second {
|
||||
return d.Round(time.Second)
|
||||
}
|
||||
return d.Round(time.Millisecond)
|
||||
}
|
||||
|
||||
func printFullTreeHead(fth *tpb.FullTreeHead) {
|
||||
p.Printf("Full Tree Head:\n")
|
||||
p.Printf(" Tree Head:\n")
|
||||
p.Printf(" Tree Size: %v\n", fth.TreeHead.TreeSize)
|
||||
ts := time.UnixMilli(fth.TreeHead.Timestamp)
|
||||
p.Printf(" Timestamp: %v (%v ago)\n", ts, round(time.Now().Sub(ts)))
|
||||
p.Printf(" Signature: %x\n", fth.TreeHead.Signatures)
|
||||
|
||||
if n := len(fth.Last); n > 0 {
|
||||
p.Printf(" Consistency Proof: Given (%v entries)\n", n)
|
||||
} else {
|
||||
p.Printf(" Consistency Proof: None\n")
|
||||
}
|
||||
p.Println()
|
||||
}
|
||||
|
||||
func printSearchProof(proof *tpb.SearchProof) {
|
||||
p.Printf("Search Proof:\n")
|
||||
p.Printf(" Pos: %v\n", proof.Pos)
|
||||
p.Printf(" Inclusion Proof: %v entries\n", len(proof.Inclusion))
|
||||
for _, step := range proof.Steps {
|
||||
p.Printf(" - counter=%v commitment=%x\n", step.Prefix.Counter, step.Commitment)
|
||||
}
|
||||
p.Println()
|
||||
}
|
||||
94
cmd/kt-client/monitor.go
Normal file
94
cmd/kt-client/monitor.go
Normal file
@ -0,0 +1,94 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/crypto/vrf"
|
||||
)
|
||||
|
||||
func constructMonitorRequest(args QueryArgs, vrfVerifier vrf.PublicKey, searchResponse *pb.SearchResponse) *pb.MonitorRequest {
|
||||
monitorRequest := &pb.MonitorRequest{
|
||||
Consistency: consistency(last),
|
||||
}
|
||||
|
||||
aciCommitmentIndex, err := vrfVerifier.ECVRFVerify(append([]byte{'a'}, args.Aci...), searchResponse.Aci.VrfProof)
|
||||
checkErr("create aci commitment index", err)
|
||||
|
||||
monitorRequest.Aci = &pb.AciMonitorRequest{
|
||||
Aci: args.Aci,
|
||||
EntryPosition: searchResponse.GetAci().GetSearch().GetPos(),
|
||||
CommitmentIndex: aciCommitmentIndex[:],
|
||||
}
|
||||
|
||||
if searchResponse.E164 != nil {
|
||||
e164CommitmentIndex, err := vrfVerifier.ECVRFVerify(append([]byte{'n'}, []byte(args.E164)...), searchResponse.E164.VrfProof)
|
||||
checkErr("create e164 commitment index", err)
|
||||
monitorRequest.E164 = &pb.E164MonitorRequest{
|
||||
E164: &args.E164,
|
||||
EntryPosition: searchResponse.GetE164().GetSearch().GetPos(),
|
||||
CommitmentIndex: e164CommitmentIndex[:],
|
||||
}
|
||||
}
|
||||
|
||||
if searchResponse.UsernameHash != nil {
|
||||
usernameHashCommitmentIndex, err := vrfVerifier.ECVRFVerify(append([]byte{'u'}, args.UsernameHash...), searchResponse.UsernameHash.VrfProof)
|
||||
checkErr("create username hash commitment index", err)
|
||||
monitorRequest.UsernameHash = &pb.UsernameHashMonitorRequest{
|
||||
UsernameHash: args.UsernameHash,
|
||||
EntryPosition: searchResponse.GetUsernameHash().GetSearch().GetPos(),
|
||||
CommitmentIndex: usernameHashCommitmentIndex[:],
|
||||
}
|
||||
}
|
||||
return monitorRequest
|
||||
}
|
||||
|
||||
// handleMonitor makes 2 requests in succession:
|
||||
// 1. Search request for the specified identifiers to get the commitment index necessary for the monitor request
|
||||
// 2. Monitor request
|
||||
func handleMonitor(client pb.KeyTransparencyQueryServiceClient) {
|
||||
args := extractQueryArgs("monitor")
|
||||
|
||||
// First search the identifiers to get back the data necessary to make a monitor request
|
||||
searchResponse, err := client.Search(context.Background(), constructSearchRequest(args))
|
||||
checkErr("search identifiers before making a monitor request", err)
|
||||
fmt.Println("Search request: OK")
|
||||
|
||||
vrfVerifier := newStore().PublicConfig().VrfKey
|
||||
monitorResponse, err := client.Monitor(context.Background(), constructMonitorRequest(args, vrfVerifier, searchResponse))
|
||||
checkErr("monitor request", err)
|
||||
|
||||
printFullTreeHead(monitorResponse.TreeHead)
|
||||
p.Println("ACI monitor response:")
|
||||
p.Println("Steps: ")
|
||||
for _, step := range monitorResponse.Aci.Steps {
|
||||
p.Printf(" - counter=%v commitment=%x\n", step.Prefix.Counter, step.Commitment)
|
||||
}
|
||||
|
||||
if monitorResponse.E164 != nil {
|
||||
p.Println("\nE164 monitor response:")
|
||||
p.Println("Steps: ")
|
||||
for _, step := range monitorResponse.E164.Steps {
|
||||
p.Printf(" - counter=%v commitment=%x\n", step.Prefix.Counter, step.Commitment)
|
||||
}
|
||||
}
|
||||
|
||||
if monitorResponse.UsernameHash != nil {
|
||||
p.Println("\nUsername hash monitor response:")
|
||||
p.Println("Steps: ")
|
||||
for _, step := range monitorResponse.UsernameHash.Steps {
|
||||
p.Printf(" - counter=%v commitment=%x\n", step.Prefix.Counter, step.Commitment)
|
||||
}
|
||||
}
|
||||
|
||||
// Verifying the monitor response would require persistent state, which kt-client doesn't have,
|
||||
// so we skip it.
|
||||
p.Println("\nVerification skipped for the monitor response.")
|
||||
}
|
||||
33
cmd/kt-client/monitor_timing.go
Normal file
33
cmd/kt-client/monitor_timing.go
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
)
|
||||
|
||||
func handleMonitorTiming(client pb.KeyTransparencyQueryServiceClient) {
|
||||
args := extractQueryArgs("[-sample-size int] [-num-samples int] monitor-timing")
|
||||
samplingArgs := extractSamplingArgs()
|
||||
|
||||
// First search the identifiers to get back the data necessary to make a monitor request
|
||||
searchResponse, err := client.Search(context.Background(), constructSearchRequest(args))
|
||||
|
||||
checkErr("Search identifiers before making a monitor request", err)
|
||||
fmt.Println("Search request: OK")
|
||||
fmt.Printf("Measuring latency for monitoring ACI %x and E164 %s (%d rounds, %d requests per round)\n", args.Aci, *e164, samplingArgs.NumSamples, samplingArgs.SampleSize)
|
||||
|
||||
vrfVerifier := newStore().PublicConfig().VrfKey
|
||||
|
||||
req := constructMonitorRequest(args, vrfVerifier, searchResponse)
|
||||
timeRequest(func() error {
|
||||
_, err := client.Monitor(context.Background(), req)
|
||||
return err
|
||||
}, samplingArgs)
|
||||
}
|
||||
120
cmd/kt-client/search.go
Normal file
120
cmd/kt-client/search.go
Normal file
@ -0,0 +1,120 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
func constructSearchRequest(args QueryArgs) *pb.SearchRequest {
|
||||
req := &pb.SearchRequest{
|
||||
Aci: args.Aci,
|
||||
AciIdentityKey: args.AciIdentityKey,
|
||||
Consistency: consistency(last),
|
||||
}
|
||||
if args.E164 != "" {
|
||||
req.E164SearchRequest = &pb.E164SearchRequest{
|
||||
E164: &args.E164,
|
||||
UnidentifiedAccessKey: args.UnidentifiedAccessKey,
|
||||
}
|
||||
}
|
||||
|
||||
if args.UsernameHash != nil {
|
||||
req.UsernameHash = args.UsernameHash
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func handleSearch(client pb.KeyTransparencyQueryServiceClient) {
|
||||
args := extractQueryArgs("search")
|
||||
res, err := client.Search(context.Background(), constructSearchRequest(args))
|
||||
checkErr("search request", err)
|
||||
|
||||
printFullTreeHead(res.TreeHead)
|
||||
p.Printf("ACI search response: \n")
|
||||
p.Printf("VRF: %x\n\n", res.Aci.VrfProof)
|
||||
printSearchProof(res.Aci.Search)
|
||||
p.Printf("Opening: %x\n", res.Aci.Opening)
|
||||
p.Printf("Value: %x\n\n", res.Aci.Value.Value)
|
||||
|
||||
if res.E164 != nil {
|
||||
p.Printf("E164 search response: \n")
|
||||
p.Printf("VRF: %x\n\n", res.E164.VrfProof)
|
||||
printSearchProof(res.E164.Search)
|
||||
p.Printf("Opening: %x\n", res.E164.Opening)
|
||||
p.Printf("Value: %x\n\n", res.E164.Value.Value)
|
||||
}
|
||||
|
||||
if res.UsernameHash != nil {
|
||||
p.Printf("Username hash search response: \n")
|
||||
p.Printf("VRF: %x\n\n", res.UsernameHash.VrfProof)
|
||||
printSearchProof(res.UsernameHash.Search)
|
||||
p.Printf("Opening: %x\n", res.UsernameHash.Opening)
|
||||
p.Printf("Value: %x\n\n", res.UsernameHash.Value.Value)
|
||||
}
|
||||
|
||||
if *configFile == "" {
|
||||
p.Printf("Verification skipped\n")
|
||||
return
|
||||
}
|
||||
|
||||
if *last != -1 {
|
||||
// Verifying the consistency proof would require persistent state, which kt-client doesn't have,
|
||||
// so we nullify these fields.
|
||||
res.TreeHead.Last = nil
|
||||
res.TreeHead.Distinguished = nil
|
||||
}
|
||||
|
||||
allVerificationsSuccessful := true
|
||||
if err := transparency.VerifySearch(newStore(), createIdentifierSearchRequest(util.AciPrefix, args.Aci), createTreeSearchResponse(res.Aci, res.TreeHead)); err != nil {
|
||||
p.Printf("ACI verification failed: %v\n", err)
|
||||
allVerificationsSuccessful = false
|
||||
}
|
||||
|
||||
if res.E164 != nil {
|
||||
if err := transparency.VerifySearch(newStore(), createIdentifierSearchRequest(util.NumberPrefix, []byte(*e164)), createTreeSearchResponse(res.E164, res.TreeHead)); err != nil {
|
||||
p.Printf("E164 verification failed: %v\n", err)
|
||||
allVerificationsSuccessful = false
|
||||
}
|
||||
}
|
||||
|
||||
if res.UsernameHash != nil {
|
||||
if err := transparency.VerifySearch(newStore(), createIdentifierSearchRequest(util.UsernameHashPrefix, args.UsernameHash), createTreeSearchResponse(res.UsernameHash, res.TreeHead)); err != nil {
|
||||
p.Printf("Username hash verification failed: %v\n", err)
|
||||
allVerificationsSuccessful = false
|
||||
}
|
||||
}
|
||||
|
||||
if allVerificationsSuccessful {
|
||||
p.Printf("All verifications successful\n")
|
||||
}
|
||||
}
|
||||
|
||||
func createIdentifierSearchRequest(prefix byte, identifier []byte) *tpb.TreeSearchRequest {
|
||||
return createTreeSearchRequest(append([]byte{prefix}, identifier...))
|
||||
}
|
||||
|
||||
func createTreeSearchRequest(key []byte) *tpb.TreeSearchRequest {
|
||||
return &tpb.TreeSearchRequest{
|
||||
SearchKey: key,
|
||||
Consistency: consistency(last),
|
||||
}
|
||||
}
|
||||
|
||||
func createTreeSearchResponse(response *pb.CondensedTreeSearchResponse, treeHead *tpb.FullTreeHead) *tpb.TreeSearchResponse {
|
||||
return &tpb.TreeSearchResponse{
|
||||
TreeHead: treeHead,
|
||||
VrfProof: response.VrfProof,
|
||||
Search: response.Search,
|
||||
Opening: response.Opening,
|
||||
Value: response.Value,
|
||||
}
|
||||
}
|
||||
26
cmd/kt-client/search_timing.go
Normal file
26
cmd/kt-client/search_timing.go
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
)
|
||||
|
||||
func handleSearchTiming(client pb.KeyTransparencyQueryServiceClient) {
|
||||
args := extractQueryArgs("[-sample-size int] [-num-samples int] search-timing")
|
||||
samplingArgs := extractSamplingArgs()
|
||||
|
||||
fmt.Printf("Measuring latency for searching ACI %x and E164 %s (%d rounds, %d requests per round)\n", args.Aci, *e164, samplingArgs.NumSamples, samplingArgs.SampleSize)
|
||||
req := constructSearchRequest(args)
|
||||
|
||||
timeRequest(func() error {
|
||||
_, err := client.Search(context.Background(), req)
|
||||
return err
|
||||
}, samplingArgs)
|
||||
}
|
||||
45
cmd/kt-client/store.go
Normal file
45
cmd/kt-client/store.go
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
)
|
||||
|
||||
// clientStorage is a no-op implementation of the ClientStorage interface.
|
||||
type clientStorage struct {
|
||||
config *transparency.PublicConfig
|
||||
head *db.TransparencyTreeHead
|
||||
root []byte
|
||||
}
|
||||
|
||||
func newStore() *clientStorage {
|
||||
config, err := config.Read(*configFile)
|
||||
checkErr("loading config", err)
|
||||
|
||||
return &clientStorage{config: config.APIConfig.TreeConfig().Public()}
|
||||
}
|
||||
|
||||
func (cs *clientStorage) PublicConfig() *transparency.PublicConfig { return cs.config }
|
||||
|
||||
func (cs *clientStorage) GetLastTreeHead() (*db.TransparencyTreeHead, []byte, error) {
|
||||
return cs.head, cs.root, nil
|
||||
}
|
||||
|
||||
func (cs *clientStorage) SetLastTreeHead(head *db.TransparencyTreeHead, root []byte) error {
|
||||
cs.head, cs.root = head, root
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *clientStorage) GetData(key []byte) (*transparency.MonitoringData, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (cs *clientStorage) SetData(key []byte, data *transparency.MonitoringData) error {
|
||||
return nil
|
||||
}
|
||||
108
cmd/kt-client/update.go
Normal file
108
cmd/kt-client/update.go
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
func handleUpdate(client pb.KeyTransparencyTestServiceClient) {
|
||||
var updateKey []byte
|
||||
var updateValue []byte
|
||||
switch flag.Arg(1) {
|
||||
case "aci":
|
||||
if flag.Arg(2) == "" {
|
||||
log.Fatal("No update key given. Usage: kt-client update aci <UUID> <base64_encoded_aci_identity_key>")
|
||||
} else if flag.Arg(3) == "" {
|
||||
log.Fatal("No update value given. Usage: kt-client update aci <UUID> <base64_encoded_aci_identity_key>")
|
||||
}
|
||||
aci, err := uuid.Parse(flag.Arg(2))
|
||||
checkErr("invalid UUID string for ACI", err)
|
||||
|
||||
aciBytes, err := aci.MarshalBinary()
|
||||
checkErr("getting UUID bytes", err)
|
||||
|
||||
updateKey = append([]byte{util.AciPrefix}, aciBytes...)
|
||||
|
||||
aciIdentityKeyBytes, err := base64.StdEncoding.DecodeString(flag.Arg(3))
|
||||
checkErr("decoding base64 encoding for ACI identity key", err)
|
||||
|
||||
updateValue = append([]byte{0}, aciIdentityKeyBytes...)
|
||||
case "e164":
|
||||
if flag.Arg(2) == "" {
|
||||
log.Fatal("No update key given. Usage: kt-client update e164 <e164_string> <UUID>")
|
||||
} else if flag.Arg(3) == "" {
|
||||
log.Fatal("No update value given. Usage: kt-client update e164 <e164_string> <UUID>")
|
||||
}
|
||||
updateKey = append([]byte{util.NumberPrefix}, []byte(flag.Arg(2))...)
|
||||
|
||||
aci, err := uuid.Parse(flag.Arg(3))
|
||||
checkErr("invalid UUID string for ACI", err)
|
||||
|
||||
aciBytes, err := aci.MarshalBinary()
|
||||
checkErr("getting UUID bytes", err)
|
||||
|
||||
updateValue = append([]byte{0}, aciBytes...)
|
||||
case "username_hash":
|
||||
if flag.Arg(2) == "" {
|
||||
log.Fatal("No update key given. Usage: kt-client update username_hash <base64url_encoded_username_hash> <UUID>")
|
||||
} else if flag.Arg(3) == "" {
|
||||
log.Fatal("No update value given. Usage: kt-client update username_hash <base64url_encoded_username_hash> <UUID>")
|
||||
}
|
||||
usernameHashBytes, err := base64.URLEncoding.DecodeString(flag.Arg(2))
|
||||
checkErr("decoding base64url encoding for username hash", err)
|
||||
|
||||
updateKey = append([]byte{util.UsernameHashPrefix}, usernameHashBytes...)
|
||||
|
||||
aci, err := uuid.Parse(flag.Arg(3))
|
||||
checkErr("invalid UUID string for ACI", err)
|
||||
|
||||
aciBytes, err := aci.MarshalBinary()
|
||||
checkErr("getting UUID bytes", err)
|
||||
|
||||
updateValue = append([]byte{0}, aciBytes...)
|
||||
}
|
||||
|
||||
req := &tpb.UpdateRequest{
|
||||
SearchKey: updateKey,
|
||||
Value: updateValue,
|
||||
Consistency: consistency(last),
|
||||
ReturnUpdateResponse: true,
|
||||
}
|
||||
res, err := client.Update(context.Background(), req)
|
||||
checkErr("update request", err)
|
||||
|
||||
printFullTreeHead(res.TreeHead)
|
||||
p.Printf("VRF: %x\n\n", res.VrfProof)
|
||||
printSearchProof(res.Search)
|
||||
p.Printf("Opening: %x\n\n", res.Opening)
|
||||
|
||||
if *configFile == "" {
|
||||
p.Printf("Verification skipped\n")
|
||||
} else {
|
||||
// Verifying the consistency proof would require persistent state, which kt-client doesn't have,
|
||||
// so we nullify these fields.
|
||||
if *last != -1 {
|
||||
req.Consistency = nil
|
||||
res.TreeHead.Last = nil
|
||||
}
|
||||
if err := transparency.VerifyUpdate(newStore(), req, res); err != nil {
|
||||
p.Printf("Verification failed: %v\n", err)
|
||||
} else {
|
||||
p.Printf("Verification successful\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
145
cmd/kt-client/util.go
Normal file
145
cmd/kt-client/util.go
Normal file
@ -0,0 +1,145 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type QueryArgs struct {
|
||||
Aci []byte
|
||||
AciIdentityKey []byte
|
||||
E164 string
|
||||
UnidentifiedAccessKey []byte
|
||||
UsernameHash []byte
|
||||
}
|
||||
|
||||
func extractQueryArgs(command string) QueryArgs {
|
||||
if flag.Arg(1) == "" {
|
||||
log.Fatalf("No ACI given. Usage: kt-client [-e164 e164] [-uak base64_encoded_uak] [-username-hash base64url_encoded_username_hash] %s <UUID> <base64_encoded_aci_identity_key>", command)
|
||||
} else if flag.Arg(2) == "" {
|
||||
log.Fatalf("No ACI identity key given. Usage: kt-client [-e164 e164] [-uak base64_encoded_uak] [-username-hash base64url_encoded_username_hash] %s <UUID> <base64_encoded_aci_identity_key>", command)
|
||||
}
|
||||
|
||||
aci, err := uuid.Parse(flag.Arg(1))
|
||||
checkErr("invalid UUID string for ACI", err)
|
||||
|
||||
aciBytes, err := aci.MarshalBinary()
|
||||
checkErr("getting UUID bytes", err)
|
||||
|
||||
aciIdentityKeyBytes, err := base64.StdEncoding.DecodeString(flag.Arg(2))
|
||||
checkErr("decoding base64 encoding for ACI identity key", err)
|
||||
|
||||
var unidentifiedAccessKeyBytes []byte
|
||||
if *uak == "" {
|
||||
unidentifiedAccessKeyBytes = db.UnidentifiedAccessKey
|
||||
} else {
|
||||
unidentifiedAccessKeyBytes, err = base64.StdEncoding.DecodeString(*uak)
|
||||
checkErr("decoding base64 encoding for unidentified access key", err)
|
||||
}
|
||||
|
||||
var usernameHashBytes []byte
|
||||
if *usernameHash != "" {
|
||||
usernameHashBytes, err = base64.URLEncoding.DecodeString(*usernameHash)
|
||||
checkErr("decoding base64url encoding for username hash", err)
|
||||
}
|
||||
|
||||
return QueryArgs{
|
||||
aciBytes,
|
||||
aciIdentityKeyBytes,
|
||||
*e164,
|
||||
unidentifiedAccessKeyBytes,
|
||||
usernameHashBytes,
|
||||
}
|
||||
}
|
||||
|
||||
type SamplingArgs struct {
|
||||
SampleSize int
|
||||
NumSamples int
|
||||
}
|
||||
|
||||
func extractSamplingArgs() SamplingArgs {
|
||||
var sampleSize int
|
||||
if *timingSampleSize == 0 {
|
||||
log.Fatal("sample size cannot be 0")
|
||||
}
|
||||
|
||||
var numSamples int
|
||||
if *timingNumSamples == 0 {
|
||||
log.Fatal("number of samples cannot be 0")
|
||||
|
||||
}
|
||||
return SamplingArgs{
|
||||
SampleSize: sampleSize,
|
||||
NumSamples: numSamples,
|
||||
}
|
||||
}
|
||||
|
||||
func timeRequest(grpcCall func() error, samplingArgs SamplingArgs) {
|
||||
for j := 0; j < samplingArgs.NumSamples; j++ {
|
||||
fmt.Printf("\n\nRound %d of %d:\n", j, samplingArgs.NumSamples)
|
||||
var totalDuration time.Duration
|
||||
var minDuration time.Duration
|
||||
var maxDuration time.Duration
|
||||
|
||||
minDuration = time.Hour
|
||||
for i := 0; i < samplingArgs.SampleSize; i++ {
|
||||
start := time.Now()
|
||||
err := grpcCall()
|
||||
duration := time.Since(start)
|
||||
|
||||
gprcError, ok := status.FromError(err)
|
||||
if !ok {
|
||||
fmt.Printf("Could not parse err: %v", err)
|
||||
}
|
||||
|
||||
if gprcError.Code() == codes.NotFound {
|
||||
if i%10 == 0 {
|
||||
fmt.Printf("Request %d returned not found: %.5f seconds\n", i, duration.Seconds())
|
||||
}
|
||||
} else if gprcError.Code() == codes.OK {
|
||||
if i%10 == 0 {
|
||||
fmt.Printf("Request %d succeeded: %.5f seconds\n", i, duration.Seconds())
|
||||
}
|
||||
} else {
|
||||
log.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
totalDuration += duration
|
||||
|
||||
if duration < minDuration {
|
||||
minDuration = duration
|
||||
}
|
||||
|
||||
if duration > maxDuration {
|
||||
maxDuration = duration
|
||||
}
|
||||
}
|
||||
|
||||
avgDuration := totalDuration / time.Duration(samplingArgs.SampleSize)
|
||||
fmt.Println("\nResults:")
|
||||
fmt.Printf(" Min latency: %.5f seconds\n", minDuration.Seconds())
|
||||
fmt.Printf(" Max latency: %.5f seconds\n", maxDuration.Seconds())
|
||||
fmt.Printf(" Avg latency: %.5f seconds\n", avgDuration.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
func checkErr(context string, err error) {
|
||||
if err != nil {
|
||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("%s: %v\n", context, err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
92
cmd/kt-server/distinguished.go
Normal file
92
cmd/kt-server/distinguished.go
Normal file
@ -0,0 +1,92 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-metrics"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
const (
|
||||
distinguishedSearchKey = "distinguished"
|
||||
)
|
||||
|
||||
// distinguishedLookup looks up the value of the distinguished key, parses it as
|
||||
// a Unix timestamp, and returns the timestamp.
|
||||
func distinguishedLookup(updateHandler *KtUpdateHandler) time.Time {
|
||||
tree, err := updateHandler.config.NewTree(updateHandler.tx)
|
||||
if err != nil {
|
||||
util.Log().Fatalf("failed to initialize tree for distinguished lookup: %v", err)
|
||||
}
|
||||
for {
|
||||
res, err := tree.Search(&tpb.TreeSearchRequest{
|
||||
SearchKey: []byte(distinguishedSearchKey),
|
||||
Consistency: &tpb.Consistency{},
|
||||
})
|
||||
metrics.IncrCounterWithLabels([]string{"distinguished_lookup"}, 1, []metrics.Label{successLabel(err)})
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if gprcError, ok := status.FromError(err); ok && gprcError.Code() == codes.NotFound {
|
||||
return time.Time{}
|
||||
} else if strings.HasSuffix(errStr, "tree is empty") {
|
||||
return time.Time{}
|
||||
}
|
||||
util.Log().Warnf("Failed to lookup distinguished key: %v", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
timestamp, err := strconv.ParseInt(string(res.Value.Value), 10, 64)
|
||||
if err != nil {
|
||||
util.Log().Fatalf("Failed to parse distinguished key value: %v", err)
|
||||
}
|
||||
return time.Unix(timestamp, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// distinguishedUpdate updates the value of the distinguished key to be the
|
||||
// current time.
|
||||
func distinguishedUpdate(updateHandler *KtUpdateHandler) {
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err := updateHandler.update(context.Background(), &tpb.UpdateRequest{
|
||||
SearchKey: []byte(distinguishedSearchKey),
|
||||
Value: []byte(fmt.Sprint(time.Now().Unix())),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}, 5*time.Second)
|
||||
metrics.IncrCounterWithLabels([]string{"distinguished_update"}, 1, []metrics.Label{successLabel(err)})
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
util.Log().Warnf("Failed to update distinguished key: %v", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
util.Log().Warnf("Failed to update distinguished key")
|
||||
}
|
||||
|
||||
// distinguished maintains a distinguished key in the log.
|
||||
func distinguished(updateHandler *KtUpdateHandler, interval time.Duration) {
|
||||
var durUntilNext time.Duration
|
||||
|
||||
durSinceLast := time.Now().Sub(distinguishedLookup(updateHandler))
|
||||
if durSinceLast < interval {
|
||||
durUntilNext = interval - durSinceLast
|
||||
}
|
||||
|
||||
for {
|
||||
time.Sleep(durUntilNext)
|
||||
distinguishedUpdate(updateHandler)
|
||||
durUntilNext = interval
|
||||
}
|
||||
}
|
||||
122
cmd/kt-server/kt_handler.go
Normal file
122
cmd/kt-server/kt_handler.go
Normal file
@ -0,0 +1,122 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-metrics"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
type KtHandler struct {
|
||||
config *config.APIConfig
|
||||
tx db.TransparencyStore
|
||||
ch chan<- updateRequest
|
||||
auditorTreeHeadsCh chan<- updateAuditorTreeHeadRequest // Channel used to set auditor tree heads
|
||||
|
||||
pb.UnimplementedKeyTransparencyServiceServer
|
||||
}
|
||||
|
||||
func (h *KtHandler) Audit(ctx context.Context, req *pb.AuditRequest) (*pb.AuditResponse, error) {
|
||||
auditor, err := extractAuditorName(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
res, err := h.audit(ctx, req)
|
||||
lbls := []metrics.Label{successLabel(err), auditorLabel(auditor)}
|
||||
metrics.IncrCounterWithLabels([]string{"audit_requests"}, 1, lbls)
|
||||
metrics.MeasureSinceWithLabels([]string{"audit_duration"}, start, lbls)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (h *KtHandler) audit(ctx context.Context, req *pb.AuditRequest) (*pb.AuditResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid request")
|
||||
}
|
||||
tree, err := h.config.NewTree(h.tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updates, more, err := tree.Audit(req.Start, req.Limit)
|
||||
if err != nil {
|
||||
if errors.Is(err, transparency.ErrInvalidArgument) {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &pb.AuditResponse{Updates: updates, More: more}, nil
|
||||
}
|
||||
|
||||
func (h *KtHandler) SetAuditorHead(ctx context.Context, head *tpb.AuditorTreeHead) (*emptypb.Empty, error) {
|
||||
start := time.Now()
|
||||
auditor, err := extractAuditorName(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := h.setAuditorHead(ctx, head, auditor)
|
||||
lbls := []metrics.Label{successLabel(err), auditorLabel(auditor)}
|
||||
metrics.IncrCounterWithLabels([]string{"auditor_head_requests"}, 1, lbls)
|
||||
metrics.MeasureSinceWithLabels([]string{"auditor_head_duration"}, start, lbls)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (h *KtHandler) setAuditorHead(ctx context.Context, head *tpb.AuditorTreeHead, auditorName string) (*emptypb.Empty, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
errorCh := make(chan error, 1)
|
||||
select {
|
||||
case h.auditorTreeHeadsCh <- updateAuditorTreeHeadRequest{auditorTreeHead: head, auditorName: auditorName, err: errorCh}:
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("submitting auditor head timed out: %w", ctx.Err())
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errorCh:
|
||||
if errors.Is(err, transparency.ErrInvalidArgument) {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
} else if _, isType := err.(*transparency.ErrAuditorSignatureVerificationFailed); isType {
|
||||
util.Log().Errorf("failed to verify auditor tree head signature: %s", err.Error())
|
||||
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
||||
} else if errors.Is(err, transparency.ErrFailedPrecondition) {
|
||||
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
||||
} else if err != nil {
|
||||
return nil, status.Error(codes.Unavailable, err.Error())
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("waiting for auditor head insertion result timed out: %w", ctx.Err())
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// Checks that the auditor name exists on the incoming context and if so, parses and returns it
|
||||
func extractAuditorName(ctx context.Context) (string, error) {
|
||||
auditorName, ok := ctx.Value(AuditorNameContextKey).(string)
|
||||
if !ok {
|
||||
return "", status.Error(codes.InvalidArgument, fmt.Sprintf("invalid type for auditor name. expected string, got %T", auditorName))
|
||||
}
|
||||
|
||||
if len(auditorName) == 0 {
|
||||
return "", status.Error(codes.InvalidArgument, "no auditor name in context")
|
||||
}
|
||||
|
||||
return auditorName, nil
|
||||
}
|
||||
52
cmd/kt-server/kt_handler_test.go
Normal file
52
cmd/kt-server/kt_handler_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var testAuditEndpointsParameters = []struct {
|
||||
key string
|
||||
value string
|
||||
expectError bool
|
||||
expectedName string
|
||||
}{
|
||||
// No metadata
|
||||
{"", "", true, ""},
|
||||
// Wrong metadata key
|
||||
{"wrong-metadata-key", "", true, ""},
|
||||
{"wrong-metadata-key", "some-value", true, ""},
|
||||
// Right metadata key, no auditor name
|
||||
{AuditorNameContextKey, "", true, ""},
|
||||
{AuditorNameContextKey, "some-value", false, "some-value"},
|
||||
}
|
||||
|
||||
func TestExtractAuditorName(t *testing.T) {
|
||||
for _, p := range testAuditEndpointsParameters {
|
||||
ctx := context.Background()
|
||||
if len(p.key) > 0 {
|
||||
ctx = context.WithValue(ctx, p.key, p.value)
|
||||
}
|
||||
|
||||
name, err := extractAuditorName(ctx)
|
||||
if p.expectError {
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.InvalidArgument || !ok {
|
||||
t.Fatalf("Expected %v, got %v", codes.InvalidArgument, err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, p.expectedName, name)
|
||||
}
|
||||
}
|
||||
399
cmd/kt-server/kt_query_handler.go
Normal file
399
cmd/kt-server/kt_query_handler.go
Normal file
@ -0,0 +1,399 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-metrics"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
type KtQueryHandler struct {
|
||||
config *config.APIConfig
|
||||
tx db.TransparencyStore
|
||||
accountDB db.AccountDB
|
||||
|
||||
pb.UnimplementedKeyTransparencyQueryServiceServer
|
||||
}
|
||||
|
||||
func (h *KtQueryHandler) Distinguished(ctx context.Context, req *pb.DistinguishedRequest) (*pb.DistinguishedResponse, error) {
|
||||
start := time.Now()
|
||||
res, err := h.distinguished(req)
|
||||
lbls := []metrics.Label{successLabel(err), grpcStatusLabel(err)}
|
||||
metrics.IncrCounterWithLabels([]string{"distinguished_requests"}, 1, lbls)
|
||||
metrics.MeasureSinceWithLabels([]string{"distinguished_duration"}, start, lbls)
|
||||
if err, _ := status.FromError(err); err.Code() == codes.Unknown {
|
||||
util.Log().Errorf("Unexpected search error for distinguished key in key transparency service: %v", err.Err())
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (h *KtQueryHandler) distinguished(req *pb.DistinguishedRequest) (*pb.DistinguishedResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid request")
|
||||
}
|
||||
|
||||
tree, err := h.config.NewTree(h.tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchReq := &tpb.TreeSearchRequest{
|
||||
SearchKey: []byte(distinguishedSearchKey),
|
||||
Consistency: &tpb.Consistency{
|
||||
Distinguished: req.Last,
|
||||
},
|
||||
}
|
||||
resp, err := tree.Search(searchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.DistinguishedResponse{
|
||||
TreeHead: resp.TreeHead,
|
||||
Distinguished: convertToCondensedSearchResponse(resp),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *KtQueryHandler) Search(ctx context.Context, req *pb.SearchRequest) (*pb.SearchResponse, error) {
|
||||
start := time.Now()
|
||||
tree, err := h.config.NewTree(h.tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := h.search(req, tree)
|
||||
lbls := []metrics.Label{successLabel(err), grpcStatusLabel(err)}
|
||||
metrics.IncrCounterWithLabels([]string{"search_requests"}, 1, lbls)
|
||||
metrics.MeasureSinceWithLabels([]string{"search_duration"}, start, lbls)
|
||||
|
||||
if err, _ := status.FromError(err); err.Code() == codes.Unknown {
|
||||
util.Log().Errorf("Unexpected search error in key transparency service: %v", err.Err())
|
||||
}
|
||||
|
||||
// Achieve some minimum delay with jitter on the request to avoid a timing side-channel.
|
||||
addRandomDelay(start, time.Now(), h.config.MinimumSearchDelay, h.config.JitterPercent, "search")
|
||||
metrics.MeasureSinceWithLabels([]string{"total_search_duration"}, start, lbls)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Only ACI searches result in a `NotFound` or `PermissionDenied` response.
|
||||
// Phone numbers and username hashes that are not found or fail verification will return an empty `TreeSearchResponse`.
|
||||
func (h *KtQueryHandler) search(req *pb.SearchRequest, tree *transparency.Tree) (*pb.SearchResponse, error) {
|
||||
err := validateRequestParameters(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fullTreeHead, aciResponse, err := aciSearch(req, tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usernameHashResponse, err := usernameHashSearch(req, tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
phoneNumberResponse, err := h.phoneNumberSearch(req, tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.SearchResponse{
|
||||
TreeHead: fullTreeHead,
|
||||
Aci: aciResponse,
|
||||
UsernameHash: usernameHashResponse,
|
||||
E164: phoneNumberResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateRequestParameters(req *pb.SearchRequest) error {
|
||||
if req == nil {
|
||||
return status.Error(codes.InvalidArgument, "invalid request")
|
||||
} else if len(req.Aci) != 16 {
|
||||
return status.Error(codes.InvalidArgument, "invalid ACI")
|
||||
} else if len(req.AciIdentityKey) == 0 {
|
||||
return status.Error(codes.InvalidArgument, "must provide ACI identity key")
|
||||
} else if req.E164SearchRequest != nil {
|
||||
if len(req.E164SearchRequest.UnidentifiedAccessKey) == 0 {
|
||||
return status.Error(codes.InvalidArgument, "must provide unidentified access key for a phone number search")
|
||||
} else if !isPossiblePhoneNumber(req.E164SearchRequest.GetE164()) {
|
||||
return status.Error(codes.InvalidArgument, "invalid phone number")
|
||||
}
|
||||
} else if len(req.UsernameHash) != 0 && len(req.UsernameHash) != 32 {
|
||||
return status.Error(codes.InvalidArgument, "invalid username hash")
|
||||
} else if req.Consistency == nil {
|
||||
return status.Error(codes.InvalidArgument, "consistency cannot be nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Looks up the ACI identifier and verifies that the mapped value matches.
|
||||
// Other queries will not be allowed to continue unless this one verifies successfully.
|
||||
func aciSearch(req *pb.SearchRequest, tree *transparency.Tree) (*tpb.FullTreeHead, *pb.CondensedTreeSearchResponse, error) {
|
||||
consistency := &tpb.Consistency{
|
||||
Last: req.Consistency.Last,
|
||||
Distinguished: req.Consistency.Distinguished,
|
||||
}
|
||||
|
||||
aciResponse, err := tree.Search(&tpb.TreeSearchRequest{
|
||||
SearchKey: append([]byte{util.AciPrefix}, req.Aci...),
|
||||
Consistency: consistency,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(aciResponse.Value.Value) < 2 || aciResponse.Value.Value[0] != 0 {
|
||||
return nil, nil, status.Error(codes.Internal, "unexpected response value")
|
||||
}
|
||||
|
||||
err = verifyMappedValueConstantTime(req.AciIdentityKey, aciResponse.Value.Value[1:])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
fullTreeHead := aciResponse.GetTreeHead()
|
||||
return fullTreeHead, convertToCondensedSearchResponse(aciResponse), nil
|
||||
}
|
||||
|
||||
func usernameHashSearch(req *pb.SearchRequest, tree *transparency.Tree) (*pb.CondensedTreeSearchResponse, error) {
|
||||
if len(req.UsernameHash) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
usernameHashResponse, responseErr := tree.Search(&tpb.TreeSearchRequest{
|
||||
SearchKey: append([]byte{util.UsernameHashPrefix}, req.UsernameHash...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
})
|
||||
|
||||
if responseErr != nil {
|
||||
// A non-nil err should be returned except in the case where it's "not found".
|
||||
// In that case, we don't respond to the search but still allow a phone number search to continue.
|
||||
if grpcError, _ := status.FromError(responseErr); grpcError.Code() == codes.NotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, responseErr
|
||||
} else if len(usernameHashResponse.Value.Value) < 2 || usernameHashResponse.Value.Value[0] != 0 {
|
||||
return nil, status.Error(codes.Internal, "unexpected response value")
|
||||
} else {
|
||||
err := verifyMappedValueConstantTime(req.Aci, usernameHashResponse.Value.Value[1:])
|
||||
if err != nil {
|
||||
// If the ACI doesn't match, don't respond to the search
|
||||
// but still allow a phone number search to continue.
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
return convertToCondensedSearchResponse(usernameHashResponse), nil
|
||||
}
|
||||
|
||||
func (h *KtQueryHandler) phoneNumberSearch(req *pb.SearchRequest, tree *transparency.Tree) (*pb.CondensedTreeSearchResponse, error) {
|
||||
if req.E164SearchRequest == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
accountData, err := h.accountDB.GetAccountByAci(req.Aci)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// A non-nil responseErr should be returned except in the case where it's "not found" for a phone number lookup.
|
||||
// This is to prevent short-circuiting and creating a timing difference between an account that doesn't exist
|
||||
// with the given phone number, and one that does but is undiscoverable.
|
||||
phoneNumberResponse, responseErr := tree.Search(&tpb.TreeSearchRequest{
|
||||
SearchKey: append([]byte{util.NumberPrefix}, []byte(req.E164SearchRequest.GetE164())...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
})
|
||||
|
||||
var valueForComparison []byte
|
||||
if responseErr != nil {
|
||||
if grpcError, _ := status.FromError(responseErr); grpcError.Code() == codes.NotFound {
|
||||
// Set this value to something that will always fail comparison
|
||||
valueForComparison = createDistinctValue(req.Aci)
|
||||
} else {
|
||||
return nil, responseErr
|
||||
}
|
||||
} else if len(phoneNumberResponse.Value.Value) < 2 || phoneNumberResponse.Value.Value[0] != 0 {
|
||||
return nil, status.Error(codes.Internal, "unexpected response value")
|
||||
} else {
|
||||
valueForComparison = phoneNumberResponse.Value.Value[1:]
|
||||
}
|
||||
|
||||
err = verifyPhoneNumberSearchConstantTime(req.Aci, valueForComparison, req.E164SearchRequest.UnidentifiedAccessKey, accountData)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return convertToCondensedSearchResponse(phoneNumberResponse), nil
|
||||
}
|
||||
|
||||
func (h *KtQueryHandler) Monitor(ctx context.Context, req *pb.MonitorRequest) (*pb.MonitorResponse, error) {
|
||||
start := time.Now()
|
||||
res, err := h.monitor(req)
|
||||
lbls := []metrics.Label{successLabel(err), grpcStatusLabel(err)}
|
||||
metrics.IncrCounterWithLabels([]string{"monitor_requests"}, 1, lbls)
|
||||
metrics.MeasureSinceWithLabels([]string{"monitor_duration"}, start, lbls)
|
||||
if err, _ := status.FromError(err); err.Code() == codes.Unknown {
|
||||
util.Log().Errorf("Unexpected monitor error in key transparency service: %v", err.Err())
|
||||
}
|
||||
// Achieve some minimum delay with jitter on the request to avoid a timing side-channel.
|
||||
addRandomDelay(start, time.Now(), h.config.MinimumMonitorDelay, h.config.JitterPercent, "monitor")
|
||||
metrics.MeasureSinceWithLabels([]string{"total_monitor_duration"}, start, lbls)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (h *KtQueryHandler) monitor(req *pb.MonitorRequest) (*pb.MonitorResponse, error) {
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid request")
|
||||
}
|
||||
|
||||
tree, err := h.config.NewTree(h.tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
monitorKeys := []*tpb.MonitorKey{
|
||||
{
|
||||
SearchKey: append([]byte{util.AciPrefix}, req.Aci.GetAci()...),
|
||||
EntryPosition: req.Aci.GetEntryPosition(),
|
||||
CommitmentIndex: req.Aci.GetCommitmentIndex(),
|
||||
},
|
||||
}
|
||||
|
||||
if req.GetUsernameHash() != nil {
|
||||
monitorKeys = append(monitorKeys, &tpb.MonitorKey{
|
||||
SearchKey: append([]byte{util.UsernameHashPrefix}, req.UsernameHash.GetUsernameHash()...),
|
||||
EntryPosition: req.UsernameHash.GetEntryPosition(),
|
||||
CommitmentIndex: req.UsernameHash.GetCommitmentIndex(),
|
||||
})
|
||||
}
|
||||
|
||||
if req.GetE164() != nil {
|
||||
monitorKeys = append(monitorKeys, &tpb.MonitorKey{
|
||||
SearchKey: append([]byte{util.NumberPrefix}, req.E164.GetE164()...),
|
||||
EntryPosition: req.E164.GetEntryPosition(),
|
||||
CommitmentIndex: req.E164.GetCommitmentIndex(),
|
||||
})
|
||||
}
|
||||
|
||||
internalMonitorRequest := &tpb.MonitorRequest{
|
||||
Keys: monitorKeys,
|
||||
Consistency: req.Consistency,
|
||||
}
|
||||
|
||||
internalMonitorResponse, err := tree.Monitor(internalMonitorRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var aciProof *tpb.MonitorProof
|
||||
aciProof, internalMonitorResponse.Proofs = internalMonitorResponse.Proofs[0], internalMonitorResponse.Proofs[1:]
|
||||
|
||||
var usernameHashProof *tpb.MonitorProof
|
||||
if req.GetUsernameHash() != nil {
|
||||
usernameHashProof, internalMonitorResponse.Proofs = internalMonitorResponse.Proofs[0], internalMonitorResponse.Proofs[1:]
|
||||
}
|
||||
|
||||
var e164Proof *tpb.MonitorProof
|
||||
if req.GetE164() != nil {
|
||||
e164Proof = internalMonitorResponse.Proofs[0]
|
||||
}
|
||||
|
||||
return &pb.MonitorResponse{
|
||||
TreeHead: internalMonitorResponse.TreeHead,
|
||||
Aci: aciProof,
|
||||
UsernameHash: usernameHashProof,
|
||||
E164: e164Proof,
|
||||
Inclusion: internalMonitorResponse.Inclusion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isPossiblePhoneNumber(number string) bool {
|
||||
if !strings.HasPrefix(number, "+") {
|
||||
return false
|
||||
} else if len(number[1:]) > 15 {
|
||||
// E.164 specifies a maximum of 15 digits
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Phone number searches must pass additional checks:
|
||||
// - the account must be discoverable
|
||||
// - the unidentified access key provided in the request must match the one on the account
|
||||
func verifyPhoneNumberSearchConstantTime(mappedValue, expectedValue, reqUnidentifiedAccessKey []byte, accountData *db.Account) error {
|
||||
var accountUnidentifiedAccessKeyForComparison []byte
|
||||
discoverable := 0
|
||||
|
||||
if accountData == nil {
|
||||
// If no account exists, set this value to something that will always fail comparison
|
||||
accountUnidentifiedAccessKeyForComparison = createDistinctValue(reqUnidentifiedAccessKey)
|
||||
} else {
|
||||
accountUnidentifiedAccessKeyForComparison = accountData.UnidentifiedAccessKey
|
||||
if accountData.DiscoverableByPhoneNumber {
|
||||
discoverable = 1
|
||||
}
|
||||
}
|
||||
|
||||
unidentifiedAccessKeysEqual := 0
|
||||
if subtle.ConstantTimeCompare(accountUnidentifiedAccessKeyForComparison, reqUnidentifiedAccessKey) == 1 {
|
||||
unidentifiedAccessKeysEqual = 1
|
||||
}
|
||||
|
||||
if (discoverable & unidentifiedAccessKeysEqual) == 0 {
|
||||
// We want to avoid leaking data about the existence of an account with a given phone number
|
||||
return status.Error(codes.NotFound, "user not found")
|
||||
}
|
||||
|
||||
return verifyMappedValueConstantTime(mappedValue, expectedValue)
|
||||
}
|
||||
|
||||
func convertToCondensedSearchResponse(response *tpb.TreeSearchResponse) *pb.CondensedTreeSearchResponse {
|
||||
return &pb.CondensedTreeSearchResponse{
|
||||
VrfProof: response.VrfProof,
|
||||
Search: response.Search,
|
||||
Opening: response.Opening,
|
||||
Value: response.Value,
|
||||
}
|
||||
}
|
||||
|
||||
// addJitter adds random jitter to the specified duration and returns the final duration.
|
||||
// For example, if the minDelay is 1000ms and the jitterPercent is 10, addJitter will
|
||||
// return a random duration in the interval [1000ms, 1100ms].
|
||||
func addJitter(minDelay time.Duration, jitterPercent int) time.Duration {
|
||||
upperBound := float64(minDelay.Nanoseconds()) * float64(jitterPercent) / 100.0
|
||||
|
||||
// Generate a random value between [0, upperBound]
|
||||
jitter := rand.Int63n(int64(upperBound) + 1)
|
||||
|
||||
return minDelay + time.Duration(jitter)
|
||||
}
|
||||
|
||||
// addRandomDelay injects a delay to achieve some minimum jittered delay for the request.
|
||||
func addRandomDelay(start, now time.Time, minDelay time.Duration, jitterPercent int, endpoint string) {
|
||||
elapsed := now.Sub(start)
|
||||
jitteredMinDelay := addJitter(minDelay, jitterPercent)
|
||||
timeToSleep := jitteredMinDelay - elapsed
|
||||
metrics.AddSampleWithLabels([]string{"random_delay_duration"}, float32(timeToSleep.Nanoseconds())/float32(time.Millisecond), []metrics.Label{endpointLabel(endpoint)})
|
||||
|
||||
if timeToSleep < 0 {
|
||||
metrics.IncrCounterWithLabels([]string{"elapsed_greater_than_min_delay"}, 1, []metrics.Label{endpointLabel(endpoint)})
|
||||
metrics.AddSampleWithLabels([]string{"negative_random_delay_duration"}, float32(timeToSleep.Nanoseconds())/float32(time.Millisecond), []metrics.Label{endpointLabel(endpoint)})
|
||||
}
|
||||
|
||||
time.Sleep(timeToSleep)
|
||||
}
|
||||
674
cmd/kt-server/kt_query_handler_test.go
Normal file
674
cmd/kt-server/kt_query_handler_test.go
Normal file
@ -0,0 +1,674 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
var (
|
||||
mockConfigFile = "test_config.yaml"
|
||||
validUsernameHash1 = random(32)
|
||||
validPhoneNumber1 = "+14155550101"
|
||||
unidentifiedAccessKey = random(16)
|
||||
mismatchedUnidentifiedAccessKey = createDistinctValue(unidentifiedAccessKey)
|
||||
|
||||
invalidAci = make([]byte, 15)
|
||||
invalidUsernameHash = random(31)
|
||||
invalidPhoneNumber1 = "14155550101"
|
||||
invalidPhoneNumber2 = "+1415555010101456"
|
||||
)
|
||||
|
||||
func TestDistinguished_NilRequest(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
_, err := h.distinguished(nil)
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.InvalidArgument || !ok {
|
||||
t.Fatalf("Expected %v, got %v",
|
||||
codes.InvalidArgument, err)
|
||||
}
|
||||
}
|
||||
|
||||
var testInvalidSearchRequestParameters = []struct {
|
||||
searchRequest *pb.SearchRequest
|
||||
}{
|
||||
// Nil search request
|
||||
{nil},
|
||||
// No aci
|
||||
{&pb.SearchRequest{AciIdentityKey: validAciIdentityKey1, Consistency: &tpb.Consistency{}}},
|
||||
// ACI wrong length
|
||||
{&pb.SearchRequest{Aci: invalidAci, AciIdentityKey: validAciIdentityKey1, Consistency: &tpb.Consistency{}}},
|
||||
// No ACI identity key
|
||||
{&pb.SearchRequest{Aci: validAci1, Consistency: &tpb.Consistency{}}},
|
||||
// Phone number search with no phone number or unidentified access key
|
||||
{&pb.SearchRequest{Aci: invalidAci, AciIdentityKey: validAciIdentityKey1,
|
||||
E164SearchRequest: &pb.E164SearchRequest{}, Consistency: &tpb.Consistency{}}},
|
||||
// Phone number search key with no unidentified access key
|
||||
{&pb.SearchRequest{Aci: invalidAci, AciIdentityKey: validAciIdentityKey1,
|
||||
E164SearchRequest: &pb.E164SearchRequest{E164: &validPhoneNumber1},
|
||||
Consistency: &tpb.Consistency{}}},
|
||||
// Phone number search key missing leading '+'
|
||||
{&pb.SearchRequest{Aci: invalidAci, AciIdentityKey: validAciIdentityKey1,
|
||||
E164SearchRequest: &pb.E164SearchRequest{E164: &invalidPhoneNumber1, UnidentifiedAccessKey: unidentifiedAccessKey},
|
||||
Consistency: &tpb.Consistency{}}},
|
||||
// Phone number search key with invalid length
|
||||
{&pb.SearchRequest{Aci: invalidAci, AciIdentityKey: validAciIdentityKey1,
|
||||
E164SearchRequest: &pb.E164SearchRequest{E164: &invalidPhoneNumber2, UnidentifiedAccessKey: unidentifiedAccessKey},
|
||||
Consistency: &tpb.Consistency{}}},
|
||||
// Username hash search key with invalid length
|
||||
{&pb.SearchRequest{Aci: validAci1, AciIdentityKey: validAciIdentityKey1,
|
||||
UsernameHash: invalidUsernameHash, Consistency: &tpb.Consistency{}}},
|
||||
// Consistency cannot be nil
|
||||
{&pb.SearchRequest{Aci: validAci1, AciIdentityKey: validAciIdentityKey1}},
|
||||
}
|
||||
|
||||
func TestSearch_InvalidArgument(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
tree, _ := mockConfig.APIConfig.NewTree(mockTransparencyStore)
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
for _, p := range testInvalidSearchRequestParameters {
|
||||
_, err := h.search(p.searchRequest, tree)
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.InvalidArgument || !ok {
|
||||
t.Fatalf("Expected %v, got %v",
|
||||
codes.InvalidArgument, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_AciNotFound(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
tree, _ := mockConfig.APIConfig.NewTree(mockTransparencyStore)
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
// Add ACI so that we're not searching an empty tree
|
||||
aciUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.AciPrefix}, validAci1...),
|
||||
Value: append([]byte{0}, validAciIdentityKey1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err := tree.UpdateSimple(aciUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree, %v", err)
|
||||
}
|
||||
|
||||
// Search for a different ACI
|
||||
resp, err := h.search(&pb.SearchRequest{
|
||||
Aci: random(16),
|
||||
AciIdentityKey: validAciIdentityKey1,
|
||||
Consistency: &tpb.Consistency{},
|
||||
}, tree)
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.NotFound || !ok {
|
||||
t.Fatalf("Expected %v, got %v",
|
||||
codes.NotFound, err)
|
||||
} else if resp != nil {
|
||||
t.Fatalf("Expected no search response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_AciPermissionDenied(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
tree, _ := mockConfig.APIConfig.NewTree(mockTransparencyStore)
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
// Add ACI
|
||||
aciUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.AciPrefix}, validAci1...),
|
||||
Value: append([]byte{0}, validAciIdentityKey1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err := tree.UpdateSimple(aciUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Search for the same ACI, but provide the wrong ACI identity key
|
||||
searchReq := &pb.SearchRequest{
|
||||
Aci: validAci1,
|
||||
AciIdentityKey: mismatchedUnidentifiedAccessKey,
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
|
||||
resp, err := h.search(searchReq, tree)
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.PermissionDenied || !ok {
|
||||
t.Fatalf("Expected %v, got %v",
|
||||
codes.PermissionDenied, err)
|
||||
} else if resp != nil {
|
||||
t.Fatalf("Expected no search response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_UsernameHashNotFound(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
tree, _ := mockConfig.APIConfig.NewTree(mockTransparencyStore)
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
// Add ACI so that we're not searching an empty tree
|
||||
aciUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.AciPrefix}, validAci1...),
|
||||
Value: append([]byte{0}, validAciIdentityKey1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err := tree.UpdateSimple(aciUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Search for ACI and non-existent username hash
|
||||
searchReq := &pb.SearchRequest{
|
||||
Aci: validAci1,
|
||||
AciIdentityKey: validAciIdentityKey1,
|
||||
UsernameHash: validUsernameHash1,
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
|
||||
resp, err := h.search(searchReq, tree)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
} else if resp == nil || resp.Aci == nil {
|
||||
t.Fatalf("Expected ACI search response")
|
||||
} else if resp.TreeHead == nil {
|
||||
t.Fatalf("Expected top-level tree head")
|
||||
} else if resp.UsernameHash != nil {
|
||||
t.Fatalf("Expected no username hash search response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_E164NotFound(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
tree, _ := mockConfig.APIConfig.NewTree(mockTransparencyStore)
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
// Add ACI so that we're not searching an empty tree
|
||||
aciUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.AciPrefix}, validAci1...),
|
||||
Value: append([]byte{0}, validAciIdentityKey1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err := tree.UpdateSimple(aciUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Search for ACI and non-existent E164
|
||||
resp, err := h.search(&pb.SearchRequest{
|
||||
Aci: validAci1,
|
||||
AciIdentityKey: validAciIdentityKey1,
|
||||
E164SearchRequest: &pb.E164SearchRequest{
|
||||
E164: &validPhoneNumber1,
|
||||
UnidentifiedAccessKey: unidentifiedAccessKey,
|
||||
},
|
||||
Consistency: &tpb.Consistency{},
|
||||
}, tree)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
} else if resp == nil || resp.Aci == nil {
|
||||
t.Fatalf("Expected an ACI search response")
|
||||
} else if resp.TreeHead == nil {
|
||||
t.Fatalf("Expected a top-level tree head")
|
||||
} else if resp.E164 != nil {
|
||||
t.Fatalf("Expected no E164 search response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_E164DoesNotMatch(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
tree, _ := mockConfig.APIConfig.NewTree(mockTransparencyStore)
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
// Add ACI
|
||||
aciUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.AciPrefix}, validAci1...),
|
||||
Value: append([]byte{0}, validAciIdentityKey1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err := tree.UpdateSimple(aciUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Add username hash
|
||||
usernameHashUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.UsernameHashPrefix}, validUsernameHash1...),
|
||||
Value: append([]byte{0}, validAci1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err = tree.UpdateSimple(usernameHashUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Add E164 that maps to a different ACI
|
||||
e164UpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.NumberPrefix}, []byte(validPhoneNumber1)...),
|
||||
Value: append([]byte{0}, mismatchedAci...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err = tree.UpdateSimple(e164UpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Search for ACI and E164. Provide the wrong ACI for the E164.
|
||||
searchReq := &pb.SearchRequest{
|
||||
Aci: validAci1,
|
||||
AciIdentityKey: validAciIdentityKey1,
|
||||
E164SearchRequest: &pb.E164SearchRequest{
|
||||
E164: &validPhoneNumber1,
|
||||
UnidentifiedAccessKey: unidentifiedAccessKey,
|
||||
},
|
||||
UsernameHash: validUsernameHash1,
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
|
||||
resp, err := h.search(searchReq, tree)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
} else if resp == nil || resp.Aci == nil || resp.UsernameHash == nil {
|
||||
t.Fatalf("Expected ACI and username hash search responses")
|
||||
} else if resp.TreeHead == nil {
|
||||
t.Fatalf("Expected ACI search response to have tree head")
|
||||
} else if resp.E164 != nil {
|
||||
t.Fatalf("Expected no E164 search response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_UsernameHashDoesNotMatch(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
tree, _ := mockConfig.APIConfig.NewTree(mockTransparencyStore)
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
// Add ACI
|
||||
aciUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.AciPrefix}, validAci1...),
|
||||
Value: append([]byte{0}, validAciIdentityKey1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err := tree.UpdateSimple(aciUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Add E164
|
||||
e164UpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.NumberPrefix}, []byte(validPhoneNumber1)...),
|
||||
Value: append([]byte{0}, validAci1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err = tree.UpdateSimple(e164UpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Add username hash that maps to a different ACI
|
||||
usernameHashUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: append([]byte{util.UsernameHashPrefix}, validUsernameHash1...),
|
||||
Value: append([]byte{0}, mismatchedAci...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err = tree.UpdateSimple(usernameHashUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree")
|
||||
}
|
||||
|
||||
// Search for all three identifiers
|
||||
searchReq := &pb.SearchRequest{
|
||||
Aci: validAci1,
|
||||
AciIdentityKey: validAciIdentityKey1,
|
||||
E164SearchRequest: &pb.E164SearchRequest{
|
||||
E164: &validPhoneNumber1,
|
||||
UnidentifiedAccessKey: db.UnidentifiedAccessKey,
|
||||
},
|
||||
UsernameHash: validUsernameHash1,
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
|
||||
resp, err := h.search(searchReq, tree)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
} else if resp == nil || resp.Aci == nil || resp.E164 == nil {
|
||||
t.Fatalf("Expected ACI and E164 search responses")
|
||||
} else if resp.TreeHead == nil {
|
||||
t.Fatalf("Expected top-level tree head")
|
||||
} else if resp.UsernameHash != nil {
|
||||
t.Fatalf("Expected no username hash search response")
|
||||
}
|
||||
}
|
||||
|
||||
var testInvalidMonitorParameters = []struct {
|
||||
monitorRequest *pb.MonitorRequest
|
||||
expectedError codes.Code
|
||||
}{
|
||||
// nil monitor request
|
||||
{nil, codes.InvalidArgument},
|
||||
// aci monitor request must not be nil
|
||||
{&pb.MonitorRequest{}, codes.InvalidArgument},
|
||||
// aci.Aci must not be nil
|
||||
{&pb.MonitorRequest{
|
||||
Aci: &pb.AciMonitorRequest{},
|
||||
}, codes.InvalidArgument},
|
||||
// aci.Entries must not be nil
|
||||
{&pb.MonitorRequest{
|
||||
Aci: &pb.AciMonitorRequest{Aci: validAci1},
|
||||
}, codes.InvalidArgument},
|
||||
// aci.CommitmentIndex must not be nil
|
||||
{&pb.MonitorRequest{
|
||||
Aci: &pb.AciMonitorRequest{Aci: validAci1, EntryPosition: 0},
|
||||
}, codes.InvalidArgument},
|
||||
// aci.CommitmentIndex must be of length 32
|
||||
{&pb.MonitorRequest{
|
||||
Aci: &pb.AciMonitorRequest{Aci: validAci1, EntryPosition: 0, CommitmentIndex: []byte{0}},
|
||||
}, codes.InvalidArgument},
|
||||
// aci.CommitmentIndex does not match
|
||||
{&pb.MonitorRequest{
|
||||
Aci: &pb.AciMonitorRequest{Aci: validAci1, EntryPosition: 0, CommitmentIndex: make([]byte, 32)}},
|
||||
codes.PermissionDenied},
|
||||
}
|
||||
|
||||
func TestMonitor_InvalidRequests(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
for _, p := range testInvalidMonitorParameters {
|
||||
_, err := h.monitor(p.monitorRequest)
|
||||
|
||||
if p.expectedError != codes.OK {
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != p.expectedError || !ok {
|
||||
t.Fatalf("Expected error of type %v, got %v", p.expectedError, grpcError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitor(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
accountDb := db.MockAccountDB{}
|
||||
h := KtQueryHandler{config: mockConfig.APIConfig, tx: mockTransparencyStore, accountDB: &accountDb}
|
||||
|
||||
tree, _ := mockConfig.APIConfig.NewTree(mockTransparencyStore)
|
||||
|
||||
// Setup part 1: add data so that we're not using an empty tree
|
||||
|
||||
aciSearchKey := append([]byte{util.AciPrefix}, validAci1...)
|
||||
aciUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: aciSearchKey,
|
||||
Value: append([]byte{0}, validAciIdentityKey1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err := tree.UpdateSimple(aciUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree, %v", err)
|
||||
}
|
||||
|
||||
usernameHashSearchKey := append([]byte{util.UsernameHashPrefix}, validUsernameHash1...)
|
||||
usernameHashUpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: usernameHashSearchKey,
|
||||
Value: append([]byte{0}, validAci1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err = tree.UpdateSimple(usernameHashUpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree, %v", err)
|
||||
}
|
||||
|
||||
e164SearchKey := append([]byte{util.NumberPrefix}, validPhoneNumber1...)
|
||||
e164UpdateReq := &tpb.UpdateRequest{
|
||||
SearchKey: e164SearchKey,
|
||||
Value: append([]byte{0}, validAci1...),
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
_, err = tree.UpdateSimple(e164UpdateReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error updating the tree, %v", err)
|
||||
}
|
||||
|
||||
// Setup part 2: Search, to get a valid commitment index
|
||||
|
||||
searchResponse, err := h.search(&pb.SearchRequest{
|
||||
Aci: validAci1,
|
||||
AciIdentityKey: validAciIdentityKey1,
|
||||
UsernameHash: validUsernameHash1,
|
||||
E164SearchRequest: &pb.E164SearchRequest{
|
||||
E164: &validPhoneNumber1,
|
||||
UnidentifiedAccessKey: db.UnidentifiedAccessKey,
|
||||
},
|
||||
Consistency: &tpb.Consistency{},
|
||||
}, tree)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
aciCommitmentIndex, err := mockConfig.APIConfig.TreeConfig().Public().VrfKey.ECVRFVerify(aciSearchKey, searchResponse.Aci.VrfProof)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
usernameHashCommitmentIndex, err := mockConfig.APIConfig.TreeConfig().Public().VrfKey.ECVRFVerify(usernameHashSearchKey, searchResponse.UsernameHash.VrfProof)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
e164CommitmentIndex, err := mockConfig.APIConfig.TreeConfig().Public().VrfKey.ECVRFVerify(e164SearchKey, searchResponse.E164.VrfProof)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
// test 1: just ACI
|
||||
|
||||
req := &pb.MonitorRequest{
|
||||
Aci: &pb.AciMonitorRequest{
|
||||
Aci: validAci1,
|
||||
EntryPosition: searchResponse.Aci.Search.Pos,
|
||||
CommitmentIndex: aciCommitmentIndex[:],
|
||||
},
|
||||
Consistency: &tpb.Consistency{},
|
||||
}
|
||||
|
||||
res, err := h.monitor(req)
|
||||
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.OK || !ok {
|
||||
t.Fatalf("Unexpected error %v", grpcError)
|
||||
}
|
||||
|
||||
if res.Aci == nil {
|
||||
t.Fatalf("ACI proof should not be nil")
|
||||
}
|
||||
if res.UsernameHash != nil {
|
||||
t.Fatalf("Username hash proof should be nil")
|
||||
}
|
||||
if res.E164 != nil {
|
||||
t.Fatalf("E164 proof should be nil")
|
||||
}
|
||||
if len(res.Inclusion) == 0 {
|
||||
t.Fatalf("Inclusion proof should not be empty")
|
||||
}
|
||||
|
||||
// test 2: ACI + Username Hash
|
||||
|
||||
req.UsernameHash = &pb.UsernameHashMonitorRequest{
|
||||
UsernameHash: validUsernameHash1,
|
||||
EntryPosition: searchResponse.UsernameHash.Search.Pos,
|
||||
CommitmentIndex: usernameHashCommitmentIndex[:],
|
||||
}
|
||||
|
||||
res, err = h.monitor(req)
|
||||
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.OK || !ok {
|
||||
t.Fatalf("Unexpected error %v", grpcError)
|
||||
}
|
||||
|
||||
if res.Aci == nil {
|
||||
t.Fatalf("ACI proof should not be nil")
|
||||
}
|
||||
if res.UsernameHash == nil {
|
||||
t.Fatalf("Username hash proof should not be nil")
|
||||
}
|
||||
if res.E164 != nil {
|
||||
t.Fatalf("E164 proof should be nil")
|
||||
}
|
||||
if len(res.Inclusion) == 0 {
|
||||
t.Fatalf("Inclusion proof should not be empty")
|
||||
}
|
||||
|
||||
// test 3: ACI + Username Hash + E164
|
||||
|
||||
req.E164 = &pb.E164MonitorRequest{
|
||||
E164: &validPhoneNumber1,
|
||||
EntryPosition: searchResponse.E164.Search.Pos,
|
||||
CommitmentIndex: e164CommitmentIndex[:],
|
||||
}
|
||||
|
||||
res, err = h.monitor(req)
|
||||
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.OK || !ok {
|
||||
t.Fatalf("Unexpected error %v", grpcError)
|
||||
}
|
||||
|
||||
if res.Aci == nil {
|
||||
t.Fatalf("ACI proof should not be nil")
|
||||
}
|
||||
if res.UsernameHash == nil {
|
||||
t.Fatalf("Username hash proof should not be nil")
|
||||
}
|
||||
if res.E164 == nil {
|
||||
t.Fatalf("E164 proof should not be nil")
|
||||
}
|
||||
if len(res.Inclusion) == 0 {
|
||||
t.Fatalf("Inclusion proof should not be empty")
|
||||
}
|
||||
|
||||
// test 4: ACI + E164
|
||||
|
||||
req.UsernameHash = nil
|
||||
|
||||
res, err = h.monitor(req)
|
||||
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.OK || !ok {
|
||||
t.Fatalf("Unexpected error %v", grpcError)
|
||||
}
|
||||
|
||||
if res.Aci == nil {
|
||||
t.Fatalf("ACI proof should not be nil")
|
||||
}
|
||||
if res.UsernameHash != nil {
|
||||
t.Fatalf("Username hash proof should be nil")
|
||||
}
|
||||
if res.E164 == nil {
|
||||
t.Fatalf("E164 proof should not be nil")
|
||||
}
|
||||
if len(res.Inclusion) == 0 {
|
||||
t.Fatalf("Inclusion proof should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
var testVerifyPhoneNumberSearchParameters = []struct {
|
||||
providedValue []byte
|
||||
expectedValue []byte
|
||||
unidentifiedAccessKey []byte
|
||||
account *db.Account
|
||||
expectedErrorType codes.Code
|
||||
}{
|
||||
// Discoverable; unidentified access key matches; provided value matches; no error
|
||||
{validAci1, validAci1, unidentifiedAccessKey, &db.Account{
|
||||
UnidentifiedAccessKey: unidentifiedAccessKey,
|
||||
DiscoverableByPhoneNumber: true,
|
||||
}, codes.OK},
|
||||
// Account does not exist; expect error
|
||||
{validAci1, validAci1, unidentifiedAccessKey, nil, codes.NotFound},
|
||||
// User not discoverable by phone number; expect error
|
||||
{validAci1, validAci1, unidentifiedAccessKey, &db.Account{
|
||||
UnidentifiedAccessKey: unidentifiedAccessKey,
|
||||
DiscoverableByPhoneNumber: false,
|
||||
}, codes.NotFound},
|
||||
// Unidentified access key does not match; expect error
|
||||
{validAci1, validAci1, unidentifiedAccessKey, &db.Account{
|
||||
UnidentifiedAccessKey: mismatchedUnidentifiedAccessKey,
|
||||
DiscoverableByPhoneNumber: true,
|
||||
}, codes.NotFound},
|
||||
// Provided and expected mapped values do not match; expect error
|
||||
{validAci1, mismatchedAci, unidentifiedAccessKey, &db.Account{
|
||||
UnidentifiedAccessKey: unidentifiedAccessKey,
|
||||
DiscoverableByPhoneNumber: true,
|
||||
}, codes.PermissionDenied},
|
||||
}
|
||||
|
||||
func TestVerifyPhoneNumberSearchConstantTime(t *testing.T) {
|
||||
for _, p := range testVerifyPhoneNumberSearchParameters {
|
||||
err := verifyPhoneNumberSearchConstantTime(p.providedValue, p.expectedValue, p.unidentifiedAccessKey, p.account)
|
||||
if (p.expectedErrorType != codes.OK) != (err != nil) {
|
||||
t.Fatalf("Expected %v, got %v",
|
||||
p.expectedErrorType, err)
|
||||
}
|
||||
|
||||
if p.expectedErrorType != codes.OK {
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != p.expectedErrorType || !ok {
|
||||
t.Fatalf("Expected error of type %v, got %v", p.expectedErrorType, grpcError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddJitter(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
jitteredVal := addJitter(1000, 10)
|
||||
if jitteredVal < 1000 || jitteredVal > 1100 {
|
||||
t.Errorf("Jittered value outside expected range [1000, 1100]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRandomDelay(t *testing.T) {
|
||||
start := time.Now()
|
||||
|
||||
// The request took 50 milliseconds, but require a minimum delay of 100 milliseconds.
|
||||
requestTime := 50 * time.Millisecond
|
||||
minDelay := 100 * time.Millisecond
|
||||
jitterPercent := 10
|
||||
maxJitter := time.Duration((float64(jitterPercent) / 100.0) * float64(minDelay))
|
||||
buffer := 10 * time.Millisecond
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
testStart := time.Now()
|
||||
addRandomDelay(start, start.Add(requestTime), minDelay, jitterPercent, "test")
|
||||
testDuration := time.Since(testStart)
|
||||
if testDuration < minDelay-requestTime {
|
||||
t.Errorf("Expected at least %v delay, got %v instead", minDelay-requestTime, testDuration)
|
||||
}
|
||||
if testDuration > minDelay-requestTime+maxJitter+buffer {
|
||||
t.Errorf("Expected at most %v delay, got %v instead", minDelay-requestTime+maxJitter+buffer, testDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
79
cmd/kt-server/kt_update_handler.go
Normal file
79
cmd/kt-server/kt_update_handler.go
Normal file
@ -0,0 +1,79 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
metrics "github.com/hashicorp/go-metrics"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
type KtUpdateHandler struct {
|
||||
config *config.APIConfig
|
||||
tx db.TransparencyStore
|
||||
ch chan<- updateRequest
|
||||
|
||||
pb.UnimplementedKeyTransparencyTestServiceServer
|
||||
}
|
||||
|
||||
func (h *KtUpdateHandler) Update(ctx context.Context, req *tpb.UpdateRequest) (*tpb.UpdateResponse, error) {
|
||||
start := time.Now()
|
||||
res, err := h.update(ctx, req, 5*time.Second)
|
||||
lbls := []metrics.Label{successLabel(err), grpcStatusLabel(err)}
|
||||
metrics.IncrCounterWithLabels([]string{"update_requests"}, 1, lbls)
|
||||
metrics.MeasureSinceWithLabels([]string{"update_duration"}, start, lbls)
|
||||
if err, _ := status.FromError(err); err.Code() == codes.Unknown {
|
||||
util.Log().Errorf("Unexpected update error in key transparency service: %v", err.Err())
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (h *KtUpdateHandler) update(ctx context.Context, req *tpb.UpdateRequest, timeout time.Duration) (*tpb.UpdateResponse, error) {
|
||||
tree, err := h.config.NewTree(h.tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pre, err := tree.PreUpdate(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan updateResponse, 1)
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
select {
|
||||
case h.ch <- updateRequest{req: pre, res: ch}:
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("submitting insertion request timed out: %w", ctx.Err())
|
||||
}
|
||||
select {
|
||||
case res := <-ch:
|
||||
if res.err != nil {
|
||||
return nil, res.err
|
||||
} else if res.res == nil {
|
||||
// In the case of tombstone updates, it is an expected case to get back
|
||||
// no update response and no error.
|
||||
return nil, nil
|
||||
}
|
||||
if req.ReturnUpdateResponse {
|
||||
return tree.PostUpdate(res.res)
|
||||
}
|
||||
return nil, nil
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("waiting for insertion result timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
250
cmd/kt-server/main.go
Normal file
250
cmd/kt-server/main.go
Normal file
@ -0,0 +1,250 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
//go:generate protoc -I ./pb -I ../../tree/transparency/pb --go_out=pb --go_opt=paths=source_relative --go-grpc_out=pb --go-grpc_opt=paths=source_relative key_transparency.proto key_transparency_query.proto key_transparency_test.proto
|
||||
|
||||
// Command kt-server is the main server process that answers all client
|
||||
// requests and sequences new changes to the log.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/health"
|
||||
healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
|
||||
healthpb "google.golang.org/grpc/health/grpc_health_v1"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "dev"
|
||||
GoVersion = runtime.Version()
|
||||
|
||||
configFile = flag.String("config", "", "Location of config file.")
|
||||
liveness = "liveness"
|
||||
readiness = "readiness"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}
|
||||
|
||||
// Load config from disk.
|
||||
if *configFile == "" {
|
||||
logger := zerolog.New(consoleWriter).With().Timestamp().Logger()
|
||||
logger.Fatal().Msg("no config file specified")
|
||||
}
|
||||
config, err := config.Read(*configFile)
|
||||
if err != nil {
|
||||
logger := zerolog.New(consoleWriter).With().Timestamp().Logger()
|
||||
logger.Fatal().Msgf("failed to parse config file: %v", err)
|
||||
}
|
||||
|
||||
var zeroLogLogger zerolog.Logger
|
||||
var logWriter io.Writer
|
||||
if len(config.LogOutputFile.String()) > 0 {
|
||||
logWriter = zerolog.MultiLevelWriter(
|
||||
consoleWriter,
|
||||
&lumberjack.Logger{
|
||||
Filename: config.LogOutputFile.String(),
|
||||
MaxBackups: 10,
|
||||
Compress: true,
|
||||
},
|
||||
)
|
||||
zeroLogLogger = zerolog.New(logWriter).With().Timestamp().Logger().Level(zerolog.InfoLevel)
|
||||
} else {
|
||||
logWriter = consoleWriter
|
||||
zeroLogLogger = zerolog.New(logWriter).With().Caller().Timestamp().Logger()
|
||||
}
|
||||
util.SetLoggerInstance(&zeroLogLogger)
|
||||
|
||||
// Register healthCheck service
|
||||
healthServer := grpc.NewServer()
|
||||
healthCheck := health.NewServer()
|
||||
healthgrpc.RegisterHealthServer(healthServer, healthCheck)
|
||||
|
||||
// Initialize liveness and readiness states
|
||||
healthCheck.SetServingStatus(liveness, healthpb.HealthCheckResponse_SERVING)
|
||||
healthCheck.SetServingStatus(readiness, healthpb.HealthCheckResponse_NOT_SERVING)
|
||||
|
||||
// Configure healthCheck service to listen on its dedicated port
|
||||
lis, err := net.Listen("tcp", config.HealthAddr.String())
|
||||
if err != nil {
|
||||
util.Log().Fatalf("failed to listen on health check port %v: %v", config.HealthAddr.String(), err)
|
||||
}
|
||||
util.Log().Infof("Starting health check server at: %v", config.HealthAddr)
|
||||
|
||||
// Start the health server.
|
||||
go healthServer.Serve(lis)
|
||||
|
||||
// Start the metrics server.
|
||||
exportMetrics(config.DatadogAddr.String())
|
||||
go metricsServer(config.MetricsAddr.String())
|
||||
|
||||
// Start the inserter thread.
|
||||
tx, err := config.DatabaseConfig.Connect()
|
||||
if err != nil {
|
||||
healthCheck.SetServingStatus(liveness, healthpb.HealthCheckResponse_NOT_SERVING)
|
||||
util.Log().Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Is this a new, empty tree?
|
||||
first := false
|
||||
if tth, _, err := tx.GetHead(); err != nil {
|
||||
healthCheck.SetServingStatus(liveness, healthpb.HealthCheckResponse_NOT_SERVING)
|
||||
util.Log().Fatalf("unable to get transparency tree head: %v", err)
|
||||
} else {
|
||||
util.Log().Infof("Tree size: %d", tth.TreeSize)
|
||||
first = tth.TreeSize == 0
|
||||
}
|
||||
|
||||
// This is the read-only service.
|
||||
// For now, it always queries the database directly and does not use a cache.
|
||||
if config.KtQueryServiceConfig != nil {
|
||||
// For read-only servers, cache immutable data.
|
||||
// The log tree data is only cached when the chunk is full (and therefore immutable).
|
||||
queryCachedTransparencyStore := db.NewCachedTransparencyStore(tx, db.TransparencyCache|db.PrefixCache|db.LogCache, config.CacheConfig.TopSize, config.CacheConfig.LogSize, config.CacheConfig.PrefixSize)
|
||||
|
||||
// Connect to the account DB
|
||||
accountDB, err := config.ConnectAccountDB()
|
||||
ktQueryHandler := &KtQueryHandler{config: config.APIConfig, tx: queryCachedTransparencyStore.Clone(), accountDB: accountDB}
|
||||
|
||||
// Create listener for specified port
|
||||
ktQueryListener, err := createListener(config.KtQueryServiceConfig)
|
||||
|
||||
if err != nil {
|
||||
healthCheck.SetServingStatus(liveness, healthpb.HealthCheckResponse_NOT_SERVING)
|
||||
util.Log().Fatalf("Failed to create listener for kt query server: %v", err)
|
||||
}
|
||||
|
||||
// Register kt query server
|
||||
ktQueryServer := grpc.NewServer(getServerOptions(config.KtQueryServiceConfig, nil)...)
|
||||
pb.RegisterKeyTransparencyQueryServiceServer(ktQueryServer, ktQueryHandler)
|
||||
|
||||
util.Log().Infof("Starting kt-query server at: %v", config.KtQueryServiceConfig.ServerAddr)
|
||||
if config.KtServiceConfig == nil && config.KtTestServiceConfig == nil {
|
||||
healthCheck.SetServingStatus(readiness, healthpb.HealthCheckResponse_SERVING)
|
||||
ktQueryServer.Serve(ktQueryListener)
|
||||
} else {
|
||||
go ktQueryServer.Serve(ktQueryListener)
|
||||
}
|
||||
}
|
||||
|
||||
ch := make(chan updateRequest)
|
||||
auditorTreeHeadsCh := make(chan updateAuditorTreeHeadRequest)
|
||||
|
||||
// Cache all data for the service that handles writes
|
||||
cachedTransparencyStore := db.NewCachedTransparencyStore(tx, db.TransparencyCache|db.PrefixCache|db.LogCache|db.HeadCache, config.CacheConfig.TopSize, config.CacheConfig.LogSize, config.CacheConfig.PrefixSize)
|
||||
|
||||
// Define a handler that provides a common interface for:
|
||||
// - stream-based updates
|
||||
// - distinguished updates
|
||||
// - manual updates from local testing
|
||||
// Its update functionality will only ever be exposed externally in a local development/testing context.
|
||||
updateHandler := &KtUpdateHandler{config: config.APIConfig, tx: cachedTransparencyStore.Clone(), ch: ch}
|
||||
|
||||
if config.KtServiceConfig != nil || config.KtTestServiceConfig != nil {
|
||||
updaterTree, err := config.APIConfig.NewTree(cachedTransparencyStore)
|
||||
if err != nil {
|
||||
healthCheck.SetServingStatus(liveness, healthpb.HealthCheckResponse_NOT_SERVING)
|
||||
util.Log().Fatalf("failed to initialize tree: %v", err)
|
||||
}
|
||||
// Start updater goroutine
|
||||
go updater(updaterTree, ch, auditorTreeHeadsCh, config.APIConfig.FakeUpdates)
|
||||
|
||||
// Start a goroutine that regularly updates a distinguished key
|
||||
if config.APIConfig.Distinguished != 0 {
|
||||
util.Log().Infof("Distinguished key will be maintained: %v", config.APIConfig.Distinguished)
|
||||
go distinguished(updateHandler, config.APIConfig.Distinguished)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the read and write service
|
||||
if config.KtServiceConfig != nil {
|
||||
// Start scanning a Kinesis stream, if one is provided.
|
||||
if config.StreamConfig != nil {
|
||||
s := &Streamer{config: config.APIConfig, tx: cachedTransparencyStore.Clone()}
|
||||
go func() {
|
||||
|
||||
var streamStartTimestamp time.Time
|
||||
|
||||
if first && config.StreamConfig.TableName != "" {
|
||||
// start the stream from when the backfill started, minus some padding for clock drift
|
||||
streamStartTimestamp = time.Now().Add(-time.Minute * 15)
|
||||
|
||||
util.Log().Infof("Backfilling from DynamoDB table %q", config.StreamConfig.TableName)
|
||||
if err := backfill(ctx, config.StreamConfig.TableName.String(), updateHandler); err != nil {
|
||||
healthCheck.SetServingStatus(liveness, healthpb.HealthCheckResponse_NOT_SERVING)
|
||||
util.Log().Fatalf("stream backfill failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
streamStartTimestamp = time.Now().Add(-config.StreamConfig.InitialHorizon)
|
||||
}
|
||||
util.Log().Infof("Starting stream processing from Kinesis stream %q", config.StreamConfig.Name.String())
|
||||
s.run(ctx, config.StreamConfig.Name.String(), streamStartTimestamp, updateHandler)
|
||||
}()
|
||||
}
|
||||
|
||||
ktHandler := &KtHandler{config: config.APIConfig, tx: cachedTransparencyStore.Clone(), ch: ch, auditorTreeHeadsCh: auditorTreeHeadsCh}
|
||||
ktServiceConfig := config.KtServiceConfig
|
||||
|
||||
// Create listener on the specified port
|
||||
ktListener, err := createListener(ktServiceConfig)
|
||||
|
||||
if err != nil {
|
||||
healthCheck.SetServingStatus(liveness, healthpb.HealthCheckResponse_NOT_SERVING)
|
||||
util.Log().Fatalf("Failed to create listener for kt server: %v", err)
|
||||
}
|
||||
|
||||
ktServer := grpc.NewServer(getServerOptions(config.KtServiceConfig, []grpc.UnaryServerInterceptor{storeAuditorNameInterceptor(config.KtServiceConfig)})...)
|
||||
pb.RegisterKeyTransparencyServiceServer(ktServer, ktHandler)
|
||||
util.Log().Infof("Starting kt server at: %v", ktServiceConfig.ServerAddr)
|
||||
if config.KtTestServiceConfig == nil {
|
||||
healthCheck.SetServingStatus(readiness, healthpb.HealthCheckResponse_SERVING)
|
||||
ktServer.Serve(ktListener)
|
||||
} else {
|
||||
go ktServer.Serve(ktListener)
|
||||
}
|
||||
}
|
||||
|
||||
// This is a service for local development.
|
||||
// For testing purposes, it exposes an endpoint for manual updates.
|
||||
if config.KtTestServiceConfig != nil {
|
||||
util.Log().Warnf("Test service config found. This should only be configured in a development environment.")
|
||||
ktTestServiceConfig := config.KtTestServiceConfig
|
||||
|
||||
// Create listener on the specified port
|
||||
ktTestListener, err := createListener(ktTestServiceConfig)
|
||||
|
||||
if err != nil {
|
||||
healthCheck.SetServingStatus(liveness, healthpb.HealthCheckResponse_NOT_SERVING)
|
||||
util.Log().Fatalf("Failed to create listener for kt test server: %v", err)
|
||||
}
|
||||
|
||||
ktTestServer := grpc.NewServer(getServerOptions(config.KtTestServiceConfig, nil)...)
|
||||
pb.RegisterKeyTransparencyTestServiceServer(ktTestServer, updateHandler)
|
||||
util.Log().Infof("Starting kt test server at: %v", ktTestServiceConfig.ServerAddr)
|
||||
healthCheck.SetServingStatus(readiness, healthpb.HealthCheckResponse_SERVING)
|
||||
ktTestServer.Serve(ktTestListener)
|
||||
}
|
||||
}
|
||||
|
||||
func createListener(serviceConfig *config.ServiceConfig) (net.Listener, error) {
|
||||
return net.Listen("tcp", serviceConfig.ServerAddr.String())
|
||||
}
|
||||
102
cmd/kt-server/metrics.go
Normal file
102
cmd/kt-server/metrics.go
Normal file
@ -0,0 +1,102 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
metrics "github.com/hashicorp/go-metrics"
|
||||
"github.com/hashicorp/go-metrics/datadog"
|
||||
"github.com/hashicorp/go-metrics/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
)
|
||||
|
||||
func successLabel(err error) metrics.Label {
|
||||
return metrics.Label{Name: "success", Value: fmt.Sprint(err == nil)}
|
||||
}
|
||||
|
||||
func auditorLabel(auditorName string) metrics.Label {
|
||||
return metrics.Label{Name: "auditor", Value: auditorName}
|
||||
}
|
||||
|
||||
func grpcStatusLabel(err error) metrics.Label {
|
||||
grpcError, _ := status.FromError(err)
|
||||
return metrics.Label{Name: "grpcStatus", Value: grpcError.Code().String()}
|
||||
}
|
||||
|
||||
func realLabel(real bool) metrics.Label {
|
||||
return metrics.Label{Name: "real", Value: fmt.Sprint(real)}
|
||||
}
|
||||
|
||||
func endpointLabel(endpoint string) metrics.Label {
|
||||
return metrics.Label{Name: "endpoint", Value: endpoint}
|
||||
}
|
||||
|
||||
func exportMetrics(addr string) {
|
||||
prom, err := prometheus.NewPrometheusSink()
|
||||
if err != nil {
|
||||
util.Log().Fatalf("building prometheus sink: %v", err)
|
||||
}
|
||||
sink := metrics.FanoutSink{prom}
|
||||
|
||||
if addr != "" {
|
||||
util.Log().Infof("Initiating datadog metrics at %q", addr)
|
||||
ddog, err := datadog.NewDogStatsdSink(addr, "")
|
||||
if err != nil {
|
||||
util.Log().Fatalf("error initializing statsd client: %v", err)
|
||||
}
|
||||
sink = append(sink, ddog)
|
||||
}
|
||||
|
||||
// Disable hostname tagging, this can be provided by the downstream sink
|
||||
cfg := metrics.DefaultConfig("kt")
|
||||
cfg.EnableHostname = false
|
||||
cfg.EnableHostnameLabel = false
|
||||
if _, err = metrics.NewGlobal(cfg, sink); err != nil {
|
||||
util.Log().Fatalf("error initializing metrics : %v", err)
|
||||
}
|
||||
|
||||
metrics.IncrCounterWithLabels([]string{"build_info"}, 1, []metrics.Label{
|
||||
{Name: "version", Value: Version},
|
||||
{Name: "goversion", Value: GoVersion},
|
||||
})
|
||||
}
|
||||
|
||||
func metricsServer(addr string) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path == "/" {
|
||||
fmt.Fprintln(rw, "Hi, I'm a key transparency metrics and debugging server!")
|
||||
} else {
|
||||
rw.WriteHeader(404)
|
||||
fmt.Fprintln(rw, "404 not found")
|
||||
}
|
||||
})
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
|
||||
mux.HandleFunc("/debug/version", func(w http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprintf(w, "Version: %s, GoVersion: %s", Version, GoVersion)
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
util.Log().Infof("Starting metrics server at: %v", addr)
|
||||
// go 1.24 requires a constant format string to Printf-like functions
|
||||
util.Log().Fatalf("%s", srv.ListenAndServe().Error())
|
||||
}
|
||||
228
cmd/kt-server/pb/key_transparency.pb.go
Normal file
228
cmd/kt-server/pb/key_transparency.pb.go
Normal file
@ -0,0 +1,228 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v5.28.3
|
||||
// source: key_transparency.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
pb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// AuditRequest comes from a third-party auditor that wishes to sync with the
|
||||
// latest state of the log.
|
||||
type AuditRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Start uint64 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"`
|
||||
Limit uint64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AuditRequest) Reset() {
|
||||
*x = AuditRequest{}
|
||||
mi := &file_key_transparency_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuditRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuditRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AuditRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuditRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AuditRequest) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *AuditRequest) GetStart() uint64 {
|
||||
if x != nil {
|
||||
return x.Start
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *AuditRequest) GetLimit() uint64 {
|
||||
if x != nil {
|
||||
return x.Limit
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// AuditResponse contains the list of new changes to the log.
|
||||
type AuditResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Updates []*pb.AuditorUpdate `protobuf:"bytes,1,rep,name=updates,proto3" json:"updates,omitempty"`
|
||||
More bool `protobuf:"varint,2,opt,name=more,proto3" json:"more,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AuditResponse) Reset() {
|
||||
*x = AuditResponse{}
|
||||
mi := &file_key_transparency_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuditResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuditResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuditResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuditResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AuditResponse) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *AuditResponse) GetUpdates() []*pb.AuditorUpdate {
|
||||
if x != nil {
|
||||
return x.Updates
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AuditResponse) GetMore() bool {
|
||||
if x != nil {
|
||||
return x.More
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var File_key_transparency_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_key_transparency_proto_rawDesc = []byte{
|
||||
0x0a, 0x16, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e,
|
||||
0x63, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x6b, 0x74, 0x1a, 0x1b, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
|
||||
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73,
|
||||
0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3a, 0x0a,
|
||||
0x0c, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a,
|
||||
0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x73, 0x74,
|
||||
0x61, 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x04, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x5a, 0x0a, 0x0d, 0x41, 0x75, 0x64,
|
||||
0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x07, 0x75, 0x70,
|
||||
0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x72,
|
||||
0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74,
|
||||
0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65,
|
||||
0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||
0x04, 0x6d, 0x6f, 0x72, 0x65, 0x32, 0x93, 0x01, 0x0a, 0x16, 0x4b, 0x65, 0x79, 0x54, 0x72, 0x61,
|
||||
0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
||||
0x12, 0x2e, 0x0a, 0x05, 0x41, 0x75, 0x64, 0x69, 0x74, 0x12, 0x10, 0x2e, 0x6b, 0x74, 0x2e, 0x41,
|
||||
0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x6b, 0x74,
|
||||
0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
|
||||
0x12, 0x49, 0x0a, 0x0e, 0x53, 0x65, 0x74, 0x41, 0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x48, 0x65,
|
||||
0x61, 0x64, 0x12, 0x1d, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63,
|
||||
0x79, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x54, 0x72, 0x65, 0x65, 0x48, 0x65, 0x61,
|
||||
0x64, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x30, 0x5a, 0x2e, 0x67,
|
||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c,
|
||||
0x61, 0x70, 0x70, 0x2f, 0x6b, 0x65, 0x79, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65,
|
||||
0x6e, 0x63, 0x79, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x6b, 0x74, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_key_transparency_proto_rawDescOnce sync.Once
|
||||
file_key_transparency_proto_rawDescData = file_key_transparency_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_key_transparency_proto_rawDescGZIP() []byte {
|
||||
file_key_transparency_proto_rawDescOnce.Do(func() {
|
||||
file_key_transparency_proto_rawDescData = protoimpl.X.CompressGZIP(file_key_transparency_proto_rawDescData)
|
||||
})
|
||||
return file_key_transparency_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_key_transparency_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_key_transparency_proto_goTypes = []any{
|
||||
(*AuditRequest)(nil), // 0: kt.AuditRequest
|
||||
(*AuditResponse)(nil), // 1: kt.AuditResponse
|
||||
(*pb.AuditorUpdate)(nil), // 2: transparency.AuditorUpdate
|
||||
(*pb.AuditorTreeHead)(nil), // 3: transparency.AuditorTreeHead
|
||||
(*emptypb.Empty)(nil), // 4: google.protobuf.Empty
|
||||
}
|
||||
var file_key_transparency_proto_depIdxs = []int32{
|
||||
2, // 0: kt.AuditResponse.updates:type_name -> transparency.AuditorUpdate
|
||||
0, // 1: kt.KeyTransparencyService.Audit:input_type -> kt.AuditRequest
|
||||
3, // 2: kt.KeyTransparencyService.SetAuditorHead:input_type -> transparency.AuditorTreeHead
|
||||
1, // 3: kt.KeyTransparencyService.Audit:output_type -> kt.AuditResponse
|
||||
4, // 4: kt.KeyTransparencyService.SetAuditorHead:output_type -> google.protobuf.Empty
|
||||
3, // [3:5] is the sub-list for method output_type
|
||||
1, // [1:3] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_key_transparency_proto_init() }
|
||||
func file_key_transparency_proto_init() {
|
||||
if File_key_transparency_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_key_transparency_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_key_transparency_proto_goTypes,
|
||||
DependencyIndexes: file_key_transparency_proto_depIdxs,
|
||||
MessageInfos: file_key_transparency_proto_msgTypes,
|
||||
}.Build()
|
||||
File_key_transparency_proto = out.File
|
||||
file_key_transparency_proto_rawDesc = nil
|
||||
file_key_transparency_proto_goTypes = nil
|
||||
file_key_transparency_proto_depIdxs = nil
|
||||
}
|
||||
34
cmd/kt-server/pb/key_transparency.proto
Normal file
34
cmd/kt-server/pb/key_transparency.proto
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
package kt;
|
||||
|
||||
option go_package = "github.com/signalapp/keytransparency/cmd/kt/pb";
|
||||
import "google/protobuf/empty.proto";
|
||||
import "transparency.proto";
|
||||
|
||||
// AuditRequest comes from a third-party auditor that wishes to sync with the
|
||||
// latest state of the log.
|
||||
message AuditRequest {
|
||||
uint64 start = 1;
|
||||
uint64 limit = 2;
|
||||
}
|
||||
|
||||
// AuditResponse contains the list of new changes to the log.
|
||||
message AuditResponse {
|
||||
repeated transparency.AuditorUpdate updates = 1;
|
||||
bool more = 2;
|
||||
}
|
||||
|
||||
// A key transparency service used to update the transparency log and to accept auditor-signed tree heads.
|
||||
// With the exception of the third-party auditor, this service's endpoints are *not* intended to be used by external clients.
|
||||
// It is exposed to the public internet by necessity but will reject calls from unauthenticated callers.
|
||||
service KeyTransparencyService {
|
||||
// Auditors use this endpoint to request a batch of key transparency service updates to audit.
|
||||
rpc Audit(AuditRequest) returns (AuditResponse) {}
|
||||
// Auditors use this endpoint to return a signature on the log tree root hash corresponding to the last audited update.
|
||||
rpc SetAuditorHead(transparency.AuditorTreeHead) returns (google.protobuf.Empty) {}
|
||||
}
|
||||
179
cmd/kt-server/pb/key_transparency_grpc.pb.go
Normal file
179
cmd/kt-server/pb/key_transparency_grpc.pb.go
Normal file
@ -0,0 +1,179 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.28.3
|
||||
// source: key_transparency.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
context "context"
|
||||
pb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
KeyTransparencyService_Audit_FullMethodName = "/kt.KeyTransparencyService/Audit"
|
||||
KeyTransparencyService_SetAuditorHead_FullMethodName = "/kt.KeyTransparencyService/SetAuditorHead"
|
||||
)
|
||||
|
||||
// KeyTransparencyServiceClient is the client API for KeyTransparencyService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// A key transparency service used to update the transparency log and to accept auditor-signed tree heads.
|
||||
// With the exception of the third-party auditor, this service's endpoints are *not* intended to be used by external clients.
|
||||
// It is exposed to the public internet by necessity but will reject calls from unauthenticated callers.
|
||||
type KeyTransparencyServiceClient interface {
|
||||
// Auditors use this endpoint to request a batch of key transparency service updates to audit.
|
||||
Audit(ctx context.Context, in *AuditRequest, opts ...grpc.CallOption) (*AuditResponse, error)
|
||||
// Auditors use this endpoint to return a signature on the log tree root hash corresponding to the last audited update.
|
||||
SetAuditorHead(ctx context.Context, in *pb.AuditorTreeHead, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
}
|
||||
|
||||
type keyTransparencyServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewKeyTransparencyServiceClient(cc grpc.ClientConnInterface) KeyTransparencyServiceClient {
|
||||
return &keyTransparencyServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *keyTransparencyServiceClient) Audit(ctx context.Context, in *AuditRequest, opts ...grpc.CallOption) (*AuditResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AuditResponse)
|
||||
err := c.cc.Invoke(ctx, KeyTransparencyService_Audit_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *keyTransparencyServiceClient) SetAuditorHead(ctx context.Context, in *pb.AuditorTreeHead, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, KeyTransparencyService_SetAuditorHead_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// KeyTransparencyServiceServer is the server API for KeyTransparencyService service.
|
||||
// All implementations must embed UnimplementedKeyTransparencyServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// A key transparency service used to update the transparency log and to accept auditor-signed tree heads.
|
||||
// With the exception of the third-party auditor, this service's endpoints are *not* intended to be used by external clients.
|
||||
// It is exposed to the public internet by necessity but will reject calls from unauthenticated callers.
|
||||
type KeyTransparencyServiceServer interface {
|
||||
// Auditors use this endpoint to request a batch of key transparency service updates to audit.
|
||||
Audit(context.Context, *AuditRequest) (*AuditResponse, error)
|
||||
// Auditors use this endpoint to return a signature on the log tree root hash corresponding to the last audited update.
|
||||
SetAuditorHead(context.Context, *pb.AuditorTreeHead) (*emptypb.Empty, error)
|
||||
mustEmbedUnimplementedKeyTransparencyServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedKeyTransparencyServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedKeyTransparencyServiceServer struct{}
|
||||
|
||||
func (UnimplementedKeyTransparencyServiceServer) Audit(context.Context, *AuditRequest) (*AuditResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Audit not implemented")
|
||||
}
|
||||
func (UnimplementedKeyTransparencyServiceServer) SetAuditorHead(context.Context, *pb.AuditorTreeHead) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetAuditorHead not implemented")
|
||||
}
|
||||
func (UnimplementedKeyTransparencyServiceServer) mustEmbedUnimplementedKeyTransparencyServiceServer() {
|
||||
}
|
||||
func (UnimplementedKeyTransparencyServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeKeyTransparencyServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to KeyTransparencyServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeKeyTransparencyServiceServer interface {
|
||||
mustEmbedUnimplementedKeyTransparencyServiceServer()
|
||||
}
|
||||
|
||||
func RegisterKeyTransparencyServiceServer(s grpc.ServiceRegistrar, srv KeyTransparencyServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedKeyTransparencyServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&KeyTransparencyService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _KeyTransparencyService_Audit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AuditRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KeyTransparencyServiceServer).Audit(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: KeyTransparencyService_Audit_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KeyTransparencyServiceServer).Audit(ctx, req.(*AuditRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _KeyTransparencyService_SetAuditorHead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(pb.AuditorTreeHead)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KeyTransparencyServiceServer).SetAuditorHead(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: KeyTransparencyService_SetAuditorHead_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KeyTransparencyServiceServer).SetAuditorHead(ctx, req.(*pb.AuditorTreeHead))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// KeyTransparencyService_ServiceDesc is the grpc.ServiceDesc for KeyTransparencyService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var KeyTransparencyService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "kt.KeyTransparencyService",
|
||||
HandlerType: (*KeyTransparencyServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Audit",
|
||||
Handler: _KeyTransparencyService_Audit_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetAuditorHead",
|
||||
Handler: _KeyTransparencyService_SetAuditorHead_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "key_transparency.proto",
|
||||
}
|
||||
988
cmd/kt-server/pb/key_transparency_query.pb.go
Normal file
988
cmd/kt-server/pb/key_transparency_query.pb.go
Normal file
@ -0,0 +1,988 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v5.28.3
|
||||
// source: key_transparency_query.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
pb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// DistinguishedRequest looks up the most recent `distinguished` key in the transparency log.
|
||||
type DistinguishedRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The tree size of the last verified distinguished request. With the exception of a client's
|
||||
// very first request, this field should always be populated.
|
||||
Last *uint64 `protobuf:"varint,1,opt,name=last,proto3,oneof" json:"last,omitempty"`
|
||||
}
|
||||
|
||||
func (x *DistinguishedRequest) Reset() {
|
||||
*x = DistinguishedRequest{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DistinguishedRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DistinguishedRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DistinguishedRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DistinguishedRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DistinguishedRequest) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *DistinguishedRequest) GetLast() uint64 {
|
||||
if x != nil && x.Last != nil {
|
||||
return *x.Last
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// DistinguishedResponse contains the tree head and search proof for the most recent `distinguished` key in the log.
|
||||
type DistinguishedResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
TreeHead *pb.FullTreeHead `protobuf:"bytes,1,opt,name=tree_head,json=treeHead,proto3" json:"tree_head,omitempty"`
|
||||
Distinguished *CondensedTreeSearchResponse `protobuf:"bytes,2,opt,name=distinguished,proto3" json:"distinguished,omitempty"`
|
||||
}
|
||||
|
||||
func (x *DistinguishedResponse) Reset() {
|
||||
*x = DistinguishedResponse{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DistinguishedResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DistinguishedResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DistinguishedResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DistinguishedResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DistinguishedResponse) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *DistinguishedResponse) GetTreeHead() *pb.FullTreeHead {
|
||||
if x != nil {
|
||||
return x.TreeHead
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *DistinguishedResponse) GetDistinguished() *CondensedTreeSearchResponse {
|
||||
if x != nil {
|
||||
return x.Distinguished
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchRequest comes from a user that wishes to look up one or more identifiers in the transparency log.
|
||||
type SearchRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Aci []byte `protobuf:"bytes,1,opt,name=aci,proto3" json:"aci,omitempty"`
|
||||
AciIdentityKey []byte `protobuf:"bytes,2,opt,name=aci_identity_key,json=aciIdentityKey,proto3" json:"aci_identity_key,omitempty"`
|
||||
UsernameHash []byte `protobuf:"bytes,3,opt,name=username_hash,json=usernameHash,proto3,oneof" json:"username_hash,omitempty"`
|
||||
E164SearchRequest *E164SearchRequest `protobuf:"bytes,4,opt,name=e164_search_request,json=e164SearchRequest,proto3,oneof" json:"e164_search_request,omitempty"`
|
||||
Consistency *pb.Consistency `protobuf:"bytes,5,opt,name=consistency,proto3" json:"consistency,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SearchRequest) Reset() {
|
||||
*x = SearchRequest{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SearchRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SearchRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SearchRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SearchRequest) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *SearchRequest) GetAci() []byte {
|
||||
if x != nil {
|
||||
return x.Aci
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SearchRequest) GetAciIdentityKey() []byte {
|
||||
if x != nil {
|
||||
return x.AciIdentityKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SearchRequest) GetUsernameHash() []byte {
|
||||
if x != nil {
|
||||
return x.UsernameHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SearchRequest) GetE164SearchRequest() *E164SearchRequest {
|
||||
if x != nil {
|
||||
return x.E164SearchRequest
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SearchRequest) GetConsistency() *pb.Consistency {
|
||||
if x != nil {
|
||||
return x.Consistency
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// E164SearchRequest contains the data that the user must provide when looking up an E164
|
||||
type E164SearchRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
E164 *string `protobuf:"bytes,1,opt,name=e164,proto3,oneof" json:"e164,omitempty"`
|
||||
UnidentifiedAccessKey []byte `protobuf:"bytes,2,opt,name=unidentified_access_key,json=unidentifiedAccessKey,proto3" json:"unidentified_access_key,omitempty"`
|
||||
}
|
||||
|
||||
func (x *E164SearchRequest) Reset() {
|
||||
*x = E164SearchRequest{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *E164SearchRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*E164SearchRequest) ProtoMessage() {}
|
||||
|
||||
func (x *E164SearchRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use E164SearchRequest.ProtoReflect.Descriptor instead.
|
||||
func (*E164SearchRequest) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *E164SearchRequest) GetE164() string {
|
||||
if x != nil && x.E164 != nil {
|
||||
return *x.E164
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *E164SearchRequest) GetUnidentifiedAccessKey() []byte {
|
||||
if x != nil {
|
||||
return x.UnidentifiedAccessKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CondensedTreeSearchResponse contains the search proof and other data for a given identifier.
|
||||
// It is used in the SearchResponse protobuf which is returned to external clients.
|
||||
type CondensedTreeSearchResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
VrfProof []byte `protobuf:"bytes,1,opt,name=vrf_proof,json=vrfProof,proto3" json:"vrf_proof,omitempty"`
|
||||
Search *pb.SearchProof `protobuf:"bytes,2,opt,name=search,proto3" json:"search,omitempty"`
|
||||
Opening []byte `protobuf:"bytes,3,opt,name=opening,proto3" json:"opening,omitempty"`
|
||||
Value *pb.UpdateValue `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (x *CondensedTreeSearchResponse) Reset() {
|
||||
*x = CondensedTreeSearchResponse{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CondensedTreeSearchResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CondensedTreeSearchResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CondensedTreeSearchResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CondensedTreeSearchResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CondensedTreeSearchResponse) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *CondensedTreeSearchResponse) GetVrfProof() []byte {
|
||||
if x != nil {
|
||||
return x.VrfProof
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CondensedTreeSearchResponse) GetSearch() *pb.SearchProof {
|
||||
if x != nil {
|
||||
return x.Search
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CondensedTreeSearchResponse) GetOpening() []byte {
|
||||
if x != nil {
|
||||
return x.Opening
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CondensedTreeSearchResponse) GetValue() *pb.UpdateValue {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchResponse contains search proofs for each of the requested identifiers.
|
||||
type SearchResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
TreeHead *pb.FullTreeHead `protobuf:"bytes,1,opt,name=tree_head,json=treeHead,proto3" json:"tree_head,omitempty"`
|
||||
Aci *CondensedTreeSearchResponse `protobuf:"bytes,2,opt,name=aci,proto3" json:"aci,omitempty"`
|
||||
E164 *CondensedTreeSearchResponse `protobuf:"bytes,3,opt,name=e164,proto3,oneof" json:"e164,omitempty"`
|
||||
UsernameHash *CondensedTreeSearchResponse `protobuf:"bytes,4,opt,name=username_hash,json=usernameHash,proto3,oneof" json:"username_hash,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SearchResponse) Reset() {
|
||||
*x = SearchResponse{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SearchResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SearchResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SearchResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SearchResponse) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *SearchResponse) GetTreeHead() *pb.FullTreeHead {
|
||||
if x != nil {
|
||||
return x.TreeHead
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SearchResponse) GetAci() *CondensedTreeSearchResponse {
|
||||
if x != nil {
|
||||
return x.Aci
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SearchResponse) GetE164() *CondensedTreeSearchResponse {
|
||||
if x != nil {
|
||||
return x.E164
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SearchResponse) GetUsernameHash() *CondensedTreeSearchResponse {
|
||||
if x != nil {
|
||||
return x.UsernameHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MonitorRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Aci *AciMonitorRequest `protobuf:"bytes,1,opt,name=aci,proto3" json:"aci,omitempty"`
|
||||
UsernameHash *UsernameHashMonitorRequest `protobuf:"bytes,2,opt,name=username_hash,json=usernameHash,proto3,oneof" json:"username_hash,omitempty"`
|
||||
E164 *E164MonitorRequest `protobuf:"bytes,3,opt,name=e164,proto3,oneof" json:"e164,omitempty"`
|
||||
Consistency *pb.Consistency `protobuf:"bytes,4,opt,name=consistency,proto3" json:"consistency,omitempty"`
|
||||
}
|
||||
|
||||
func (x *MonitorRequest) Reset() {
|
||||
*x = MonitorRequest{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MonitorRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MonitorRequest) ProtoMessage() {}
|
||||
|
||||
func (x *MonitorRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MonitorRequest.ProtoReflect.Descriptor instead.
|
||||
func (*MonitorRequest) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *MonitorRequest) GetAci() *AciMonitorRequest {
|
||||
if x != nil {
|
||||
return x.Aci
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MonitorRequest) GetUsernameHash() *UsernameHashMonitorRequest {
|
||||
if x != nil {
|
||||
return x.UsernameHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MonitorRequest) GetE164() *E164MonitorRequest {
|
||||
if x != nil {
|
||||
return x.E164
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MonitorRequest) GetConsistency() *pb.Consistency {
|
||||
if x != nil {
|
||||
return x.Consistency
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AciMonitorRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Aci []byte `protobuf:"bytes,1,opt,name=aci,proto3" json:"aci,omitempty"`
|
||||
EntryPosition uint64 `protobuf:"varint,2,opt,name=entry_position,json=entryPosition,proto3" json:"entry_position,omitempty"`
|
||||
CommitmentIndex []byte `protobuf:"bytes,3,opt,name=commitment_index,json=commitmentIndex,proto3" json:"commitment_index,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AciMonitorRequest) Reset() {
|
||||
*x = AciMonitorRequest{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AciMonitorRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AciMonitorRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AciMonitorRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AciMonitorRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AciMonitorRequest) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *AciMonitorRequest) GetAci() []byte {
|
||||
if x != nil {
|
||||
return x.Aci
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AciMonitorRequest) GetEntryPosition() uint64 {
|
||||
if x != nil {
|
||||
return x.EntryPosition
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *AciMonitorRequest) GetCommitmentIndex() []byte {
|
||||
if x != nil {
|
||||
return x.CommitmentIndex
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UsernameHashMonitorRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
UsernameHash []byte `protobuf:"bytes,1,opt,name=username_hash,json=usernameHash,proto3" json:"username_hash,omitempty"`
|
||||
EntryPosition uint64 `protobuf:"varint,2,opt,name=entry_position,json=entryPosition,proto3" json:"entry_position,omitempty"`
|
||||
CommitmentIndex []byte `protobuf:"bytes,3,opt,name=commitment_index,json=commitmentIndex,proto3" json:"commitment_index,omitempty"`
|
||||
}
|
||||
|
||||
func (x *UsernameHashMonitorRequest) Reset() {
|
||||
*x = UsernameHashMonitorRequest{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UsernameHashMonitorRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UsernameHashMonitorRequest) ProtoMessage() {}
|
||||
|
||||
func (x *UsernameHashMonitorRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use UsernameHashMonitorRequest.ProtoReflect.Descriptor instead.
|
||||
func (*UsernameHashMonitorRequest) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *UsernameHashMonitorRequest) GetUsernameHash() []byte {
|
||||
if x != nil {
|
||||
return x.UsernameHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *UsernameHashMonitorRequest) GetEntryPosition() uint64 {
|
||||
if x != nil {
|
||||
return x.EntryPosition
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *UsernameHashMonitorRequest) GetCommitmentIndex() []byte {
|
||||
if x != nil {
|
||||
return x.CommitmentIndex
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type E164MonitorRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
E164 *string `protobuf:"bytes,1,opt,name=e164,proto3,oneof" json:"e164,omitempty"`
|
||||
EntryPosition uint64 `protobuf:"varint,2,opt,name=entry_position,json=entryPosition,proto3" json:"entry_position,omitempty"`
|
||||
CommitmentIndex []byte `protobuf:"bytes,3,opt,name=commitment_index,json=commitmentIndex,proto3" json:"commitment_index,omitempty"`
|
||||
}
|
||||
|
||||
func (x *E164MonitorRequest) Reset() {
|
||||
*x = E164MonitorRequest{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *E164MonitorRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*E164MonitorRequest) ProtoMessage() {}
|
||||
|
||||
func (x *E164MonitorRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use E164MonitorRequest.ProtoReflect.Descriptor instead.
|
||||
func (*E164MonitorRequest) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *E164MonitorRequest) GetE164() string {
|
||||
if x != nil && x.E164 != nil {
|
||||
return *x.E164
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *E164MonitorRequest) GetEntryPosition() uint64 {
|
||||
if x != nil {
|
||||
return x.EntryPosition
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *E164MonitorRequest) GetCommitmentIndex() []byte {
|
||||
if x != nil {
|
||||
return x.CommitmentIndex
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MonitorResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
TreeHead *pb.FullTreeHead `protobuf:"bytes,1,opt,name=tree_head,json=treeHead,proto3" json:"tree_head,omitempty"`
|
||||
Aci *pb.MonitorProof `protobuf:"bytes,2,opt,name=aci,proto3" json:"aci,omitempty"`
|
||||
UsernameHash *pb.MonitorProof `protobuf:"bytes,3,opt,name=username_hash,json=usernameHash,proto3,oneof" json:"username_hash,omitempty"`
|
||||
E164 *pb.MonitorProof `protobuf:"bytes,4,opt,name=e164,proto3,oneof" json:"e164,omitempty"`
|
||||
Inclusion [][]byte `protobuf:"bytes,5,rep,name=inclusion,proto3" json:"inclusion,omitempty"`
|
||||
}
|
||||
|
||||
func (x *MonitorResponse) Reset() {
|
||||
*x = MonitorResponse{}
|
||||
mi := &file_key_transparency_query_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MonitorResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MonitorResponse) ProtoMessage() {}
|
||||
|
||||
func (x *MonitorResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_transparency_query_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MonitorResponse.ProtoReflect.Descriptor instead.
|
||||
func (*MonitorResponse) Descriptor() ([]byte, []int) {
|
||||
return file_key_transparency_query_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *MonitorResponse) GetTreeHead() *pb.FullTreeHead {
|
||||
if x != nil {
|
||||
return x.TreeHead
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MonitorResponse) GetAci() *pb.MonitorProof {
|
||||
if x != nil {
|
||||
return x.Aci
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MonitorResponse) GetUsernameHash() *pb.MonitorProof {
|
||||
if x != nil {
|
||||
return x.UsernameHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MonitorResponse) GetE164() *pb.MonitorProof {
|
||||
if x != nil {
|
||||
return x.E164
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MonitorResponse) GetInclusion() [][]byte {
|
||||
if x != nil {
|
||||
return x.Inclusion
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_key_transparency_query_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_key_transparency_query_proto_rawDesc = []byte{
|
||||
0x0a, 0x1c, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e,
|
||||
0x63, 0x79, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08,
|
||||
0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70,
|
||||
0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x38, 0x0a, 0x14,
|
||||
0x44, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x75, 0x69, 0x73, 0x68, 0x65, 0x64, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x04, 0x6c, 0x61, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x6c, 0x61, 0x73, 0x74, 0x88, 0x01, 0x01, 0x42, 0x07, 0x0a,
|
||||
0x05, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x22, 0x9d, 0x01, 0x0a, 0x15, 0x44, 0x69, 0x73, 0x74, 0x69,
|
||||
0x6e, 0x67, 0x75, 0x69, 0x73, 0x68, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x12, 0x37, 0x0a, 0x09, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e,
|
||||
0x63, 0x79, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x54, 0x72, 0x65, 0x65, 0x48, 0x65, 0x61, 0x64, 0x52,
|
||||
0x08, 0x74, 0x72, 0x65, 0x65, 0x48, 0x65, 0x61, 0x64, 0x12, 0x4b, 0x0a, 0x0d, 0x64, 0x69, 0x73,
|
||||
0x74, 0x69, 0x6e, 0x67, 0x75, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x25, 0x2e, 0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64,
|
||||
0x65, 0x6e, 0x73, 0x65, 0x64, 0x54, 0x72, 0x65, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67,
|
||||
0x75, 0x69, 0x73, 0x68, 0x65, 0x64, 0x22, 0xae, 0x02, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63,
|
||||
0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x69, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x61, 0x63, 0x69, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x63,
|
||||
0x69, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x61, 0x63, 0x69, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
|
||||
0x79, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0c, 0x75,
|
||||
0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x48, 0x61, 0x73, 0x68, 0x88, 0x01, 0x01, 0x12, 0x50,
|
||||
0x0a, 0x13, 0x65, 0x31, 0x36, 0x34, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x72, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6b, 0x74,
|
||||
0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x45, 0x31, 0x36, 0x34, 0x53, 0x65, 0x61, 0x72, 0x63,
|
||||
0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x01, 0x52, 0x11, 0x65, 0x31, 0x36, 0x34,
|
||||
0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x88, 0x01, 0x01,
|
||||
0x12, 0x3b, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18,
|
||||
0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72,
|
||||
0x65, 0x6e, 0x63, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79,
|
||||
0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x10, 0x0a,
|
||||
0x0e, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x42,
|
||||
0x16, 0x0a, 0x14, 0x5f, 0x65, 0x31, 0x36, 0x34, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f,
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x6d, 0x0a, 0x11, 0x45, 0x31, 0x36, 0x34, 0x53,
|
||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x04,
|
||||
0x65, 0x31, 0x36, 0x34, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x65, 0x31,
|
||||
0x36, 0x34, 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x17, 0x75, 0x6e, 0x69, 0x64, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x75, 0x6e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x42, 0x07, 0x0a,
|
||||
0x05, 0x5f, 0x65, 0x31, 0x36, 0x34, 0x22, 0xb8, 0x01, 0x0a, 0x1b, 0x43, 0x6f, 0x6e, 0x64, 0x65,
|
||||
0x6e, 0x73, 0x65, 0x64, 0x54, 0x72, 0x65, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x72, 0x66, 0x5f, 0x70, 0x72,
|
||||
0x6f, 0x6f, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x76, 0x72, 0x66, 0x50, 0x72,
|
||||
0x6f, 0x6f, 0x66, 0x12, 0x31, 0x0a, 0x06, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e,
|
||||
0x63, 0x79, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x06,
|
||||
0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x65, 0x6e, 0x69, 0x6e,
|
||||
0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x65, 0x6e, 0x69, 0x6e, 0x67,
|
||||
0x12, 0x2f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x19, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x55,
|
||||
0x70, 0x64, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x22, 0xae, 0x02, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x09, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x68, 0x65, 0x61,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70,
|
||||
0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x54, 0x72, 0x65, 0x65, 0x48,
|
||||
0x65, 0x61, 0x64, 0x52, 0x08, 0x74, 0x72, 0x65, 0x65, 0x48, 0x65, 0x61, 0x64, 0x12, 0x37, 0x0a,
|
||||
0x03, 0x61, 0x63, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x74, 0x5f,
|
||||
0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x65, 0x6e, 0x73, 0x65, 0x64, 0x54,
|
||||
0x72, 0x65, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x52, 0x03, 0x61, 0x63, 0x69, 0x12, 0x3e, 0x0a, 0x04, 0x65, 0x31, 0x36, 0x34, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e,
|
||||
0x43, 0x6f, 0x6e, 0x64, 0x65, 0x6e, 0x73, 0x65, 0x64, 0x54, 0x72, 0x65, 0x65, 0x53, 0x65, 0x61,
|
||||
0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x04, 0x65,
|
||||
0x31, 0x36, 0x34, 0x88, 0x01, 0x01, 0x12, 0x4f, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61,
|
||||
0x6d, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e,
|
||||
0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x65, 0x6e, 0x73,
|
||||
0x65, 0x64, 0x54, 0x72, 0x65, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x48, 0x01, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x48, 0x61, 0x73, 0x68, 0x88, 0x01, 0x01, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x65, 0x31, 0x36, 0x34,
|
||||
0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x68, 0x61,
|
||||
0x73, 0x68, 0x22, 0x9e, 0x02, 0x0a, 0x0e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x03, 0x61, 0x63, 0x69, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x41, 0x63,
|
||||
0x69, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52,
|
||||
0x03, 0x61, 0x63, 0x69, 0x12, 0x4e, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x74,
|
||||
0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x48,
|
||||
0x61, 0x73, 0x68, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x48, 0x00, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x48, 0x61, 0x73,
|
||||
0x68, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x04, 0x65, 0x31, 0x36, 0x34, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x45, 0x31,
|
||||
0x36, 0x34, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x48, 0x01, 0x52, 0x04, 0x65, 0x31, 0x36, 0x34, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x0b, 0x63,
|
||||
0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x19, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e,
|
||||
0x43, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x0b, 0x63, 0x6f, 0x6e,
|
||||
0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x75, 0x73, 0x65,
|
||||
0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x65,
|
||||
0x31, 0x36, 0x34, 0x22, 0x77, 0x0a, 0x11, 0x41, 0x63, 0x69, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f,
|
||||
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x63, 0x69, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x61, 0x63, 0x69, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e,
|
||||
0x74, 0x72, 0x79, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x04, 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f,
|
||||
0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x93, 0x01, 0x0a,
|
||||
0x1a, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x48, 0x61, 0x73, 0x68, 0x4d, 0x6f, 0x6e,
|
||||
0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x75,
|
||||
0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0c, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x48, 0x61, 0x73, 0x68,
|
||||
0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x50,
|
||||
0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x69,
|
||||
0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0c, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x64,
|
||||
0x65, 0x78, 0x22, 0x88, 0x01, 0x0a, 0x12, 0x45, 0x31, 0x36, 0x34, 0x4d, 0x6f, 0x6e, 0x69, 0x74,
|
||||
0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x04, 0x65, 0x31, 0x36,
|
||||
0x34, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x65, 0x31, 0x36, 0x34, 0x88,
|
||||
0x01, 0x01, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x70, 0x6f, 0x73, 0x69,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x72,
|
||||
0x79, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x0c, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x49,
|
||||
0x6e, 0x64, 0x65, 0x78, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x65, 0x31, 0x36, 0x34, 0x22, 0xac, 0x02,
|
||||
0x0a, 0x0f, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x37, 0x0a, 0x09, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65,
|
||||
0x6e, 0x63, 0x79, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x54, 0x72, 0x65, 0x65, 0x48, 0x65, 0x61, 0x64,
|
||||
0x52, 0x08, 0x74, 0x72, 0x65, 0x65, 0x48, 0x65, 0x61, 0x64, 0x12, 0x2c, 0x0a, 0x03, 0x61, 0x63,
|
||||
0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70,
|
||||
0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x50, 0x72,
|
||||
0x6f, 0x6f, 0x66, 0x52, 0x03, 0x61, 0x63, 0x69, 0x12, 0x44, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x1a, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x4d,
|
||||
0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x48, 0x00, 0x52, 0x0c, 0x75,
|
||||
0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x48, 0x61, 0x73, 0x68, 0x88, 0x01, 0x01, 0x12, 0x33,
|
||||
0x0a, 0x04, 0x65, 0x31, 0x36, 0x34, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74,
|
||||
0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x4d, 0x6f, 0x6e, 0x69,
|
||||
0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x48, 0x01, 0x52, 0x04, 0x65, 0x31, 0x36, 0x34,
|
||||
0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x18, 0x05, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x09, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x68,
|
||||
0x61, 0x73, 0x68, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x65, 0x31, 0x36, 0x34, 0x32, 0xf2, 0x01, 0x0a,
|
||||
0x1b, 0x4b, 0x65, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79,
|
||||
0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x0d,
|
||||
0x44, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x75, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x1e, 0x2e,
|
||||
0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x44, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67,
|
||||
0x75, 0x69, 0x73, 0x68, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e,
|
||||
0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x44, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67,
|
||||
0x75, 0x69, 0x73, 0x68, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
|
||||
0x12, 0x3d, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x17, 0x2e, 0x6b, 0x74, 0x5f,
|
||||
0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x53,
|
||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
|
||||
0x40, 0x0a, 0x07, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, 0x18, 0x2e, 0x6b, 0x74, 0x5f,
|
||||
0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6b, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e,
|
||||
0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x00, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
|
||||
0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x61, 0x70, 0x70, 0x2f, 0x6b, 0x65, 0x79, 0x74, 0x72, 0x61,
|
||||
0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x6b, 0x74,
|
||||
0x2d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_key_transparency_query_proto_rawDescOnce sync.Once
|
||||
file_key_transparency_query_proto_rawDescData = file_key_transparency_query_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_key_transparency_query_proto_rawDescGZIP() []byte {
|
||||
file_key_transparency_query_proto_rawDescOnce.Do(func() {
|
||||
file_key_transparency_query_proto_rawDescData = protoimpl.X.CompressGZIP(file_key_transparency_query_proto_rawDescData)
|
||||
})
|
||||
return file_key_transparency_query_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_key_transparency_query_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_key_transparency_query_proto_goTypes = []any{
|
||||
(*DistinguishedRequest)(nil), // 0: kt_query.DistinguishedRequest
|
||||
(*DistinguishedResponse)(nil), // 1: kt_query.DistinguishedResponse
|
||||
(*SearchRequest)(nil), // 2: kt_query.SearchRequest
|
||||
(*E164SearchRequest)(nil), // 3: kt_query.E164SearchRequest
|
||||
(*CondensedTreeSearchResponse)(nil), // 4: kt_query.CondensedTreeSearchResponse
|
||||
(*SearchResponse)(nil), // 5: kt_query.SearchResponse
|
||||
(*MonitorRequest)(nil), // 6: kt_query.MonitorRequest
|
||||
(*AciMonitorRequest)(nil), // 7: kt_query.AciMonitorRequest
|
||||
(*UsernameHashMonitorRequest)(nil), // 8: kt_query.UsernameHashMonitorRequest
|
||||
(*E164MonitorRequest)(nil), // 9: kt_query.E164MonitorRequest
|
||||
(*MonitorResponse)(nil), // 10: kt_query.MonitorResponse
|
||||
(*pb.FullTreeHead)(nil), // 11: transparency.FullTreeHead
|
||||
(*pb.Consistency)(nil), // 12: transparency.Consistency
|
||||
(*pb.SearchProof)(nil), // 13: transparency.SearchProof
|
||||
(*pb.UpdateValue)(nil), // 14: transparency.UpdateValue
|
||||
(*pb.MonitorProof)(nil), // 15: transparency.MonitorProof
|
||||
}
|
||||
var file_key_transparency_query_proto_depIdxs = []int32{
|
||||
11, // 0: kt_query.DistinguishedResponse.tree_head:type_name -> transparency.FullTreeHead
|
||||
4, // 1: kt_query.DistinguishedResponse.distinguished:type_name -> kt_query.CondensedTreeSearchResponse
|
||||
3, // 2: kt_query.SearchRequest.e164_search_request:type_name -> kt_query.E164SearchRequest
|
||||
12, // 3: kt_query.SearchRequest.consistency:type_name -> transparency.Consistency
|
||||
13, // 4: kt_query.CondensedTreeSearchResponse.search:type_name -> transparency.SearchProof
|
||||
14, // 5: kt_query.CondensedTreeSearchResponse.value:type_name -> transparency.UpdateValue
|
||||
11, // 6: kt_query.SearchResponse.tree_head:type_name -> transparency.FullTreeHead
|
||||
4, // 7: kt_query.SearchResponse.aci:type_name -> kt_query.CondensedTreeSearchResponse
|
||||
4, // 8: kt_query.SearchResponse.e164:type_name -> kt_query.CondensedTreeSearchResponse
|
||||
4, // 9: kt_query.SearchResponse.username_hash:type_name -> kt_query.CondensedTreeSearchResponse
|
||||
7, // 10: kt_query.MonitorRequest.aci:type_name -> kt_query.AciMonitorRequest
|
||||
8, // 11: kt_query.MonitorRequest.username_hash:type_name -> kt_query.UsernameHashMonitorRequest
|
||||
9, // 12: kt_query.MonitorRequest.e164:type_name -> kt_query.E164MonitorRequest
|
||||
12, // 13: kt_query.MonitorRequest.consistency:type_name -> transparency.Consistency
|
||||
11, // 14: kt_query.MonitorResponse.tree_head:type_name -> transparency.FullTreeHead
|
||||
15, // 15: kt_query.MonitorResponse.aci:type_name -> transparency.MonitorProof
|
||||
15, // 16: kt_query.MonitorResponse.username_hash:type_name -> transparency.MonitorProof
|
||||
15, // 17: kt_query.MonitorResponse.e164:type_name -> transparency.MonitorProof
|
||||
0, // 18: kt_query.KeyTransparencyQueryService.Distinguished:input_type -> kt_query.DistinguishedRequest
|
||||
2, // 19: kt_query.KeyTransparencyQueryService.Search:input_type -> kt_query.SearchRequest
|
||||
6, // 20: kt_query.KeyTransparencyQueryService.Monitor:input_type -> kt_query.MonitorRequest
|
||||
1, // 21: kt_query.KeyTransparencyQueryService.Distinguished:output_type -> kt_query.DistinguishedResponse
|
||||
5, // 22: kt_query.KeyTransparencyQueryService.Search:output_type -> kt_query.SearchResponse
|
||||
10, // 23: kt_query.KeyTransparencyQueryService.Monitor:output_type -> kt_query.MonitorResponse
|
||||
21, // [21:24] is the sub-list for method output_type
|
||||
18, // [18:21] is the sub-list for method input_type
|
||||
18, // [18:18] is the sub-list for extension type_name
|
||||
18, // [18:18] is the sub-list for extension extendee
|
||||
0, // [0:18] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_key_transparency_query_proto_init() }
|
||||
func file_key_transparency_query_proto_init() {
|
||||
if File_key_transparency_query_proto != nil {
|
||||
return
|
||||
}
|
||||
file_key_transparency_query_proto_msgTypes[0].OneofWrappers = []any{}
|
||||
file_key_transparency_query_proto_msgTypes[2].OneofWrappers = []any{}
|
||||
file_key_transparency_query_proto_msgTypes[3].OneofWrappers = []any{}
|
||||
file_key_transparency_query_proto_msgTypes[5].OneofWrappers = []any{}
|
||||
file_key_transparency_query_proto_msgTypes[6].OneofWrappers = []any{}
|
||||
file_key_transparency_query_proto_msgTypes[9].OneofWrappers = []any{}
|
||||
file_key_transparency_query_proto_msgTypes[10].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_key_transparency_query_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 11,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_key_transparency_query_proto_goTypes,
|
||||
DependencyIndexes: file_key_transparency_query_proto_depIdxs,
|
||||
MessageInfos: file_key_transparency_query_proto_msgTypes,
|
||||
}.Build()
|
||||
File_key_transparency_query_proto = out.File
|
||||
file_key_transparency_query_proto_rawDesc = nil
|
||||
file_key_transparency_query_proto_goTypes = nil
|
||||
file_key_transparency_query_proto_depIdxs = nil
|
||||
}
|
||||
108
cmd/kt-server/pb/key_transparency_query.proto
Normal file
108
cmd/kt-server/pb/key_transparency_query.proto
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
package kt_query;
|
||||
|
||||
option go_package = "github.com/signalapp/keytransparency/cmd/kt-query/pb";
|
||||
import "transparency.proto";
|
||||
|
||||
// An external-facing, read-only key transparency service used by Signal's chat server
|
||||
// to look up and monitor identifiers.
|
||||
// There are three types of identifier mappings stored by the key transparency log:
|
||||
// - An ACI which maps to an ACI identity key
|
||||
// - An E164-formatted phone number which maps to an ACI
|
||||
// - A username hash which also maps to an ACI
|
||||
// Separately, the log also stores and periodically updates a fixed value known as the `distinguished` key.
|
||||
// Clients use the verified tree head from looking up this key for future calls to the Search and Monitor endpoints.
|
||||
service KeyTransparencyQueryService {
|
||||
// An endpoint used by clients to look up the most recent `distinguished` key.
|
||||
rpc Distinguished(DistinguishedRequest) returns (DistinguishedResponse) {}
|
||||
// An endpoint used by clients to search for the given identifiers in the transparency log.
|
||||
// The server returns proof that the requested identifiers exist in the log.
|
||||
rpc Search(SearchRequest) returns (SearchResponse) {}
|
||||
// An endpoint that allows users to monitor a group of identifiers by returning proof that the log continues to be
|
||||
// constructed correctly in later entries for those identifiers.
|
||||
rpc Monitor(MonitorRequest) returns (MonitorResponse) {}
|
||||
}
|
||||
|
||||
// DistinguishedRequest looks up the most recent `distinguished` key in the transparency log.
|
||||
message DistinguishedRequest {
|
||||
// The tree size of the last verified distinguished request. With the exception of a client's
|
||||
// very first request, this field should always be populated.
|
||||
optional uint64 last = 1;
|
||||
}
|
||||
|
||||
// DistinguishedResponse contains the tree head and search proof for the most recent `distinguished` key in the log.
|
||||
message DistinguishedResponse {
|
||||
transparency.FullTreeHead tree_head = 1;
|
||||
CondensedTreeSearchResponse distinguished = 2;
|
||||
}
|
||||
|
||||
// SearchRequest comes from a user that wishes to look up one or more identifiers in the transparency log.
|
||||
message SearchRequest {
|
||||
bytes aci = 1;
|
||||
bytes aci_identity_key = 2;
|
||||
optional bytes username_hash = 3;
|
||||
optional E164SearchRequest e164_search_request = 4;
|
||||
transparency.Consistency consistency = 5;
|
||||
}
|
||||
|
||||
// E164SearchRequest contains the data that the user must provide when looking up an E164
|
||||
message E164SearchRequest {
|
||||
optional string e164 = 1;
|
||||
bytes unidentified_access_key = 2;
|
||||
}
|
||||
|
||||
// CondensedTreeSearchResponse contains the search proof and other data for a given identifier.
|
||||
// It is used in the SearchResponse protobuf which is returned to external clients.
|
||||
message CondensedTreeSearchResponse {
|
||||
bytes vrf_proof = 1;
|
||||
transparency.SearchProof search = 2;
|
||||
bytes opening = 3;
|
||||
transparency.UpdateValue value = 4;
|
||||
}
|
||||
|
||||
// SearchResponse contains search proofs for each of the requested identifiers.
|
||||
message SearchResponse {
|
||||
transparency.FullTreeHead tree_head = 1;
|
||||
CondensedTreeSearchResponse aci = 2;
|
||||
optional CondensedTreeSearchResponse e164 = 3;
|
||||
optional CondensedTreeSearchResponse username_hash = 4;
|
||||
}
|
||||
|
||||
message MonitorRequest {
|
||||
AciMonitorRequest aci = 1;
|
||||
optional UsernameHashMonitorRequest username_hash = 2;
|
||||
optional E164MonitorRequest e164 = 3;
|
||||
|
||||
transparency.Consistency consistency = 4;
|
||||
}
|
||||
|
||||
message AciMonitorRequest {
|
||||
bytes aci = 1;
|
||||
uint64 entry_position = 2;
|
||||
bytes commitment_index = 3;
|
||||
}
|
||||
|
||||
message UsernameHashMonitorRequest {
|
||||
bytes username_hash = 1;
|
||||
uint64 entry_position = 2;
|
||||
bytes commitment_index = 3;
|
||||
}
|
||||
|
||||
message E164MonitorRequest {
|
||||
optional string e164 = 1;
|
||||
uint64 entry_position = 2;
|
||||
bytes commitment_index = 3;
|
||||
}
|
||||
|
||||
message MonitorResponse {
|
||||
transparency.FullTreeHead tree_head = 1;
|
||||
transparency.MonitorProof aci = 2;
|
||||
optional transparency.MonitorProof username_hash = 3;
|
||||
optional transparency.MonitorProof e164 = 4;
|
||||
repeated bytes inclusion = 5;
|
||||
}
|
||||
231
cmd/kt-server/pb/key_transparency_query_grpc.pb.go
Normal file
231
cmd/kt-server/pb/key_transparency_query_grpc.pb.go
Normal file
@ -0,0 +1,231 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.28.3
|
||||
// source: key_transparency_query.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
KeyTransparencyQueryService_Distinguished_FullMethodName = "/kt_query.KeyTransparencyQueryService/Distinguished"
|
||||
KeyTransparencyQueryService_Search_FullMethodName = "/kt_query.KeyTransparencyQueryService/Search"
|
||||
KeyTransparencyQueryService_Monitor_FullMethodName = "/kt_query.KeyTransparencyQueryService/Monitor"
|
||||
)
|
||||
|
||||
// KeyTransparencyQueryServiceClient is the client API for KeyTransparencyQueryService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// An external-facing, read-only key transparency service used by Signal's chat server
|
||||
// to look up and monitor identifiers.
|
||||
// There are three types of identifier mappings stored by the key transparency log:
|
||||
// - An ACI which maps to an ACI identity key
|
||||
// - An E164-formatted phone number which maps to an ACI
|
||||
// - A username hash which also maps to an ACI
|
||||
// Separately, the log also stores and periodically updates a fixed value known as the `distinguished` key.
|
||||
// Clients use the verified tree head from looking up this key for future calls to the Search and Monitor endpoints.
|
||||
type KeyTransparencyQueryServiceClient interface {
|
||||
// An endpoint used by clients to look up the most recent `distinguished` key.
|
||||
Distinguished(ctx context.Context, in *DistinguishedRequest, opts ...grpc.CallOption) (*DistinguishedResponse, error)
|
||||
// An endpoint used by clients to search for the given identifiers in the transparency log.
|
||||
// The server returns proof that the requested identifiers exist in the log.
|
||||
Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error)
|
||||
// An endpoint that allows users to monitor a group of identifiers by returning proof that the log continues to be
|
||||
// constructed correctly in later entries for those identifiers.
|
||||
Monitor(ctx context.Context, in *MonitorRequest, opts ...grpc.CallOption) (*MonitorResponse, error)
|
||||
}
|
||||
|
||||
type keyTransparencyQueryServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewKeyTransparencyQueryServiceClient(cc grpc.ClientConnInterface) KeyTransparencyQueryServiceClient {
|
||||
return &keyTransparencyQueryServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *keyTransparencyQueryServiceClient) Distinguished(ctx context.Context, in *DistinguishedRequest, opts ...grpc.CallOption) (*DistinguishedResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DistinguishedResponse)
|
||||
err := c.cc.Invoke(ctx, KeyTransparencyQueryService_Distinguished_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *keyTransparencyQueryServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SearchResponse)
|
||||
err := c.cc.Invoke(ctx, KeyTransparencyQueryService_Search_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *keyTransparencyQueryServiceClient) Monitor(ctx context.Context, in *MonitorRequest, opts ...grpc.CallOption) (*MonitorResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(MonitorResponse)
|
||||
err := c.cc.Invoke(ctx, KeyTransparencyQueryService_Monitor_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// KeyTransparencyQueryServiceServer is the server API for KeyTransparencyQueryService service.
|
||||
// All implementations must embed UnimplementedKeyTransparencyQueryServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// An external-facing, read-only key transparency service used by Signal's chat server
|
||||
// to look up and monitor identifiers.
|
||||
// There are three types of identifier mappings stored by the key transparency log:
|
||||
// - An ACI which maps to an ACI identity key
|
||||
// - An E164-formatted phone number which maps to an ACI
|
||||
// - A username hash which also maps to an ACI
|
||||
// Separately, the log also stores and periodically updates a fixed value known as the `distinguished` key.
|
||||
// Clients use the verified tree head from looking up this key for future calls to the Search and Monitor endpoints.
|
||||
type KeyTransparencyQueryServiceServer interface {
|
||||
// An endpoint used by clients to look up the most recent `distinguished` key.
|
||||
Distinguished(context.Context, *DistinguishedRequest) (*DistinguishedResponse, error)
|
||||
// An endpoint used by clients to search for the given identifiers in the transparency log.
|
||||
// The server returns proof that the requested identifiers exist in the log.
|
||||
Search(context.Context, *SearchRequest) (*SearchResponse, error)
|
||||
// An endpoint that allows users to monitor a group of identifiers by returning proof that the log continues to be
|
||||
// constructed correctly in later entries for those identifiers.
|
||||
Monitor(context.Context, *MonitorRequest) (*MonitorResponse, error)
|
||||
mustEmbedUnimplementedKeyTransparencyQueryServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedKeyTransparencyQueryServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedKeyTransparencyQueryServiceServer struct{}
|
||||
|
||||
func (UnimplementedKeyTransparencyQueryServiceServer) Distinguished(context.Context, *DistinguishedRequest) (*DistinguishedResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Distinguished not implemented")
|
||||
}
|
||||
func (UnimplementedKeyTransparencyQueryServiceServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Search not implemented")
|
||||
}
|
||||
func (UnimplementedKeyTransparencyQueryServiceServer) Monitor(context.Context, *MonitorRequest) (*MonitorResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Monitor not implemented")
|
||||
}
|
||||
func (UnimplementedKeyTransparencyQueryServiceServer) mustEmbedUnimplementedKeyTransparencyQueryServiceServer() {
|
||||
}
|
||||
func (UnimplementedKeyTransparencyQueryServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeKeyTransparencyQueryServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to KeyTransparencyQueryServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeKeyTransparencyQueryServiceServer interface {
|
||||
mustEmbedUnimplementedKeyTransparencyQueryServiceServer()
|
||||
}
|
||||
|
||||
func RegisterKeyTransparencyQueryServiceServer(s grpc.ServiceRegistrar, srv KeyTransparencyQueryServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedKeyTransparencyQueryServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&KeyTransparencyQueryService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _KeyTransparencyQueryService_Distinguished_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DistinguishedRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KeyTransparencyQueryServiceServer).Distinguished(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: KeyTransparencyQueryService_Distinguished_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KeyTransparencyQueryServiceServer).Distinguished(ctx, req.(*DistinguishedRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _KeyTransparencyQueryService_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SearchRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KeyTransparencyQueryServiceServer).Search(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: KeyTransparencyQueryService_Search_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KeyTransparencyQueryServiceServer).Search(ctx, req.(*SearchRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _KeyTransparencyQueryService_Monitor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(MonitorRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KeyTransparencyQueryServiceServer).Monitor(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: KeyTransparencyQueryService_Monitor_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KeyTransparencyQueryServiceServer).Monitor(ctx, req.(*MonitorRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// KeyTransparencyQueryService_ServiceDesc is the grpc.ServiceDesc for KeyTransparencyQueryService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var KeyTransparencyQueryService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "kt_query.KeyTransparencyQueryService",
|
||||
HandlerType: (*KeyTransparencyQueryServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Distinguished",
|
||||
Handler: _KeyTransparencyQueryService_Distinguished_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Search",
|
||||
Handler: _KeyTransparencyQueryService_Search_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Monitor",
|
||||
Handler: _KeyTransparencyQueryService_Monitor_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "key_transparency_query.proto",
|
||||
}
|
||||
83
cmd/kt-server/pb/key_transparency_test.pb.go
Normal file
83
cmd/kt-server/pb/key_transparency_test.pb.go
Normal file
@ -0,0 +1,83 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.2
|
||||
// protoc v5.28.3
|
||||
// source: key_transparency_test.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
pb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
var File_key_transparency_test_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_key_transparency_test_proto_rawDesc = []byte{
|
||||
0x0a, 0x1b, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e,
|
||||
0x63, 0x79, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6b,
|
||||
0x74, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72,
|
||||
0x65, 0x6e, 0x63, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0x63, 0x0a, 0x1a, 0x4b, 0x65,
|
||||
0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x54, 0x65, 0x73,
|
||||
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61,
|
||||
0x74, 0x65, 0x12, 0x1b, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63,
|
||||
0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x1c, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x55,
|
||||
0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
|
||||
0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69,
|
||||
0x67, 0x6e, 0x61, 0x6c, 0x61, 0x70, 0x70, 0x2f, 0x6b, 0x65, 0x79, 0x74, 0x72, 0x61, 0x6e, 0x73,
|
||||
0x70, 0x61, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x6b, 0x74, 0x2d, 0x74,
|
||||
0x65, 0x73, 0x74, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var file_key_transparency_test_proto_goTypes = []any{
|
||||
(*pb.UpdateRequest)(nil), // 0: transparency.UpdateRequest
|
||||
(*pb.UpdateResponse)(nil), // 1: transparency.UpdateResponse
|
||||
}
|
||||
var file_key_transparency_test_proto_depIdxs = []int32{
|
||||
0, // 0: kt_test.KeyTransparencyTestService.Update:input_type -> transparency.UpdateRequest
|
||||
1, // 1: kt_test.KeyTransparencyTestService.Update:output_type -> transparency.UpdateResponse
|
||||
1, // [1:2] is the sub-list for method output_type
|
||||
0, // [0:1] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_key_transparency_test_proto_init() }
|
||||
func file_key_transparency_test_proto_init() {
|
||||
if File_key_transparency_test_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_key_transparency_test_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 0,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_key_transparency_test_proto_goTypes,
|
||||
DependencyIndexes: file_key_transparency_test_proto_depIdxs,
|
||||
}.Build()
|
||||
File_key_transparency_test_proto = out.File
|
||||
file_key_transparency_test_proto_rawDesc = nil
|
||||
file_key_transparency_test_proto_goTypes = nil
|
||||
file_key_transparency_test_proto_depIdxs = nil
|
||||
}
|
||||
16
cmd/kt-server/pb/key_transparency_test.proto
Normal file
16
cmd/kt-server/pb/key_transparency_test.proto
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
package kt_test;
|
||||
|
||||
option go_package = "github.com/signalapp/keytransparency/cmd/kt-test/pb";
|
||||
import "transparency.proto";
|
||||
|
||||
// A key transparency service intended for local testing and development purposes only.
|
||||
service KeyTransparencyTestService {
|
||||
// An endpoint used by local clients to update a search key.
|
||||
rpc Update(transparency.UpdateRequest) returns (transparency.UpdateResponse) {}
|
||||
}
|
||||
134
cmd/kt-server/pb/key_transparency_test_grpc.pb.go
Normal file
134
cmd/kt-server/pb/key_transparency_test_grpc.pb.go
Normal file
@ -0,0 +1,134 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.28.3
|
||||
// source: key_transparency_test.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
context "context"
|
||||
pb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
KeyTransparencyTestService_Update_FullMethodName = "/kt_test.KeyTransparencyTestService/Update"
|
||||
)
|
||||
|
||||
// KeyTransparencyTestServiceClient is the client API for KeyTransparencyTestService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// A key transparency service intended for local testing and development purposes only.
|
||||
type KeyTransparencyTestServiceClient interface {
|
||||
// An endpoint used by local clients to update a search key.
|
||||
Update(ctx context.Context, in *pb.UpdateRequest, opts ...grpc.CallOption) (*pb.UpdateResponse, error)
|
||||
}
|
||||
|
||||
type keyTransparencyTestServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewKeyTransparencyTestServiceClient(cc grpc.ClientConnInterface) KeyTransparencyTestServiceClient {
|
||||
return &keyTransparencyTestServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *keyTransparencyTestServiceClient) Update(ctx context.Context, in *pb.UpdateRequest, opts ...grpc.CallOption) (*pb.UpdateResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(pb.UpdateResponse)
|
||||
err := c.cc.Invoke(ctx, KeyTransparencyTestService_Update_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// KeyTransparencyTestServiceServer is the server API for KeyTransparencyTestService service.
|
||||
// All implementations must embed UnimplementedKeyTransparencyTestServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// A key transparency service intended for local testing and development purposes only.
|
||||
type KeyTransparencyTestServiceServer interface {
|
||||
// An endpoint used by local clients to update a search key.
|
||||
Update(context.Context, *pb.UpdateRequest) (*pb.UpdateResponse, error)
|
||||
mustEmbedUnimplementedKeyTransparencyTestServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedKeyTransparencyTestServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedKeyTransparencyTestServiceServer struct{}
|
||||
|
||||
func (UnimplementedKeyTransparencyTestServiceServer) Update(context.Context, *pb.UpdateRequest) (*pb.UpdateResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Update not implemented")
|
||||
}
|
||||
func (UnimplementedKeyTransparencyTestServiceServer) mustEmbedUnimplementedKeyTransparencyTestServiceServer() {
|
||||
}
|
||||
func (UnimplementedKeyTransparencyTestServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeKeyTransparencyTestServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to KeyTransparencyTestServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeKeyTransparencyTestServiceServer interface {
|
||||
mustEmbedUnimplementedKeyTransparencyTestServiceServer()
|
||||
}
|
||||
|
||||
func RegisterKeyTransparencyTestServiceServer(s grpc.ServiceRegistrar, srv KeyTransparencyTestServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedKeyTransparencyTestServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&KeyTransparencyTestService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _KeyTransparencyTestService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(pb.UpdateRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KeyTransparencyTestServiceServer).Update(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: KeyTransparencyTestService_Update_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KeyTransparencyTestServiceServer).Update(ctx, req.(*pb.UpdateRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// KeyTransparencyTestService_ServiceDesc is the grpc.ServiceDesc for KeyTransparencyTestService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var KeyTransparencyTestService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "kt_test.KeyTransparencyTestService",
|
||||
HandlerType: (*KeyTransparencyTestServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Update",
|
||||
Handler: _KeyTransparencyTestService_Update_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "key_transparency_test.proto",
|
||||
}
|
||||
440
cmd/kt-server/stream.go
Normal file
440
cmd/kt-server/stream.go
Normal file
@ -0,0 +1,440 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/retry"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
kinesistypes "github.com/aws/aws-sdk-go-v2/service/kinesis/types"
|
||||
consumer "github.com/harlow/kinesis-consumer"
|
||||
metrics "github.com/hashicorp/go-metrics"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
"github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
const (
|
||||
backfillScanShards = 1000
|
||||
backfillWorkers = 96
|
||||
withinBackfill = "backfill"
|
||||
withinStream = "stream"
|
||||
tombstoneString = "tombstone"
|
||||
)
|
||||
|
||||
var tombstoneBytes = marshalValue([]byte(tombstoneString))
|
||||
var logUpdater = &LogUpdater{}
|
||||
|
||||
// metricsCounter implements the consumer.Counter interface for exporting
|
||||
// Kinesis metrics.
|
||||
type metricsCounter struct{}
|
||||
|
||||
func (pc metricsCounter) Add(name string, val int64) {
|
||||
metrics.IncrCounterWithLabels([]string{withinStream, "kinesis"}, float32(val), []metrics.Label{{Name: "type", Value: name}})
|
||||
}
|
||||
|
||||
// kinesisLogger implements the consumer.Logger interface for printing Kinesis
|
||||
// logs to stdout.
|
||||
type kinesisLogger struct{}
|
||||
|
||||
func (kl kinesisLogger) Log(v ...any) { util.Log().Infof("%s", fmt.Sprintln(v...)) }
|
||||
|
||||
// shardState is used to keep track of the scan state of each shard.
|
||||
type shardState struct {
|
||||
// sinceLast is the number of entries from this shard that have been
|
||||
// sequenced since its last checkpoint.
|
||||
sinceLast int
|
||||
// wg is a WaitGroup that all goroutines sequencing entries from this shard
|
||||
// are given.
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
type Streamer struct {
|
||||
config *config.APIConfig
|
||||
tx db.TransparencyStore
|
||||
}
|
||||
|
||||
// run runs the streamer, blocking forever.
|
||||
func (s *Streamer) run(ctx context.Context, name string, startAtTimestamp time.Time, updateHandler *KtUpdateHandler) {
|
||||
i := 0
|
||||
for {
|
||||
var updatesWg sync.WaitGroup
|
||||
var shardsMu sync.Mutex
|
||||
|
||||
// Reset the shards map on consumer restart.
|
||||
shards := make(map[string]*shardState)
|
||||
|
||||
// Create a new context for each run.
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
// Note on thread safety: The Kinesis consumer library will use one
|
||||
// goroutine per shard to scan. As such, a mutex is required to lookup shard
|
||||
// state from the `shards` map because many shards may be read/written to
|
||||
// the map in parallel. But the returned shardState struct can then used
|
||||
// without a mutex because there is only one goroutine working with it.
|
||||
|
||||
c, err := consumer.New(
|
||||
name,
|
||||
consumer.WithLogger(kinesisLogger{}),
|
||||
consumer.WithCounter(metricsCounter{}),
|
||||
consumer.WithStore(s.tx.StreamStore()),
|
||||
consumer.WithShardIteratorType(string(kinesistypes.ShardIteratorTypeAtTimestamp)),
|
||||
consumer.WithTimestamp(startAtTimestamp),
|
||||
)
|
||||
if err != nil {
|
||||
util.Log().Errorf("stream consumer initialization error: %v", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
err = c.Scan(runCtx, func(r *consumer.Record) error {
|
||||
// Get the existing shardState struct or initialize a new one if needed.
|
||||
shardsMu.Lock()
|
||||
if _, ok := shards[r.ShardID]; !ok {
|
||||
shards[r.ShardID] = &shardState{0, &sync.WaitGroup{}}
|
||||
}
|
||||
state := shards[r.ShardID]
|
||||
shardsMu.Unlock()
|
||||
|
||||
// Start a dedicated goroutine to process this update. We don't call
|
||||
// wg.Done until the update is successfully executed, retrying
|
||||
// infinitely if necessary. This is required to prevent us from
|
||||
// checkpointing past an update that we might've failed to sequence.
|
||||
state.sinceLast += 1
|
||||
state.wg.Add(1)
|
||||
go func(ctx context.Context, data []byte, wg *sync.WaitGroup) {
|
||||
updatesWg.Add(1)
|
||||
defer updatesWg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
err := updateFromStream(ctx, data, updateHandler, logUpdater)
|
||||
if err != nil {
|
||||
util.Log().Infof("failed to update entry from stream: %v", err)
|
||||
metrics.IncrCounter([]string{withinStream, "errors"}, 1)
|
||||
time.Sleep(3 * time.Second)
|
||||
} else {
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}(runCtx, dup(r.Data), state.wg)
|
||||
|
||||
// If only a few entries have been sequenced from this shard, move on.
|
||||
if state.sinceLast < 100 {
|
||||
return consumer.ErrSkipCheckpoint
|
||||
}
|
||||
// If many entries have been sequenced, we need to checkpoint. First
|
||||
// wait for all processing updates to complete.
|
||||
state.wg.Wait()
|
||||
state.sinceLast = 0
|
||||
return nil
|
||||
})
|
||||
util.Log().Errorf("stream consumer error: %v", err)
|
||||
|
||||
// We only reach this point if c.Scan returns an error.
|
||||
// In this case, clean up the current context, sleep with an exponential backoff,
|
||||
// and wait for all spawned goroutines to exit.
|
||||
cancel()
|
||||
|
||||
// Cap the backoff at 60 seconds
|
||||
delay := time.Duration(math.Min(60, math.Pow(2, float64(i)))) * time.Second
|
||||
util.Log().Infof("iteration %d of stream consumer, sleeping %s", i, delay)
|
||||
time.Sleep(delay)
|
||||
|
||||
// Ensure that all update goroutines have exited before restarting the consumer
|
||||
updatesWg.Wait()
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func backfill(ctx context.Context, table string, updateHandler *KtUpdateHandler) error {
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRetryer(func() aws.Retryer {
|
||||
// Max attempts set to 0 indicates that the attempt should be retried until it succeeds
|
||||
// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/aws/retry#AdaptiveMode.MaxAttempts
|
||||
return retry.AddWithMaxAttempts(retry.NewAdaptiveMode(), 0)
|
||||
}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading aws sdk config: %w", err)
|
||||
}
|
||||
ddb := dynamodb.NewFromConfig(cfg)
|
||||
totalShards := int32(backfillScanShards)
|
||||
eg.SetLimit(backfillWorkers)
|
||||
|
||||
for shard := int32(0); shard < backfillScanShards; shard++ {
|
||||
shard := shard
|
||||
eg.Go(func() (returnedErr error) {
|
||||
util.Log().Infof("Starting processing of backfill shard %d", shard)
|
||||
defer func() {
|
||||
metrics.IncrCounter([]string{withinBackfill, "shards_processed"}, 1)
|
||||
util.Log().Infof("Finished processing of backfill shard %d: err=%v", shard, returnedErr)
|
||||
}()
|
||||
var exclusiveStartKey map[string]types.AttributeValue
|
||||
for i := 0; ; i++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := ddb.Scan(ctx, &dynamodb.ScanInput{
|
||||
TableName: &table,
|
||||
Segment: &shard,
|
||||
TotalSegments: &totalShards,
|
||||
ExclusiveStartKey: exclusiveStartKey,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("scan %d of shard %d failed: %w", i, shard, err)
|
||||
} else if err := backfillScanOutput(ctx, out, updateHandler, logUpdater); err != nil {
|
||||
return fmt.Errorf("scan %d of shard %d backfill processing failed: %w", i, shard, err)
|
||||
} else if exclusiveStartKey = out.LastEvaluatedKey; len(exclusiveStartKey) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func backfillScanOutput(ctx context.Context, scan *dynamodb.ScanOutput, updateHandler *KtUpdateHandler, updater Updater) error {
|
||||
for i, item := range scan.Items {
|
||||
if err := updateFromBackfill(ctx, item, updateHandler, updater); err != nil {
|
||||
return fmt.Errorf("processing item %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type accountPair struct {
|
||||
Prev *account `json:"prev"`
|
||||
Next *account `json:"next"`
|
||||
}
|
||||
|
||||
type account struct {
|
||||
ACI []byte `json:"aci"`
|
||||
ACIIdentityKey []byte `json:"aciIdentityKey"`
|
||||
|
||||
Number string `json:"number"`
|
||||
UsernameHash []byte `json:"usernameHash"`
|
||||
}
|
||||
|
||||
func updateFromBackfill(ctx context.Context, item map[string]types.AttributeValue, updateHandler *KtUpdateHandler, updater Updater) error {
|
||||
type backfillAccount struct {
|
||||
Number string `json:"number"`
|
||||
ACIIdentityKey []byte `json:"identityKey"`
|
||||
UsernameHash string `json:"usernameHash"` // URL-encoded base64
|
||||
}
|
||||
u := item["U"]
|
||||
if u == nil {
|
||||
return fmt.Errorf("no account ID")
|
||||
}
|
||||
ub, ok := u.(*types.AttributeValueMemberB)
|
||||
if !ok {
|
||||
return fmt.Errorf("account ID not bytes")
|
||||
} else if len(ub.Value) != 16 {
|
||||
return fmt.Errorf("account ID not valid")
|
||||
}
|
||||
accountID := ub.Value
|
||||
d := item["D"]
|
||||
if d == nil {
|
||||
return fmt.Errorf("account %x no data", accountID)
|
||||
}
|
||||
db, ok := d.(*types.AttributeValueMemberB)
|
||||
if !ok {
|
||||
return fmt.Errorf("account %x data not bytes", accountID)
|
||||
}
|
||||
var account backfillAccount
|
||||
if err := json.Unmarshal(db.Value, &account); err != nil {
|
||||
return fmt.Errorf("parsing account %x data: %w", accountID, err)
|
||||
} else if len(account.Number) == 0 {
|
||||
return fmt.Errorf("account %x data has empty number", accountID)
|
||||
}
|
||||
if len(account.ACIIdentityKey) > 0 {
|
||||
if err := updater.update(ctx, withinBackfill,
|
||||
append([]byte{util.AciPrefix}, accountID...),
|
||||
marshalValue(account.ACIIdentityKey), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating %x ACI: %w", accountID, err)
|
||||
}
|
||||
}
|
||||
if len(account.Number) > 0 {
|
||||
if err := updater.update(ctx, withinBackfill,
|
||||
append([]byte{util.NumberPrefix}, []byte(account.Number)...),
|
||||
marshalValue(accountID), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating %x Number: %w", accountID, err)
|
||||
}
|
||||
}
|
||||
if len(account.UsernameHash) > 0 {
|
||||
usernameHash, err := base64.RawURLEncoding.DecodeString(account.UsernameHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating %x username hash: failed to base64 decode hash: %w", accountID, err)
|
||||
}
|
||||
if err := updater.update(ctx, withinBackfill,
|
||||
append([]byte{util.UsernameHashPrefix}, usernameHash...),
|
||||
marshalValue(accountID), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating %x username hash: %w", accountID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateFromStream(ctx context.Context, data []byte, updateHandler *KtUpdateHandler, updater Updater) error {
|
||||
pair := &accountPair{}
|
||||
if err := json.Unmarshal(data, pair); err != nil {
|
||||
// Note: This is not a temporary error and will intentionally cause the
|
||||
// scanner to get stuck until new code is deployed that can handle
|
||||
// whatever is in the stream.
|
||||
return fmt.Errorf("unmarshaling from stream: %w", err)
|
||||
} else if pair.Prev == nil && pair.Next == nil {
|
||||
// This should never happen, but we want to know about it if it does
|
||||
metrics.IncrCounter([]string{"stream_empty_pair"}, 1)
|
||||
return nil
|
||||
} else if pair.Prev == nil {
|
||||
// New registration. ACI and number should always be present on these updates.
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.AciPrefix}, pair.Next.ACI...),
|
||||
marshalValue(pair.Next.ACIIdentityKey), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating ACI: %w", err)
|
||||
}
|
||||
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.NumberPrefix}, []byte(pair.Next.Number)...),
|
||||
marshalValue(pair.Next.ACI), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating number: %w", err)
|
||||
}
|
||||
|
||||
if len(pair.Next.UsernameHash) > 0 {
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.UsernameHashPrefix}, pair.Next.UsernameHash...),
|
||||
marshalValue(pair.Next.ACI), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating username hash: %w", err)
|
||||
}
|
||||
}
|
||||
} else if pair.Next == nil {
|
||||
// Account deletion. Overwrite all associated mappings to point to a tombstone value.
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.AciPrefix}, pair.Prev.ACI...),
|
||||
tombstoneBytes, updateHandler, marshalValue(pair.Prev.ACIIdentityKey)); err != nil {
|
||||
return fmt.Errorf("updating ACI: %w", err)
|
||||
}
|
||||
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.NumberPrefix}, []byte(pair.Prev.Number)...),
|
||||
tombstoneBytes, updateHandler, marshalValue(pair.Prev.ACI)); err != nil {
|
||||
return fmt.Errorf("updating number: %w", err)
|
||||
}
|
||||
|
||||
if len(pair.Prev.UsernameHash) > 0 {
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.UsernameHashPrefix}, pair.Prev.UsernameHash...),
|
||||
tombstoneBytes, updateHandler, marshalValue(pair.Prev.ACI)); err != nil {
|
||||
return fmt.Errorf("updating username hash: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !bytes.Equal(pair.Prev.ACIIdentityKey, pair.Next.ACIIdentityKey) {
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.AciPrefix}, pair.Next.ACI...),
|
||||
marshalValue(pair.Next.ACIIdentityKey), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating ACI: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !bytes.Equal(pair.Prev.UsernameHash, pair.Next.UsernameHash) {
|
||||
if len(pair.Prev.UsernameHash) > 0 {
|
||||
// Tombstone the old username hash
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.UsernameHashPrefix}, pair.Prev.UsernameHash...),
|
||||
tombstoneBytes, updateHandler, marshalValue(pair.Prev.ACI)); err != nil {
|
||||
return fmt.Errorf("updating username hash: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(pair.Next.UsernameHash) > 0 {
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.UsernameHashPrefix}, pair.Next.UsernameHash...),
|
||||
marshalValue(pair.Next.ACI), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating username hash: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pair.Prev.Number != pair.Next.Number {
|
||||
if len(pair.Prev.Number) > 0 {
|
||||
// Tombstone the old phone number
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.NumberPrefix}, pair.Prev.Number...),
|
||||
tombstoneBytes, updateHandler, marshalValue(pair.Prev.ACI)); err != nil {
|
||||
return fmt.Errorf("updating number: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(pair.Next.Number) > 0 {
|
||||
if err := updater.update(ctx, withinStream,
|
||||
append([]byte{util.NumberPrefix}, []byte(pair.Next.Number)...),
|
||||
marshalValue(pair.Next.ACI), updateHandler, nil); err != nil {
|
||||
return fmt.Errorf("updating number: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Updater interface supports mocking in tests
|
||||
type Updater interface {
|
||||
update(ctx context.Context, within string, key, value []byte, handler *KtUpdateHandler, expectedPreUpdateValue []byte) error
|
||||
}
|
||||
|
||||
type LogUpdater struct{}
|
||||
|
||||
func (s *LogUpdater) update(ctx context.Context, within string, key, value []byte, updateHandler *KtUpdateHandler, expectedPreUpdateValue []byte) (returnedErr error) {
|
||||
defer func() {
|
||||
success := returnedErr == nil
|
||||
metrics.IncrCounterWithLabels([]string{within, "items_processed"}, 1, []metrics.Label{{Name: "success", Value: strconv.FormatBool(success)}})
|
||||
}()
|
||||
updateReq := &pb.UpdateRequest{
|
||||
SearchKey: key,
|
||||
Value: value,
|
||||
Consistency: &pb.Consistency{}}
|
||||
|
||||
if expectedPreUpdateValue != nil {
|
||||
updateReq.ExpectedPreUpdateValue = expectedPreUpdateValue
|
||||
}
|
||||
|
||||
_, err := updateHandler.update(ctx, updateReq, 30*time.Minute)
|
||||
return err
|
||||
}
|
||||
|
||||
func marshalValue(bytes []byte) []byte {
|
||||
// It's not clear to me if we'll want to store more information in the log
|
||||
// later. For now, prefix with a 0 as a format version identifier.
|
||||
return append([]byte{0}, bytes...)
|
||||
}
|
||||
|
||||
func dup(in []byte) []byte {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]byte, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
}
|
||||
158
cmd/kt-server/stream_test.go
Normal file
158
cmd/kt-server/stream_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
)
|
||||
|
||||
var (
|
||||
validAciIdentityKey2 = createDistinctValue(validAciIdentityKey1)
|
||||
validUsernameHash2 = createDistinctValue(validUsernameHash1)
|
||||
validPhoneNumber2 = "+14155550102"
|
||||
)
|
||||
|
||||
type mockLogUpdater struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockLogUpdater) update(ctx context.Context, within string, key, value []byte, updateHandler *KtUpdateHandler, expectedPreUpdateValue []byte) (returnedErr error) {
|
||||
m.Called(ctx, within, key, value, updateHandler, expectedPreUpdateValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
type expectedUpdateInputs struct {
|
||||
key []byte
|
||||
value []byte
|
||||
preUpdateValue []byte
|
||||
}
|
||||
|
||||
var testUpdateAccountPairs = []struct {
|
||||
prev *account
|
||||
next *account
|
||||
expectedNumUpdates int
|
||||
expectedUpdateInputs []expectedUpdateInputs
|
||||
}{
|
||||
// No change
|
||||
{
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
0,
|
||||
[]expectedUpdateInputs{},
|
||||
},
|
||||
// New registration
|
||||
{
|
||||
nil,
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
3,
|
||||
[]expectedUpdateInputs{
|
||||
{key: append([]byte{util.AciPrefix}, validAci1...), value: marshalValue(validAciIdentityKey1), preUpdateValue: nil},
|
||||
{key: append([]byte{util.NumberPrefix}, []byte(validPhoneNumber1)...), value: marshalValue(validAci1), preUpdateValue: nil},
|
||||
{key: append([]byte{util.UsernameHashPrefix}, validUsernameHash1...), value: marshalValue(validAci1), preUpdateValue: nil}},
|
||||
},
|
||||
// Re-registration - the server sets the old username to null but keeps it reserved for the client to reclaim
|
||||
{
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey2, Number: validPhoneNumber1},
|
||||
2,
|
||||
[]expectedUpdateInputs{
|
||||
{key: append([]byte{util.AciPrefix}, validAci1...), value: marshalValue(validAciIdentityKey2), preUpdateValue: nil},
|
||||
{key: append([]byte{util.UsernameHashPrefix}, validUsernameHash1...), value: tombstoneBytes, preUpdateValue: marshalValue(validAci1)}},
|
||||
},
|
||||
// Re-registration - client reclaims username
|
||||
{
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey2, Number: validPhoneNumber1},
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey2, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
1,
|
||||
[]expectedUpdateInputs{
|
||||
{key: append([]byte{util.UsernameHashPrefix}, validUsernameHash1...), value: marshalValue(validAci1), preUpdateValue: nil}},
|
||||
},
|
||||
// Some re-registrations do not change the identity key
|
||||
{
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, Number: validPhoneNumber1},
|
||||
1,
|
||||
[]expectedUpdateInputs{
|
||||
{key: append([]byte{util.UsernameHashPrefix}, validUsernameHash1...), value: tombstoneBytes, preUpdateValue: marshalValue(validAci1)}},
|
||||
},
|
||||
// Account deletion with username
|
||||
{
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
nil,
|
||||
3,
|
||||
[]expectedUpdateInputs{
|
||||
{key: append([]byte{util.AciPrefix}, validAci1...), value: tombstoneBytes, preUpdateValue: marshalValue(validAciIdentityKey1)},
|
||||
{key: append([]byte{util.NumberPrefix}, []byte(validPhoneNumber1)...), value: tombstoneBytes, preUpdateValue: marshalValue(validAci1)},
|
||||
{key: append([]byte{util.UsernameHashPrefix}, validUsernameHash1...), value: tombstoneBytes, preUpdateValue: marshalValue(validAci1)}},
|
||||
},
|
||||
// Account deletion with no username
|
||||
{
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, Number: validPhoneNumber1},
|
||||
nil,
|
||||
2,
|
||||
[]expectedUpdateInputs{
|
||||
{key: append([]byte{util.AciPrefix}, validAci1...), value: tombstoneBytes, preUpdateValue: marshalValue(validAciIdentityKey1)},
|
||||
{key: append([]byte{util.NumberPrefix}, []byte(validPhoneNumber1)...), value: tombstoneBytes, preUpdateValue: marshalValue(validAci1)}},
|
||||
},
|
||||
// Username change
|
||||
{
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash2, Number: validPhoneNumber1},
|
||||
2,
|
||||
[]expectedUpdateInputs{
|
||||
{key: append([]byte{util.UsernameHashPrefix}, validUsernameHash2...), value: marshalValue(validAci1), preUpdateValue: nil},
|
||||
{key: append([]byte{util.UsernameHashPrefix}, validUsernameHash1...), value: tombstoneBytes, preUpdateValue: marshalValue(validAci1)}},
|
||||
},
|
||||
// Phone number change
|
||||
{
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber1},
|
||||
&account{ACI: validAci1, ACIIdentityKey: validAciIdentityKey1, UsernameHash: validUsernameHash1, Number: validPhoneNumber2},
|
||||
2,
|
||||
[]expectedUpdateInputs{
|
||||
{key: append([]byte{util.NumberPrefix}, validPhoneNumber2...), value: marshalValue(validAci1), preUpdateValue: nil},
|
||||
{key: append([]byte{util.NumberPrefix}, validPhoneNumber1...), value: tombstoneBytes, preUpdateValue: marshalValue(validAci1)}},
|
||||
},
|
||||
}
|
||||
|
||||
func TestUpdateFromStream(t *testing.T) {
|
||||
mockConfig, _ := config.Read(mockConfigFile)
|
||||
mockTransparencyStore := db.NewMemoryTransparencyStore()
|
||||
updateRequestChannel := make(chan updateRequest)
|
||||
mockUpdateHandler := &KtUpdateHandler{
|
||||
config: mockConfig.APIConfig,
|
||||
tx: mockTransparencyStore,
|
||||
ch: updateRequestChannel,
|
||||
}
|
||||
|
||||
for _, p := range testUpdateAccountPairs {
|
||||
mockUpdater := new(mockLogUpdater)
|
||||
|
||||
accounts := &accountPair{Prev: p.prev, Next: p.next}
|
||||
marshaledData, err := json.Marshal(accounts)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error marshaling acocunt pair")
|
||||
}
|
||||
|
||||
for _, pair := range p.expectedUpdateInputs {
|
||||
mockUpdater.On("update", mock.Anything, mock.Anything, pair.key, pair.value, mock.Anything, pair.preUpdateValue).Return(nil)
|
||||
}
|
||||
|
||||
err = updateFromStream(context.Background(), marshaledData, mockUpdateHandler, mockUpdater)
|
||||
|
||||
assert.NoError(t, err)
|
||||
mockUpdater.AssertNumberOfCalls(t, "update", p.expectedNumUpdates)
|
||||
mockUpdater.AssertExpectations(t)
|
||||
}
|
||||
}
|
||||
40
cmd/kt-server/test_config.yaml
Normal file
40
cmd/kt-server/test_config.yaml
Normal file
@ -0,0 +1,40 @@
|
||||
kt:
|
||||
server-addr: localhost:8082
|
||||
authorized-headers:
|
||||
ExampleHeader1:
|
||||
- example value one
|
||||
- example value two
|
||||
header-value-to-auditor-name:
|
||||
example value one: example-auditor-1
|
||||
example value two: example-auditor-2
|
||||
|
||||
kt-query:
|
||||
server-addr: localhost:8080
|
||||
|
||||
kt-test:
|
||||
server-addr: localhost:8081
|
||||
|
||||
metrics-addr: localhost:8083
|
||||
health-addr: localhost:8084
|
||||
|
||||
# Paste in the keys generated via `go run github.com/signalapp/keytransparency/cmd/generate-keys`
|
||||
api:
|
||||
signing-key: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
vrf-key: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
prefix-key: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
opening-key: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
# fake:
|
||||
# count: 1
|
||||
# interval: 10s
|
||||
distinguished: 1m
|
||||
auditors:
|
||||
example-auditor-1: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
example-auditor-2: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
min-search-delay: 1s
|
||||
min-monitor-delay: 1s
|
||||
jitter-percent: 10
|
||||
|
||||
db:
|
||||
file: example/db
|
||||
|
||||
account-db: mock
|
||||
145
cmd/kt-server/updater.go
Normal file
145
cmd/kt-server/updater.go
Normal file
@ -0,0 +1,145 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
metrics "github.com/hashicorp/go-metrics"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/cmd/internal/util"
|
||||
"github.com/signalapp/keytransparency/tree/transparency"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
type updateRequest struct {
|
||||
req *transparency.PreUpdateState
|
||||
res chan<- updateResponse
|
||||
}
|
||||
|
||||
type updateResponse struct {
|
||||
res *transparency.PostUpdateState
|
||||
err error
|
||||
}
|
||||
|
||||
type updateAuditorTreeHeadRequest struct {
|
||||
auditorTreeHead *tpb.AuditorTreeHead
|
||||
auditorName string
|
||||
err chan<- error
|
||||
}
|
||||
|
||||
// updater is a goroutine that:
|
||||
// - Updates the log from the Kinesis account update stream by receiving update requests over `ch`
|
||||
// - Updates the "distinguished" key by receiving update requests over `ch`
|
||||
// - Inserts fake updates
|
||||
// - Sets the auditor tree head
|
||||
func updater(tree *transparency.Tree, ch chan updateRequest, auditorTreeHeadsCh chan updateAuditorTreeHeadRequest, fake *config.FakeUpdates) {
|
||||
var ticker <-chan time.Time
|
||||
if fake != nil {
|
||||
ticker = time.NewTicker(fake.Interval).C
|
||||
}
|
||||
|
||||
sinceLastTick := 0
|
||||
for {
|
||||
select {
|
||||
case <-ticker: // Apply some fake updates to keep update rate consistent.
|
||||
if tree.CanFakeUpdate() {
|
||||
if sinceLastTick < fake.Count {
|
||||
numFakeUpdatesNeeded := fake.Count - sinceLastTick
|
||||
start := time.Now()
|
||||
err := tree.BatchUpdateFake(numFakeUpdatesNeeded)
|
||||
incrementInsertMetrics(err, start, float32(numFakeUpdatesNeeded), false)
|
||||
if err != nil {
|
||||
util.Log().Warnf("Error applying fake updates: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
sinceLastTick = 0
|
||||
|
||||
case first := <-ch: // Handle a real request to update the tree.
|
||||
if isTombstoneUpdate(first.req.GetUpdateRequest()) {
|
||||
handleTombstoneUpdate(tree, first)
|
||||
sinceLastTick++
|
||||
continue // Do not start a batch of updates
|
||||
}
|
||||
|
||||
reqs := []updateRequest{first}
|
||||
var tombstoneUpdate *updateRequest
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case req := <-ch:
|
||||
// If the channel receives a tombstone update, process the batch of updates so far
|
||||
// and then handle the tombstone update separately to preserve the ordering of the updates
|
||||
// as they're received from the channel.
|
||||
if isTombstoneUpdate(first.req.GetUpdateRequest()) {
|
||||
tombstoneUpdate = &req
|
||||
break loop
|
||||
} else {
|
||||
reqs = append(reqs, req)
|
||||
}
|
||||
default:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
states := make([]*transparency.PreUpdateState, len(reqs))
|
||||
for i, req := range reqs {
|
||||
states[i] = req.req
|
||||
}
|
||||
res, err := tree.BatchUpdate(states)
|
||||
|
||||
incrementInsertMetrics(err, start, float32(len(states)), true)
|
||||
sinceLastTick += len(reqs)
|
||||
|
||||
for i, req := range reqs {
|
||||
// These channel writes are guaranteed to not block, since this is the
|
||||
// only time we write to them, and they're buffered channels of size 1.
|
||||
if err == nil {
|
||||
req.res <- updateResponse{res: res[i], err: nil}
|
||||
} else {
|
||||
req.res <- updateResponse{res: nil, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
if tombstoneUpdate != nil {
|
||||
handleTombstoneUpdate(tree, *tombstoneUpdate)
|
||||
sinceLastTick++
|
||||
}
|
||||
|
||||
case req := <-auditorTreeHeadsCh: // We received a new tree head from our auditor.
|
||||
if err := tree.SetAuditorHead(req.auditorTreeHead, req.auditorName); err != nil {
|
||||
util.Log().Warnf("Error updating auditor head: %v", err)
|
||||
req.err <- err
|
||||
} else {
|
||||
req.err <- nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func incrementInsertMetrics(err error, start time.Time, batchSize float32, real bool) {
|
||||
metrics.IncrCounterWithLabels([]string{"inserts"}, batchSize, []metrics.Label{realLabel(real), successLabel(err)})
|
||||
metrics.IncrCounterWithLabels([]string{"insert_operations"}, 1, []metrics.Label{realLabel(real), successLabel(err), grpcStatusLabel(err)})
|
||||
metrics.AddSampleWithLabels([]string{"insert_batch_size"}, batchSize, []metrics.Label{realLabel(real), successLabel(err)})
|
||||
metrics.MeasureSinceWithLabels([]string{"insert_duration"}, start, []metrics.Label{realLabel(real), successLabel(err)})
|
||||
}
|
||||
|
||||
func handleTombstoneUpdate(tree *transparency.Tree, internalUpdateRequest updateRequest) {
|
||||
start := time.Now()
|
||||
res, err := tree.UpdateExistingIndexWithTombstoneValue(internalUpdateRequest.req)
|
||||
metrics.IncrCounterWithLabels([]string{"tombstone_update"}, 1, []metrics.Label{successLabel(err), grpcStatusLabel(err)})
|
||||
incrementInsertMetrics(err, start, 1, true)
|
||||
|
||||
if err == nil {
|
||||
internalUpdateRequest.res <- updateResponse{res: res, err: nil}
|
||||
} else {
|
||||
internalUpdateRequest.res <- updateResponse{res: nil, err: err}
|
||||
}
|
||||
}
|
||||
133
cmd/kt-server/util.go
Normal file
133
cmd/kt-server/util.go
Normal file
@ -0,0 +1,133 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
const (
|
||||
AuditorNameContextKey = "auditor-name"
|
||||
HeaderValueContextKey = "header-value"
|
||||
)
|
||||
|
||||
func verifyMappedValueConstantTime(mappedValue, expectedValue []byte) error {
|
||||
if 1 != subtle.ConstantTimeCompare(expectedValue, mappedValue) {
|
||||
return status.Error(codes.PermissionDenied, "provided value does not match expected value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDistinctValue returns a value that is different from the given []byte
|
||||
func createDistinctValue(value []byte) []byte {
|
||||
if len(value) < 1 {
|
||||
// This should only ever happen in the case of a programmer error.
|
||||
return []byte{0}
|
||||
}
|
||||
distinctValue := make([]byte, len(value))
|
||||
copy(distinctValue, value)
|
||||
distinctValue[0] = distinctValue[0] + 1
|
||||
return distinctValue
|
||||
}
|
||||
|
||||
func getServerOptions(config *config.ServiceConfig, additionalInterceptors []grpc.UnaryServerInterceptor) []grpc.ServerOption {
|
||||
if config.AuthorizedHeaders == nil || len(config.AuthorizedHeaders) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
interceptors := []grpc.UnaryServerInterceptor{func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.Unavailable, "metadata read error")
|
||||
}
|
||||
|
||||
matchedHeaderValue, err := validateAuthorizedHeaders(config.AuthorizedHeaders, md)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store the matched header value in the context for downstream interceptors
|
||||
ctx = context.WithValue(ctx, HeaderValueContextKey, matchedHeaderValue)
|
||||
|
||||
return handler(ctx, req)
|
||||
}}
|
||||
|
||||
if len(additionalInterceptors) > 0 {
|
||||
interceptors = append(interceptors, additionalInterceptors...)
|
||||
}
|
||||
|
||||
return []grpc.ServerOption{
|
||||
grpc.ChainUnaryInterceptor(interceptors...),
|
||||
}
|
||||
}
|
||||
|
||||
func storeAuditorNameInterceptor(config *config.ServiceConfig) func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
|
||||
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
|
||||
headerValue, ok := ctx.Value(HeaderValueContextKey).(string)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid type for header value. expected string, got %T", headerValue))
|
||||
}
|
||||
|
||||
if len(headerValue) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "no matched header value in context")
|
||||
}
|
||||
|
||||
auditorName := config.HeaderValueToAuditorName[headerValue]
|
||||
if len(auditorName) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("auditor name not specified for header value: %s", headerValue))
|
||||
}
|
||||
|
||||
// Store the auditor name in the context
|
||||
ctx = context.WithValue(ctx, AuditorNameContextKey, auditorName)
|
||||
return handler(ctx, req)
|
||||
}
|
||||
}
|
||||
|
||||
// validateAuthorizedHeaders ensures that at least one of the specified header to value mappings is present on the request
|
||||
// Returns the last header value that matched.
|
||||
func validateAuthorizedHeaders(authorizedHeaders map[string][]string, md metadata.MD) (string, error) {
|
||||
if authorizedHeaders == nil || len(authorizedHeaders) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
passedValidation := false
|
||||
matchedValue := ""
|
||||
for header, authorizedHeaderValues := range authorizedHeaders {
|
||||
requestHeaderValues := md.Get(header)
|
||||
if len(requestHeaderValues) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, requestHeaderValue := range requestHeaderValues {
|
||||
for _, authorizedValue := range authorizedHeaderValues {
|
||||
if subtle.ConstantTimeCompare([]byte(authorizedValue), []byte(requestHeaderValue)) == 1 {
|
||||
matchedValue = requestHeaderValue
|
||||
passedValidation = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !passedValidation {
|
||||
return "", status.Error(codes.PermissionDenied, fmt.Sprintf("invalid header values"))
|
||||
}
|
||||
|
||||
return matchedValue, nil
|
||||
}
|
||||
|
||||
func isTombstoneUpdate(updateRequest *pb.UpdateRequest) bool {
|
||||
return bytes.Equal(updateRequest.GetValue(), tombstoneBytes)
|
||||
}
|
||||
163
cmd/kt-server/util_test.go
Normal file
163
cmd/kt-server/util_test.go
Normal file
@ -0,0 +1,163 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var (
|
||||
validAci1 = random(16)
|
||||
mismatchedAci = createDistinctValue(validAci1)
|
||||
validAciIdentityKey1 = random(16)
|
||||
)
|
||||
|
||||
func random(length int) []byte {
|
||||
out := make([]byte, length)
|
||||
if _, err := rand.Read(out); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var testVerifyMappedValueParameters = []struct {
|
||||
providedValue []byte
|
||||
expectedValue []byte
|
||||
expectedErrorType codes.Code
|
||||
}{
|
||||
{validAci1, validAci1, codes.OK},
|
||||
{validAciIdentityKey1, validAciIdentityKey1, codes.OK},
|
||||
{validAci1, mismatchedAci, codes.PermissionDenied},
|
||||
}
|
||||
|
||||
func TestVerifyMappedValueConstantTime(t *testing.T) {
|
||||
for _, p := range testVerifyMappedValueParameters {
|
||||
err := verifyMappedValueConstantTime(p.providedValue, p.expectedValue)
|
||||
if (p.expectedErrorType != codes.OK) != (err != nil) {
|
||||
t.Fatalf("Expected %v, got %v",
|
||||
p.expectedErrorType, err)
|
||||
}
|
||||
|
||||
if p.expectedErrorType != codes.OK {
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != p.expectedErrorType || !ok {
|
||||
t.Fatalf("Expected error of type %v, got %v", p.expectedErrorType, grpcError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testValidateAuthorizedHeadersParameters = []struct {
|
||||
authorizedHeaders map[string][]string
|
||||
metadataHeaders map[string]string
|
||||
expectedErrorType codes.Code
|
||||
expectedMatchedValue string
|
||||
}{
|
||||
// empty is ok
|
||||
{nil, nil, codes.OK, ""},
|
||||
{map[string][]string{}, map[string]string{}, codes.OK, ""},
|
||||
// extra headers ok
|
||||
{nil, map[string]string{"H": "V"}, codes.OK, ""},
|
||||
// missing header is not ok
|
||||
{map[string][]string{"H": {"V"}}, nil, codes.PermissionDenied, ""},
|
||||
{map[string][]string{"H": {"V"}}, map[string]string{}, codes.PermissionDenied, ""},
|
||||
// authorized value with incorrect header is not ok
|
||||
{map[string][]string{"H": {"V"}}, map[string]string{"H1": "V"}, codes.PermissionDenied, ""},
|
||||
// correct header, incorrect value is not ok
|
||||
{map[string][]string{"H": {"V"}}, map[string]string{"H": "V1"}, codes.PermissionDenied, ""},
|
||||
// single header matches
|
||||
{map[string][]string{"H": {"V"}}, map[string]string{"H": "V"}, codes.OK, "V"},
|
||||
// one match, one missing is ok
|
||||
{map[string][]string{"H1": {"V1", "V2"}}, map[string]string{"H1": "V1"}, codes.OK, "V1"},
|
||||
// one match, one not match is ok
|
||||
{map[string][]string{"H1": {"V1"}, "H2": {"V3"}}, map[string]string{"H1": "V1", "H2": "V2"}, codes.OK, "V1"},
|
||||
}
|
||||
|
||||
func TestValidateAuthorizedHeaders(t *testing.T) {
|
||||
for _, p := range testValidateAuthorizedHeadersParameters {
|
||||
md := metadata.New(p.metadataHeaders)
|
||||
matchedValue, err := validateAuthorizedHeaders(p.authorizedHeaders, md)
|
||||
if (p.expectedErrorType != codes.OK) != (err != nil) {
|
||||
t.Fatalf("Expected %v, got %v",
|
||||
p.expectedErrorType, err)
|
||||
}
|
||||
if p.expectedErrorType != codes.OK {
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != p.expectedErrorType || !ok {
|
||||
t.Fatalf("Expected error of type %v, got %v", p.expectedErrorType, grpcError)
|
||||
}
|
||||
} else {
|
||||
if matchedValue != p.expectedMatchedValue {
|
||||
t.Fatalf("Expected matched value %s, got %s", p.expectedMatchedValue, matchedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testStoreAuditorNameInterceptorErrorParameters = []string{"example2.auditor", ""}
|
||||
|
||||
func TestStoreAuditorNameInterceptor_Error(t *testing.T) {
|
||||
cfg := &config.ServiceConfig{
|
||||
HeaderValueToAuditorName: map[string]string{
|
||||
"example1.auditor": "example-auditor-1",
|
||||
},
|
||||
}
|
||||
|
||||
mockHandler := func(ctx context.Context, req any) (any, error) {
|
||||
auditorName, ok := ctx.Value(AuditorNameContextKey).(string)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.Internal, "auditor name not found in context")
|
||||
}
|
||||
return auditorName, nil
|
||||
}
|
||||
|
||||
for _, invalidHeaderValue := range testStoreAuditorNameInterceptorErrorParameters {
|
||||
ctx := context.WithValue(context.Background(), HeaderValueContextKey, invalidHeaderValue)
|
||||
|
||||
interceptor := storeAuditorNameInterceptor(cfg)
|
||||
|
||||
_, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{}, mockHandler)
|
||||
|
||||
if grpcError, ok := status.FromError(err); grpcError.Code() != codes.InvalidArgument || !ok {
|
||||
t.Fatalf("Expected error of type %v, got %v", codes.InvalidArgument, grpcError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreAuditorNameInterceptor_Success(t *testing.T) {
|
||||
cfg := &config.ServiceConfig{
|
||||
HeaderValueToAuditorName: map[string]string{
|
||||
"example1.auditor": "example-auditor-1",
|
||||
},
|
||||
}
|
||||
|
||||
// Mock handler that returns the auditor name from context
|
||||
mockHandler := func(ctx context.Context, req any) (any, error) {
|
||||
auditorName, ok := ctx.Value(AuditorNameContextKey).(string)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.Internal, "auditor name not found in context")
|
||||
}
|
||||
return auditorName, nil
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), HeaderValueContextKey, "example1.auditor")
|
||||
|
||||
interceptor := storeAuditorNameInterceptor(cfg)
|
||||
|
||||
resp, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{}, mockHandler)
|
||||
|
||||
assert.NoError(t, err)
|
||||
auditorName, ok := resp.(string)
|
||||
assert.True(t, ok, "expected string response")
|
||||
assert.Equal(t, "example-auditor-1", auditorName)
|
||||
}
|
||||
97
cmd/kt-stress/main.go
Normal file
97
cmd/kt-stress/main.go
Normal file
@ -0,0 +1,97 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Command kt-stress is a tool for stress testing a key transparency server.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/message"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/signalapp/keytransparency/cmd/kt-server/pb"
|
||||
tpb "github.com/signalapp/keytransparency/tree/transparency/pb"
|
||||
)
|
||||
|
||||
var (
|
||||
p = message.NewPrinter(message.MatchLanguage("en"))
|
||||
|
||||
serverAddr = flag.String("addr", "localhost:8080", "Address of test server.")
|
||||
threads = flag.Int("threads", 1, "Number of threads to use.")
|
||||
)
|
||||
|
||||
var (
|
||||
TotalTime int64
|
||||
TotalUpdates int64
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC)
|
||||
flag.Parse()
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < *threads; i++ {
|
||||
go stress()
|
||||
}
|
||||
|
||||
p.Println("Testing started. Press Ctrl^C to stop...")
|
||||
ch := make(chan os.Signal)
|
||||
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(10 * time.Second):
|
||||
}
|
||||
|
||||
dur := time.Since(start)
|
||||
latency := atomic.LoadInt64(&TotalTime)
|
||||
updates := atomic.LoadInt64(&TotalUpdates)
|
||||
|
||||
p.Println()
|
||||
p.Println("Report:")
|
||||
p.Printf(" Duration: %v\n", dur.Round(time.Second))
|
||||
p.Printf(" Threads: %v\n", *threads)
|
||||
p.Printf(" Total updates: %d\n", updates)
|
||||
p.Printf(" Throughput: %.0f op/s\n", float64(updates)/dur.Seconds())
|
||||
p.Printf(" Latency: %.0f us/op\n", float64(latency)/float64(updates))
|
||||
}
|
||||
|
||||
func random() []byte {
|
||||
out := make([]byte, 16)
|
||||
if _, err := rand.Read(out); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stress() {
|
||||
conn, err := grpc.Dial(*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := pb.NewKeyTransparencyTestServiceClient(conn)
|
||||
|
||||
for {
|
||||
start := time.Now()
|
||||
req := &tpb.UpdateRequest{SearchKey: random(), Value: random()}
|
||||
_, err := client.Update(context.Background(), req)
|
||||
if err != nil {
|
||||
log.Fatalf("Error executing request: %v", err)
|
||||
}
|
||||
atomic.AddInt64(&TotalTime, time.Since(start).Microseconds())
|
||||
atomic.AddInt64(&TotalUpdates, 1)
|
||||
}
|
||||
}
|
||||
76
crypto/commitments/commitments.go
Normal file
76
crypto/commitments/commitments.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package commitments implements a cryptographic commitment.
|
||||
//
|
||||
// Commitment scheme is as follows:
|
||||
// T = HMAC(fixedKey, 16 byte nonce || message)
|
||||
// message is defined as: len(searchKey) || searchKey || len(data) || data
|
||||
package commitments
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
hashAlgo = sha256.New
|
||||
// key is publicly known random fixed key for use in the HMAC function.
|
||||
// This fixed key allows the commitment scheme to be modeled as a random oracle.
|
||||
fixedKey = []byte{0xd8, 0x21, 0xf8, 0x79, 0x0d, 0x97, 0x70, 0x97, 0x96, 0xb4, 0xd7, 0x90, 0x33, 0x57, 0xc3, 0xf5}
|
||||
// ErrInvalidCommitment occurs when the commitment doesn't match the profile.
|
||||
ErrInvalidCommitment = errors.New("invalid commitment")
|
||||
// ErrInvalidNonce occurs when the nonce is not 16 bytes.
|
||||
ErrInvalidNonce = errors.New("invalid nonce")
|
||||
)
|
||||
|
||||
// Commit returns an HMAC over the provided search key, data, and nonce using the SHA256 algorithm.
|
||||
// Returns an error if the length of the provided nonce is not 16.
|
||||
func Commit(searchKey, data, nonce []byte) ([]byte, error) {
|
||||
if len(nonce) != 16 {
|
||||
return nil, ErrInvalidNonce
|
||||
}
|
||||
|
||||
mac := hmac.New(hashAlgo, fixedKey)
|
||||
mac.Write(nonce)
|
||||
if len(searchKey) >= 1<<16 {
|
||||
panic("search key too large")
|
||||
}
|
||||
|
||||
// An error is not possible from any of the following Write() calls:
|
||||
// 1. binary.Write() delegates to mac.Write()
|
||||
// 2. mac is a hash.Hash, which is documented as never returning an error from Write()
|
||||
_ = binary.Write(mac, binary.BigEndian, uint16(len(searchKey)))
|
||||
mac.Write(searchKey)
|
||||
if len(data) >= 1<<32 {
|
||||
panic("data too large")
|
||||
}
|
||||
_ = binary.Write(mac, binary.BigEndian, uint32(len(data)))
|
||||
mac.Write(data)
|
||||
|
||||
return mac.Sum(nil), nil
|
||||
}
|
||||
|
||||
func Verify(searchKey, commitment, data, nonce []byte) error {
|
||||
got, err := Commit(searchKey, data, nonce)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hmac.Equal(got, commitment) {
|
||||
return ErrInvalidCommitment
|
||||
}
|
||||
return nil
|
||||
}
|
||||
111
crypto/commitments/commitments_test.go
Normal file
111
crypto/commitments/commitments_test.go
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commitments
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Use a constant key of zero to obtain consistent test vectors.
|
||||
var zeroKey = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
|
||||
func TestCommit(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
userID, data string
|
||||
muserID, mdata string
|
||||
mutate bool
|
||||
want error
|
||||
}{
|
||||
{"foo", "bar", "foo", "bar", false, nil},
|
||||
{"foo", "bar", "fo", "obar", false, ErrInvalidCommitment},
|
||||
{"foo", "bar", "foob", "ar", false, ErrInvalidCommitment},
|
||||
{"foo", "bar", "foo", "bar", true, ErrInvalidCommitment},
|
||||
} {
|
||||
data := []byte(tc.data)
|
||||
c, err := Commit([]byte(tc.userID), data, zeroKey)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error calculating commitment: %v", err)
|
||||
}
|
||||
if tc.mutate {
|
||||
c[0] ^= 1
|
||||
}
|
||||
if got := Verify([]byte(tc.muserID), c, data, zeroKey); !errors.Is(got, tc.want) {
|
||||
t.Errorf("Verify(%v, %x, %x, %x): %v, want %v", tc.userID, c, data, zeroKey, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidNonces(t *testing.T) {
|
||||
nonce1 := []byte{}
|
||||
nonce2 := []byte{0x00, 0x02}
|
||||
|
||||
commit1, err := Commit([]byte{0x00, 0x00}, []byte{0x00, 0x00, 0x00, 0x00}, nonce1)
|
||||
if err == nil || err != ErrInvalidNonce {
|
||||
t.Errorf("Expected %v in calculating commitment, got no error instead", ErrInvalidNonce)
|
||||
}
|
||||
|
||||
commit2, err := Commit([]byte{0x00, 0x00}, []byte{0x00, 0x00, 0x00, 0x00}, nonce2)
|
||||
if err == nil || err != ErrInvalidNonce {
|
||||
t.Errorf("Expected %v in calculating commitment, got no error instead", ErrInvalidNonce)
|
||||
}
|
||||
|
||||
if commit1 != nil || commit2 != nil {
|
||||
t.Errorf("Expected no commitments, got: \ncommit1=%x\ncommit2=%x", commit1, commit2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVectors(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
userID, data string
|
||||
want []byte
|
||||
}{
|
||||
{"", "", dh("edc3f59798cd87f2f48ec8836e2b6ef425cde9ab121ffdefc93d769db7cebabf")},
|
||||
{"foo", "bar", dh("25df431e884358826fe66f96d65702580104240abd63fa741d9ea3f32914bbf5")},
|
||||
{"foo1", "bar", dh("6c31a163a7660d1467fc1c997bd78b0a70b8921ca76b7eb0c6ca077f1e5e121e")},
|
||||
{"foo", "bar1", dh("5de6c6c9ed4bf48122f6c851c80e6eacbf885947f02f974cdc794b14c8e975f1")},
|
||||
} {
|
||||
userID := []byte(tc.userID)
|
||||
data := []byte(tc.data)
|
||||
got, err := Commit(userID, data, zeroKey)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error calculating commitment: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, tc.want) {
|
||||
t.Errorf("Commit(%v, %v): %x ,want %x", tc.userID, tc.data, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCommit(b *testing.B) {
|
||||
searchKey := make([]byte, 32)
|
||||
data := make([]byte, 500)
|
||||
nonce := make([]byte, 16)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Commit(searchKey, data, nonce)
|
||||
}
|
||||
}
|
||||
|
||||
func dh(h string) []byte {
|
||||
result, err := hex.DecodeString(h)
|
||||
if err != nil {
|
||||
panic("DecodeString failed")
|
||||
}
|
||||
return result
|
||||
}
|
||||
261
crypto/vrf/ed25519/ed25519.go
Normal file
261
crypto/vrf/ed25519/ed25519.go
Normal file
@ -0,0 +1,261 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Package ed25519 implements ECVRF-EDWARDS25519-SHA512-TAI from RFC 9381 (https://www.ietf.org/rfc/rfc9381.html).
|
||||
package ed25519
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"filippo.io/edwards25519"
|
||||
|
||||
"github.com/signalapp/keytransparency/crypto/vrf"
|
||||
)
|
||||
|
||||
var (
|
||||
ed25519IdentityPoint = edwards25519.NewIdentityPoint()
|
||||
|
||||
hashAlgo = sha512.New
|
||||
|
||||
// ErrInvalidPrivateKey occurs when a private key is the wrong size.
|
||||
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||
// ErrInvalidPublicKey occurs when a public key is the wrong size or has low order
|
||||
ErrInvalidPublicKey = errors.New("invalid public key")
|
||||
// ErrInvalidVRF occurs when the VRF does not validate.
|
||||
ErrInvalidVRF = errors.New("invalid VRF proof")
|
||||
)
|
||||
|
||||
// PublicKey holds a public VRF key.
|
||||
type PublicKey struct {
|
||||
inner ed25519.PublicKey
|
||||
}
|
||||
|
||||
// PrivateKey holds a private VRF key.
|
||||
type PrivateKey struct {
|
||||
inner []byte
|
||||
}
|
||||
|
||||
// GenerateKey generates a fresh keypair for this VRF.
|
||||
func GenerateKey() (vrf.PrivateKey, vrf.PublicKey) {
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &PrivateKey{inner: priv.Seed()}, &PublicKey{inner: pub}
|
||||
}
|
||||
|
||||
func encodeToCurve(salt, data []byte) (p *edwards25519.Point) {
|
||||
h := hashAlgo()
|
||||
for i := 0; i < 100; {
|
||||
h.Reset()
|
||||
h.Write([]byte{0x03, 0x01})
|
||||
h.Write(salt)
|
||||
h.Write(data)
|
||||
h.Write([]byte{byte(i), 0x00})
|
||||
|
||||
r := h.Sum(nil)
|
||||
p = interpretHashValueAsPoint(r[:32])
|
||||
if p != nil {
|
||||
p.MultByCofactor(p)
|
||||
// Check that we're not returning the identity point
|
||||
if ed25519IdentityPoint.Equal(p) != 1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
// This is practically unreachable
|
||||
panic("No curve point found")
|
||||
}
|
||||
|
||||
// interpretHashValueAsPoint checks if a 32-byte hash can be interpreted as a point on the edwards 25519 curve
|
||||
// as defined in [Section 5.1.3 of RFC8032](https://www.rfc-editor.org/rfc/rfc8032#section-5.1.3)
|
||||
// and returns that point if so.
|
||||
func interpretHashValueAsPoint(hash []byte) *edwards25519.Point {
|
||||
// Validate that the hash is 32 bytes
|
||||
if len(hash) != 32 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the input bytes are such that bytes 1 to 30 have value 255, byte 31 has value 255 or 127,
|
||||
// and byte 0 has value 255 - i for value i in the (0, 2, 3, 4, 8, 9, 12, 13, 14, 15) list, then
|
||||
// the encoding is invalid.
|
||||
invalidBytes1To30 := true
|
||||
for _, b := range hash[1:31] {
|
||||
if b != 255 {
|
||||
invalidBytes1To30 = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
invalidByte31 := hash[31] == 255 || hash[31] == 127
|
||||
|
||||
set := []uint8{0, 2, 3, 4, 8, 9, 12, 13, 14, 15}
|
||||
invalidByte0 := slices.Contains(set, 255-hash[0])
|
||||
|
||||
if invalidByte0 && invalidBytes1To30 && invalidByte31 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// the only error is if len(hash) != 32, which we validate above, so it is safe to ignore
|
||||
p, _ := new(edwards25519.Point).SetBytes(hash)
|
||||
return p
|
||||
}
|
||||
|
||||
func generateNonce(x, data []byte) *edwards25519.Scalar {
|
||||
h := hashAlgo()
|
||||
h.Write(x)
|
||||
h.Write(data)
|
||||
kStr := h.Sum(nil)
|
||||
|
||||
k, err := new(edwards25519.Scalar).SetUniformBytes(kStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func generateChallenge(p1, p2, p3, p4, p5 []byte) []byte {
|
||||
h := hashAlgo()
|
||||
h.Write([]byte{0x03, 0x02})
|
||||
h.Write(p1)
|
||||
h.Write(p2)
|
||||
h.Write(p3)
|
||||
h.Write(p4)
|
||||
h.Write(p5)
|
||||
h.Write([]byte{0x00})
|
||||
cStr := h.Sum(nil)
|
||||
|
||||
return cStr[:16]
|
||||
}
|
||||
|
||||
func proofToHash(Gamma *edwards25519.Point) [32]byte {
|
||||
h := hashAlgo()
|
||||
h.Write([]byte{0x03, 0x03})
|
||||
h.Write(new(edwards25519.Point).MultByCofactor(Gamma).Bytes())
|
||||
h.Write([]byte{0x00})
|
||||
|
||||
index := [32]byte{}
|
||||
copy(index[:], h.Sum(nil))
|
||||
return index
|
||||
}
|
||||
|
||||
// ECVRFProve returns the verifiable random function evaluated at m
|
||||
func (k PrivateKey) ECVRFProve(m []byte) (index [32]byte, proof []byte) {
|
||||
h := hashAlgo()
|
||||
h.Write(k.inner)
|
||||
hashedSk := h.Sum(nil)
|
||||
x, err := new(edwards25519.Scalar).SetBytesWithClamping(hashedSk[:32])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Y := k.Public().([]byte)
|
||||
|
||||
H := encodeToCurve(Y, m)
|
||||
hStr := H.Bytes()
|
||||
|
||||
Gamma := new(edwards25519.Point).ScalarMult(x, H)
|
||||
gammaStr := Gamma.Bytes()
|
||||
|
||||
nonce := generateNonce(hashedSk[32:], hStr)
|
||||
kB := new(edwards25519.Point).ScalarBaseMult(nonce)
|
||||
kH := new(edwards25519.Point).ScalarMult(nonce, H)
|
||||
cStr := generateChallenge(Y, hStr, gammaStr, kB.Bytes(), kH.Bytes())
|
||||
|
||||
c, err := new(edwards25519.Scalar).SetCanonicalBytes(append(cStr, make([]byte, 16)...))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s := new(edwards25519.Scalar).MultiplyAdd(c, x, nonce)
|
||||
|
||||
proof = append(append(gammaStr, cStr...), s.Bytes()...)
|
||||
|
||||
return proofToHash(Gamma), proof
|
||||
}
|
||||
|
||||
// ECVRFVerify checks that proof is correct for m and outputs index. It only supports key validation.
|
||||
func (pk PublicKey) ECVRFVerify(m, proof []byte) ([32]byte, error) {
|
||||
nilIndex := [32]byte{}
|
||||
if len(proof) != 80 {
|
||||
return nilIndex, ErrInvalidVRF
|
||||
}
|
||||
Gamma, err := new(edwards25519.Point).SetBytes(proof[:32])
|
||||
if err != nil {
|
||||
return nilIndex, ErrInvalidVRF
|
||||
}
|
||||
cStr := proof[32:48]
|
||||
cFull := make([]byte, 32)
|
||||
copy(cFull[:16], cStr)
|
||||
c, err := new(edwards25519.Scalar).SetCanonicalBytes(cFull)
|
||||
if err != nil {
|
||||
return nilIndex, ErrInvalidVRF
|
||||
}
|
||||
s, err := new(edwards25519.Scalar).SetCanonicalBytes(proof[48:80])
|
||||
if err != nil {
|
||||
return nilIndex, ErrInvalidVRF
|
||||
}
|
||||
|
||||
H := encodeToCurve(pk.inner, m)
|
||||
|
||||
U := new(edwards25519.Point).ScalarBaseMult(s)
|
||||
temp, err := new(edwards25519.Point).SetBytes(pk.inner)
|
||||
if err != nil {
|
||||
return nilIndex, ErrInvalidVRF
|
||||
}
|
||||
temp.ScalarMult(c, temp)
|
||||
U.Subtract(U, temp)
|
||||
|
||||
V := new(edwards25519.Point).ScalarMult(s, H)
|
||||
temp.ScalarMult(c, Gamma)
|
||||
V.Subtract(V, temp)
|
||||
|
||||
cPrime := generateChallenge(pk.inner, H.Bytes(), proof[:32], U.Bytes(), V.Bytes())
|
||||
if !bytes.Equal(cStr, cPrime) {
|
||||
return nilIndex, ErrInvalidVRF
|
||||
}
|
||||
|
||||
return proofToHash(Gamma), nil
|
||||
}
|
||||
|
||||
func (pk PublicKey) Bytes() []byte { return pk.inner }
|
||||
|
||||
// NewVRFSigner creates a signer object from a private key.
|
||||
func NewVRFSigner(key []byte) (vrf.PrivateKey, error) {
|
||||
if len(key) != 32 {
|
||||
return nil, ErrInvalidPrivateKey
|
||||
}
|
||||
return &PrivateKey{inner: key}, nil
|
||||
}
|
||||
|
||||
// Public returns the corresponding public key as bytes.
|
||||
func (k PrivateKey) Public() crypto.PublicKey {
|
||||
pub := ed25519.NewKeyFromSeed(k.inner).Public()
|
||||
return []byte(pub.(ed25519.PublicKey))
|
||||
}
|
||||
|
||||
// NewVRFVerifier creates a verifier object from a public key.
|
||||
func NewVRFVerifier(key ed25519.PublicKey) (vrf.PublicKey, error) {
|
||||
if len(key) != 32 {
|
||||
return nil, ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
// Reject a public key with low order
|
||||
p, err := new(edwards25519.Point).SetBytes(key)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidPublicKey
|
||||
}
|
||||
p.MultByCofactor(p)
|
||||
if ed25519IdentityPoint.Equal(p) == 1 {
|
||||
return nil, ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
return &PublicKey{inner: key}, nil
|
||||
}
|
||||
193
crypto/vrf/ed25519/ed25519_test.go
Normal file
193
crypto/vrf/ed25519/ed25519_test.go
Normal file
@ -0,0 +1,193 @@
|
||||
package ed25519
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
vectors = []struct {
|
||||
sk, pk, alpha, h, k, u, v, pi, beta []byte
|
||||
}{
|
||||
{
|
||||
sk: dh("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"),
|
||||
pk: dh("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"),
|
||||
alpha: []byte(""),
|
||||
h: dh("91bbed02a99461df1ad4c6564a5f5d829d0b90cfc7903e7a5797bd658abf3318"),
|
||||
k: dh("8a49edbd1492a8ee09766befe50a7d563051bf3406cbffc20a88def030730f0f"),
|
||||
u: dh("aef27c725be964c6a9bf4c45ca8e35df258c1878b838f37d9975523f09034071"),
|
||||
v: dh("5016572f71466c646c119443455d6cb9b952f07d060ec8286d678615d55f954f"),
|
||||
pi: dh("8657106690b5526245a92b003bb079ccd1a92130477671f6fc01ad16f26f723f26f8a57ccaed74ee1b190bed1f479d9727d2d0f9b005a6e456a35d4fb0daab1268a1b0db10836d9826a528ca76567805"),
|
||||
beta: dh("90cf1df3b703cce59e2a35b925d411164068269d7b2d29f3301c03dd757876ff66b71dda49d2de59d03450451af026798e8f81cd2e333de5cdf4f3e140fdd8ae"),
|
||||
},
|
||||
{
|
||||
sk: dh("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb"),
|
||||
pk: dh("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"),
|
||||
alpha: dh("72"),
|
||||
h: dh("5b659fc3d4e9263fd9a4ed1d022d75eaacc20df5e09f9ea937502396598dc551"),
|
||||
k: dh("d8c3a66921444cb3427d5d989f9b315aa8ca3375e9ec4d52207711a1fdb44107"),
|
||||
u: dh("1dcb0a4821a2c48bf53548228b7f170962988f6d12f5439f31987ef41f034ab3"),
|
||||
v: dh("fd03c0bf498c752161bae4719105a074630a2aa5f200ff7b3995f7bfb1513423"),
|
||||
pi: dh("f3141cd382dc42909d19ec5110469e4feae18300e94f304590abdced48aed5933bf0864a62558b3ed7f2fea45c92a465301b3bbf5e3e54ddf2d935be3b67926da3ef39226bbc355bdc9850112c8f4b02"),
|
||||
beta: dh("eb4440665d3891d668e7e0fcaf587f1b4bd7fbfe99d0eb2211ccec90496310eb5e33821bc613efb94db5e5b54c70a848a0bef4553a41befc57663b56373a5031"),
|
||||
},
|
||||
{
|
||||
sk: dh("c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7"),
|
||||
pk: dh("fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025"),
|
||||
alpha: dh("af82"),
|
||||
h: dh("bf4339376f5542811de615e3313d2b36f6f53c0acfebb482159711201192576a"),
|
||||
k: dh("5ffdbc72135d936014e8ab708585fda379405542b07e3bd2c0bd48437fbac60a"),
|
||||
u: dh("2bae73e15a64042fcebf062abe7e432b2eca6744f3e8265bc38e009cd577ecd5"),
|
||||
v: dh("88cba1cb0d4f9b649d9a86026b69de076724a93a65c349c988954f0961c5d506"),
|
||||
pi: dh("9bc0f79119cc5604bf02d23b4caede71393cedfbb191434dd016d30177ccbf8096bb474e53895c362d8628ee9f9ea3c0e52c7a5c691b6c18c9979866568add7a2d41b00b05081ed0f58ee5e31b3a970e"),
|
||||
beta: dh("645427e5d00c62a23fb703732fa5d892940935942101e456ecca7bb217c61c452118fec1219202a0edcf038bb6373241578be7217ba85a2687f7a0310b2df19f"),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestEncodeToCurve(t *testing.T) {
|
||||
for _, v := range vectors {
|
||||
H := encodeToCurve(v.pk, v.alpha)
|
||||
if got, want := H.Bytes(), v.h; !bytes.Equal(got, want) {
|
||||
t.Errorf("got = %x, want = %x", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func TestGenerateNonce(t *testing.T) {
|
||||
// for _, v := range vectors {
|
||||
// if got, want := generateNonce(v.sk, v.h).Bytes(), v.k; !bytes.Equal(got, want) {
|
||||
// t.Errorf("got = %x, want = %x", got, want)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestEvaluate(t *testing.T) {
|
||||
for _, v := range vectors {
|
||||
signer, err := NewVRFSigner(v.sk)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
index, proof := signer.ECVRFProve(v.alpha)
|
||||
if got, want := index[:], v.beta[:32]; !bytes.Equal(got, want) {
|
||||
t.Errorf("got = %x, want = %x", got, want)
|
||||
}
|
||||
if got, want := proof, v.pi; !bytes.Equal(got, want) {
|
||||
t.Errorf("got = %x, want = %x", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProofToHash(t *testing.T) {
|
||||
for _, v := range vectors {
|
||||
verifier, err := NewVRFVerifier(v.pk)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
index, err := verifier.ECVRFVerify(v.alpha, v.pi)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got, want := index[:], v.beta[:32]; !bytes.Equal(got, want) {
|
||||
t.Errorf("got = %x, want = %x", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProofToHashFails(t *testing.T) {
|
||||
for _, v := range vectors {
|
||||
verifier, err := NewVRFVerifier(v.pk)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = verifier.ECVRFVerify([]byte("a"), v.pi)
|
||||
if got, want := err, ErrInvalidVRF; got != want {
|
||||
t.Errorf("got = %v, want = %v", got, want)
|
||||
}
|
||||
for i := 0; i < len(v.pi); i++ {
|
||||
pi := make([]byte, len(v.pi))
|
||||
copy(pi, v.pi)
|
||||
pi[i] ^= 1
|
||||
_, err = verifier.ECVRFVerify(v.alpha, pi)
|
||||
if got, want := err, ErrInvalidVRF; got != want {
|
||||
t.Errorf("got = %v, want = %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testHashValueAsPointInvalidParameters = []struct {
|
||||
hash []byte
|
||||
}{
|
||||
// Length of hash is not 32
|
||||
{[]byte{0}},
|
||||
|
||||
// Non-canonical encodings
|
||||
{[]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{251, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{247, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{246, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{243, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{242, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{241, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
{[]byte{240, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}},
|
||||
|
||||
{[]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{251, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{247, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{246, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{243, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{242, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{241, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
{[]byte{240, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}},
|
||||
}
|
||||
|
||||
func TestInterpretHashValueAsPointInvalid(t *testing.T) {
|
||||
for _, p := range testHashValueAsPointInvalidParameters {
|
||||
point := interpretHashValueAsPoint(p.hash)
|
||||
|
||||
if point != nil {
|
||||
t.Fatal("expected no point")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretHashValueAsPointValid(t *testing.T) {
|
||||
hash := []byte{1, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}
|
||||
point := interpretHashValueAsPoint(hash)
|
||||
|
||||
if point == nil {
|
||||
t.Fatal("expected a point")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEvaluate(b *testing.B) {
|
||||
priv := make([]byte, 32)
|
||||
rand.Read(priv)
|
||||
|
||||
signer, err := NewVRFSigner(priv)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
m := make([]byte, 32)
|
||||
rand.Read(m)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
signer.ECVRFProve(m)
|
||||
}
|
||||
}
|
||||
|
||||
func dh(h string) []byte {
|
||||
result, err := hex.DecodeString(h)
|
||||
if err != nil {
|
||||
panic("DecodeString failed")
|
||||
}
|
||||
return result
|
||||
}
|
||||
34
crypto/vrf/vrf.go
Normal file
34
crypto/vrf/vrf.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package vrf defines the interface to a verifiable random function.
|
||||
package vrf
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
)
|
||||
|
||||
// PrivateKey supports evaluating the VRF function.
|
||||
type PrivateKey interface {
|
||||
// ECVRFProve returns the output of H(f_k(m)) and its proof.
|
||||
ECVRFProve(m []byte) (index [32]byte, proof []byte)
|
||||
// Public returns the corresponding public key.
|
||||
Public() crypto.PublicKey
|
||||
}
|
||||
|
||||
// PublicKey supports verifying output from the VRF function.
|
||||
type PublicKey interface {
|
||||
// ECVRFVerify verifies the NP-proof supplied by Proof and outputs Index.
|
||||
ECVRFVerify(m, proof []byte) (index [32]byte, err error)
|
||||
}
|
||||
122
db/cache_control.go
Normal file
122
db/cache_control.go
Normal file
@ -0,0 +1,122 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-metrics"
|
||||
)
|
||||
|
||||
func countPrefixCacheControlHit(hit bool) {
|
||||
lbls := []metrics.Label{{Name: "cache_hit", Value: fmt.Sprint(hit)}}
|
||||
metrics.IncrCounterWithLabels([]string{"prefix_cache_control"}, 1, lbls)
|
||||
}
|
||||
|
||||
// PrefixCacheControl is used to minimize database requests during update operations
|
||||
// by fetching and caching the necessary prefix tree data beforehand.
|
||||
// Blocking the update critical path on network requests to the database can significantly degrade performance.
|
||||
// This cache is separate from the LRU cache because the LRU cache can end up evicting data needed for the update prematurely.
|
||||
type PrefixCacheControl struct {
|
||||
db PrefixStore
|
||||
|
||||
tracking bool
|
||||
data map[uint64][]byte
|
||||
}
|
||||
|
||||
func NewPrefixCacheControl(db PrefixStore) *PrefixCacheControl {
|
||||
return &PrefixCacheControl{db: db}
|
||||
}
|
||||
|
||||
// StartTracking begins keeping a copy of all data fetched.
|
||||
func (p *PrefixCacheControl) StartTracking() {
|
||||
p.tracking = true
|
||||
p.data = make(map[uint64][]byte)
|
||||
}
|
||||
|
||||
// StopTracking stops keeping copies of data fetched and clears the data stored.
|
||||
func (p *PrefixCacheControl) StopTracking() {
|
||||
p.tracking = false
|
||||
p.data = nil
|
||||
}
|
||||
|
||||
// ExportCache returns the set of database entries cached.
|
||||
func (p *PrefixCacheControl) ExportCache() map[uint64][]byte {
|
||||
return p.data
|
||||
}
|
||||
|
||||
// ImportCache adds a set of cached database lookups to the current store's
|
||||
// cache.
|
||||
func (p *PrefixCacheControl) ImportCache(data map[uint64][]byte) error {
|
||||
if p.data == nil {
|
||||
p.data = make(map[uint64][]byte)
|
||||
}
|
||||
for key, val := range data {
|
||||
if existing, ok := p.data[key]; ok {
|
||||
if !bytes.Equal(val, existing) {
|
||||
return fmt.Errorf("different values found for the same prefix tree key: %v", key)
|
||||
}
|
||||
} else {
|
||||
p.data[key] = val
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchGet fetches a batch of keys by first checking the cache and then the underlying datastore.
|
||||
func (p *PrefixCacheControl) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
remaining := make([]uint64, 0)
|
||||
data := make(map[uint64][]byte)
|
||||
|
||||
for _, key := range keys {
|
||||
if val, ok := p.data[key]; ok {
|
||||
countPrefixCacheControlHit(true)
|
||||
data[key] = dup(val)
|
||||
} else {
|
||||
countPrefixCacheControlHit(false)
|
||||
remaining = append(remaining, key)
|
||||
}
|
||||
}
|
||||
|
||||
if len(remaining) > 0 {
|
||||
partial, err := p.db.BatchGet(remaining)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, val := range partial {
|
||||
if p.tracking {
|
||||
p.data[key] = dup(val)
|
||||
}
|
||||
data[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (p *PrefixCacheControl) GetCached(key uint64) []byte {
|
||||
if val, ok := p.data[key]; ok {
|
||||
countPrefixCacheControlHit(true)
|
||||
return dup(val)
|
||||
}
|
||||
countPrefixCacheControlHit(false)
|
||||
val := p.db.GetCached(key)
|
||||
if val != nil && p.tracking {
|
||||
p.data[key] = dup(val)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Put stores the given key-value pair in the underlying datastore and caches it if tracking is enabled.
|
||||
func (p *PrefixCacheControl) Put(key uint64, val []byte) {
|
||||
if p.tracking {
|
||||
p.data[key] = dup(val)
|
||||
} else if _, ok := p.data[key]; ok {
|
||||
p.data[key] = dup(val)
|
||||
}
|
||||
p.db.Put(key, val)
|
||||
}
|
||||
168
db/db.go
Normal file
168
db/db.go
Normal file
@ -0,0 +1,168 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Package db implements database wrappers that match a common interface.
|
||||
package db
|
||||
|
||||
import consumer "github.com/harlow/kinesis-consumer"
|
||||
|
||||
// LogStore is the interface a log tree uses to communicate with its database.
|
||||
type LogStore interface {
|
||||
BatchGet(keys []uint64) (data map[uint64][]byte, err error)
|
||||
BatchPut(data map[uint64][]byte)
|
||||
}
|
||||
|
||||
// PrefixStore is the interface a prefix tree uses to communicate with its
|
||||
// database.
|
||||
type PrefixStore interface {
|
||||
BatchGet(keys []uint64) (data map[uint64][]byte, err error)
|
||||
// GetCached gets a value if and only if it's cached. If the requested value
|
||||
// is not cached, it returns nil.
|
||||
GetCached(key uint64) []byte
|
||||
|
||||
Put(key uint64, data []byte)
|
||||
}
|
||||
|
||||
// Account contains the account info necessary to verify a phone number search
|
||||
type Account struct {
|
||||
DiscoverableByPhoneNumber bool `dynamodbav:"C"`
|
||||
UnidentifiedAccessKey []byte `dynamodbav:"UAK"`
|
||||
}
|
||||
|
||||
type AccountDB interface {
|
||||
// GetAccountByAci returns the Account struct corresponding to the ACI, or nil if none exists
|
||||
GetAccountByAci(aci []byte) (*Account, error)
|
||||
}
|
||||
|
||||
// storedTreeHead represents the key transparency service's signed transparency tree head, as well as
|
||||
// any auditor-signed tree heads.
|
||||
type storedTreeHead struct {
|
||||
TreeHead *TransparencyTreeHead `json:"head"`
|
||||
AuditorHeads map[string]*AuditorTreeHead `json:"auditor-heads,omitempty"`
|
||||
}
|
||||
|
||||
// Signature represents the key transparency service's signature over the head of a transparency tree.
|
||||
// The signed data includes the auditor public key, so the service generates one Signature for each auditor.
|
||||
type Signature struct {
|
||||
Signature []byte
|
||||
AuditorPublicKey []byte
|
||||
}
|
||||
|
||||
// TransparencyTreeHead is used by the key transparency service to represent the signed head of a transparency tree.
|
||||
type TransparencyTreeHead struct {
|
||||
TreeSize uint64 `json:"n"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Signatures []*Signature `json:"sigs"`
|
||||
}
|
||||
|
||||
// AuditorTransparencyTreeHead is used by a third-party auditor to represent its signed view of the transparency tree.
|
||||
type AuditorTransparencyTreeHead struct {
|
||||
TreeSize uint64 `json:"n"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Signature []byte `json:"sig,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AuditorTransparencyTreeHead) Clone() *AuditorTransparencyTreeHead {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &AuditorTransparencyTreeHead{
|
||||
TreeSize: a.TreeSize,
|
||||
Timestamp: a.Timestamp,
|
||||
Signature: dup(a.Signature),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TransparencyTreeHead) Clone() *TransparencyTreeHead {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
return &TransparencyTreeHead{
|
||||
TreeSize: h.TreeSize,
|
||||
Timestamp: h.Timestamp,
|
||||
Signatures: cloneTransparencyTreeHeadSignatures(h.Signatures),
|
||||
}
|
||||
}
|
||||
|
||||
func cloneTransparencyTreeHeadSignatures(original []*Signature) []*Signature {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var clonedSignatures []*Signature
|
||||
for _, sig := range original {
|
||||
if sig == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
newSignature := &Signature{dup(sig.Signature), dup(sig.AuditorPublicKey)}
|
||||
clonedSignatures = append(clonedSignatures, newSignature)
|
||||
}
|
||||
return clonedSignatures
|
||||
}
|
||||
|
||||
// AuditorTreeHead represents the signed head of a transparency tree, from a
|
||||
// third-party auditor.
|
||||
type AuditorTreeHead struct {
|
||||
AuditorTransparencyTreeHead
|
||||
RootValue []byte `json:"root"`
|
||||
Consistency [][]byte `json:"consistency"`
|
||||
}
|
||||
|
||||
func (h *AuditorTreeHead) Clone() *AuditorTreeHead {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
var consistency [][]byte
|
||||
if h.Consistency != nil {
|
||||
consistency = make([][]byte, len(h.Consistency))
|
||||
for i, val := range h.Consistency {
|
||||
consistency[i] = dup(val)
|
||||
}
|
||||
}
|
||||
return &AuditorTreeHead{
|
||||
AuditorTransparencyTreeHead: *h.AuditorTransparencyTreeHead.Clone(),
|
||||
RootValue: dup(h.RootValue),
|
||||
Consistency: consistency,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneAuditorTreeHeadMap(original map[string]*AuditorTreeHead) map[string]*AuditorTreeHead {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
clonedAuditorTreeHeads := make(map[string]*AuditorTreeHead)
|
||||
for key, value := range original {
|
||||
clonedAuditorTreeHeads[key] = value.Clone()
|
||||
}
|
||||
return clonedAuditorTreeHeads
|
||||
}
|
||||
|
||||
type StreamStore consumer.Store
|
||||
|
||||
// TransparencyStore is the interface a transparency tree uses to communicate
|
||||
// with its database.
|
||||
type TransparencyStore interface {
|
||||
// Clone returns a read-only clone of the current transparency store,
|
||||
// suitable for distributing to child goroutines.
|
||||
Clone() TransparencyStore
|
||||
|
||||
// GetHead returns the most recent tree head, or the zero value of
|
||||
// TransparencyTreeHead if there hasn't been a signed head yet.
|
||||
// It also returns a map of auditor name to the corresponding most recently set auditor tree head.
|
||||
// The auditor map must not be nil if err is nil.
|
||||
GetHead() (*TransparencyTreeHead, map[string]*AuditorTreeHead, error)
|
||||
|
||||
Get(key uint64) ([]byte, error)
|
||||
Put(key uint64, data []byte)
|
||||
|
||||
LogStore() LogStore
|
||||
PrefixStore() PrefixStore
|
||||
StreamStore() StreamStore
|
||||
|
||||
Commit(head *TransparencyTreeHead, auditors map[string]*AuditorTreeHead) error
|
||||
}
|
||||
118
db/db_test.go
Normal file
118
db/db_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createTestSignature(sig, pubKey []byte) *Signature {
|
||||
return &Signature{
|
||||
Signature: sig,
|
||||
AuditorPublicKey: pubKey,
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneTransparencyTreeHeadSignatures(t *testing.T) {
|
||||
original := []*Signature{
|
||||
createTestSignature([]byte("sig1"), []byte("key1")),
|
||||
createTestSignature([]byte("sig2"), []byte("key2")),
|
||||
}
|
||||
|
||||
cloned := cloneTransparencyTreeHeadSignatures(original)
|
||||
|
||||
// Check if the signatures are correctly cloned
|
||||
for i, orig := range original {
|
||||
// Verify Signature and AuditorPublicKey have same content as original
|
||||
if !bytes.Equal(cloned[i].Signature, orig.Signature) {
|
||||
t.Fatalf("cloned Signature doesn't match original: got %x, want %x",
|
||||
cloned[i].Signature, orig.Signature)
|
||||
}
|
||||
|
||||
if !bytes.Equal(cloned[i].AuditorPublicKey, orig.AuditorPublicKey) {
|
||||
t.Fatalf("cloned AuditorPublicKey doesn't match original: got %x, want %x",
|
||||
cloned[i].AuditorPublicKey, orig.AuditorPublicKey)
|
||||
}
|
||||
|
||||
// Verify that the clone is a deep copy
|
||||
if &cloned[i].Signature[0] == &original[i].Signature[0] {
|
||||
t.Fatalf("Signature %d was not deep copied", i)
|
||||
}
|
||||
|
||||
if &cloned[i].AuditorPublicKey[0] == &original[i].AuditorPublicKey[0] {
|
||||
t.Fatalf("AuditorPublicKey %d was not deep copied", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneAuditorTreeHeadMap(t *testing.T) {
|
||||
original := map[string]*AuditorTreeHead{
|
||||
"auditor1": createTestAuditorTreeHead(1, 100, []byte("sig1")),
|
||||
"auditor2": createTestAuditorTreeHead(2, 200, []byte("sig2")),
|
||||
}
|
||||
|
||||
cloned := cloneAuditorTreeHeadMap(original)
|
||||
|
||||
for auditor, origAuditorTreeHead := range original {
|
||||
assert.True(t, isAuditorTreeHeadDeepCopy(*origAuditorTreeHead, *cloned[auditor]),
|
||||
fmt.Sprintf("cloned auditor head %+v is not a deep copy of %+v", cloned[auditor], origAuditorTreeHead))
|
||||
}
|
||||
}
|
||||
|
||||
func createTestAuditorTreeHead(treeSize uint64, timestamp int64, sig []byte) *AuditorTreeHead {
|
||||
return &AuditorTreeHead{
|
||||
AuditorTransparencyTreeHead: AuditorTransparencyTreeHead{
|
||||
TreeSize: treeSize,
|
||||
Timestamp: timestamp,
|
||||
Signature: sig,
|
||||
},
|
||||
RootValue: []byte("root"),
|
||||
Consistency: [][]byte{[]byte("cons1"), []byte("cons2")},
|
||||
}
|
||||
}
|
||||
|
||||
// isDeepCopy checks that the contents of a and b are equal, but that they do not point to the same underlying array
|
||||
func isDeepCopy(a, b []byte) bool {
|
||||
if !bytes.Equal(a, b) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return &a[0] != &b[0]
|
||||
}
|
||||
|
||||
func isAuditorTreeHeadDeepCopy(a, b AuditorTreeHead) bool {
|
||||
if a.TreeSize != b.TreeSize || a.Timestamp != b.Timestamp {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isDeepCopy(a.Signature, b.Signature) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isDeepCopy(a.RootValue, b.RootValue) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(a.Consistency) != len(b.Consistency) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a.Consistency {
|
||||
if !isDeepCopy(a.Consistency[i], b.Consistency[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
650
db/dynamodb.go
Normal file
650
db/dynamodb.go
Normal file
@ -0,0 +1,650 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/ratelimit"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/retry"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
|
||||
metrics "github.com/hashicorp/go-metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
maxBatchKeys = 90
|
||||
maxDynamoBatchSize = 100
|
||||
keyLabel = "k"
|
||||
valueLabel = "v"
|
||||
attrCanonicallyDiscoverable = "C"
|
||||
attrUnidentifiedAccessKey = "UAK"
|
||||
attrAci = "U"
|
||||
)
|
||||
|
||||
type dynamoReadReq struct {
|
||||
keys []string
|
||||
data map[string][]byte
|
||||
resp chan error
|
||||
consistent bool
|
||||
}
|
||||
|
||||
// ddbConn is a wrapper around a base DynamoDB connection that handles batching
|
||||
// writes between commits transparently.
|
||||
type ddbConn struct {
|
||||
conn *dynamodb.Client
|
||||
table string
|
||||
ch chan dynamoReadReq
|
||||
readonly bool
|
||||
// If readonly is true, batch will by definition be nil (thus: empty).
|
||||
// Since a writable ddbConn doesn't allow concurrent reads/writes,
|
||||
// and readonly ddbConn always has a nil map, there is no need to lock this
|
||||
// against concurrent access.
|
||||
batch map[string][]byte
|
||||
}
|
||||
|
||||
func newDDBConn(conn *dynamodb.Client, table string, parallel int) *ddbConn {
|
||||
batches := make(chan []dynamoReadReq)
|
||||
out := &ddbConn{
|
||||
conn: conn,
|
||||
table: table,
|
||||
ch: make(chan dynamoReadReq, 100),
|
||||
readonly: false,
|
||||
batch: make(map[string][]byte),
|
||||
}
|
||||
|
||||
// Start a number of worker goroutines, configured by `parallel`, that will
|
||||
// take batches of read requests and send them to Dynamo. Build batches in a
|
||||
// dedicated goroutine, rather than have each worker goroutine build it's
|
||||
// own batches, to ensure we have the largest batches possible.
|
||||
//
|
||||
// Manual batching like this is required in the first place because Dynamo
|
||||
// doesn't support HTTP/2 -- firing off a bunch of concurrent requests
|
||||
// results in a large number of distinct connections being made, and the
|
||||
// overhead of all those connections degrades performance.
|
||||
go func() {
|
||||
for {
|
||||
batches <- out.receiveBatch()
|
||||
}
|
||||
}()
|
||||
for i := 0; i < parallel; i++ {
|
||||
go func() {
|
||||
for {
|
||||
reqs := <-batches
|
||||
err := out.handleBatch(reqs)
|
||||
for _, req := range reqs {
|
||||
req.resp <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// receiveBatch takes a series of read requests off of the queue, trying to get
|
||||
// a batch of maxBatchKeys keys.
|
||||
//
|
||||
// The max batch size to Dynamo is 100 keys, but once we take a request off of
|
||||
// the queue we have to handle it. Aims for maxBatchKeys to avoid overcommitting.
|
||||
func (c *ddbConn) receiveBatch() []dynamoReadReq {
|
||||
var out []dynamoReadReq
|
||||
total := 0
|
||||
|
||||
req := <-c.ch
|
||||
out = append(out, req)
|
||||
total += len(req.keys)
|
||||
|
||||
loop:
|
||||
for total < maxBatchKeys {
|
||||
select {
|
||||
case req := <-c.ch:
|
||||
out = append(out, req)
|
||||
total += len(req.keys)
|
||||
default:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// handleBatch processes a batch of read requests and writes the fetched data into a receiving map on each request.
|
||||
func (c *ddbConn) handleBatch(reqs []dynamoReadReq) error {
|
||||
// Build an index from the keys to lookup, to the requests for that key.
|
||||
keyIndex := make(map[string][]int)
|
||||
|
||||
for i, req := range reqs {
|
||||
for _, key := range req.keys {
|
||||
keyIndex[key] = append(keyIndex[key], i)
|
||||
}
|
||||
}
|
||||
|
||||
// Build a slice of keys to batch get
|
||||
keys := make([]string, 0, len(keyIndex))
|
||||
for key := range keyIndex {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
// Fetch keys in batch
|
||||
data := make(map[string][]byte, len(keys))
|
||||
err := c.batchGet(keys, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metrics.IncrCounter([]string{"dynamodb", "strongly_consistent_read"}, 1)
|
||||
|
||||
// Move the fetched data into the receiving maps.
|
||||
for key, val := range data {
|
||||
for i, j := range keyIndex[key] {
|
||||
if i == 0 {
|
||||
reqs[j].data[key] = val
|
||||
} else {
|
||||
reqs[j].data[key] = dup(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metrics.AddSample([]string{"dynamodb", "batch_size"}, float32(len(keys)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// batchGet fetches a batch of keys from DynamoDB and writes the data into the provided map.
|
||||
func (c *ddbConn) batchGet(keys []string, res map[string][]byte) error {
|
||||
kvs := make([]map[string]types.AttributeValue, len(keys))
|
||||
for i, key := range keys {
|
||||
kvs[i] = map[string]types.AttributeValue{
|
||||
keyLabel: &types.AttributeValueMemberS{Value: key},
|
||||
}
|
||||
}
|
||||
consistent := true
|
||||
for len(kvs) > 0 {
|
||||
var now []map[string]types.AttributeValue
|
||||
if len(kvs) > maxDynamoBatchSize {
|
||||
now, kvs = kvs[:maxDynamoBatchSize], kvs[maxDynamoBatchSize:]
|
||||
} else {
|
||||
now, kvs = kvs, nil
|
||||
}
|
||||
out, err := c.conn.BatchGetItem(context.Background(), &dynamodb.BatchGetItemInput{
|
||||
RequestItems: map[string]types.KeysAndAttributes{c.table: {
|
||||
Keys: now,
|
||||
ConsistentRead: &consistent,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unprocessed := out.UnprocessedKeys[c.table].Keys
|
||||
if len(unprocessed) > 0 {
|
||||
kvs = append(kvs, unprocessed...)
|
||||
}
|
||||
|
||||
consumed := len(now) - len(unprocessed)
|
||||
metrics.IncrCounterWithLabels(
|
||||
[]string{"dynamodb", "read_capacity"},
|
||||
float32(consumed),
|
||||
[]metrics.Label{{Name: "consistent", Value: fmt.Sprint(consistent)}},
|
||||
)
|
||||
|
||||
for _, data := range out.Responses[c.table] {
|
||||
key, ok := data[keyLabel].(*types.AttributeValueMemberS)
|
||||
if !ok {
|
||||
return fmt.Errorf("malformed database entry")
|
||||
}
|
||||
value, ok := data[valueLabel].(*types.AttributeValueMemberB)
|
||||
if !ok {
|
||||
return fmt.Errorf("malformed database entry")
|
||||
}
|
||||
res[key.Value] = value.Value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get fetches a single key from DynamoDB.
|
||||
func (c *ddbConn) Get(key string) ([]byte, error) {
|
||||
keySet := [1]string{key}
|
||||
out, err := c.BatchGet(keySet[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out[key], nil
|
||||
}
|
||||
|
||||
// BatchGet fetches a batch of keys from DynamoDB.
|
||||
func (c *ddbConn) BatchGet(keys []string) (map[string][]byte, error) {
|
||||
data := map[string][]byte{}
|
||||
var uncachedKeys []string
|
||||
// Move cached keys directly into the output data map.
|
||||
for _, key := range keys {
|
||||
if value, ok := c.batch[key]; ok {
|
||||
data[key] = dup(value)
|
||||
} else {
|
||||
uncachedKeys = append(uncachedKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch remaining keys from the database.
|
||||
start := time.Now()
|
||||
consistent := true
|
||||
req := dynamoReadReq{
|
||||
keys: uncachedKeys,
|
||||
data: data,
|
||||
resp: make(chan error, 1),
|
||||
consistent: consistent,
|
||||
}
|
||||
c.ch <- req
|
||||
if err := <-req.resp; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metrics.MeasureSinceWithLabels([]string{"dynamodb", "get_duration"}, start, []metrics.Label{
|
||||
{Name: "readonly", Value: fmt.Sprint(c.readonly)},
|
||||
{Name: "consistent", Value: fmt.Sprint(consistent)},
|
||||
{Name: "singular", Value: fmt.Sprint(len(keys) == 1)},
|
||||
})
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *ddbConn) Put(key string, value []byte) {
|
||||
if c.readonly {
|
||||
panic("connection is readonly")
|
||||
}
|
||||
c.batch[key] = dup(value)
|
||||
}
|
||||
|
||||
// Commit commits all outstanding writes to DynamoDB.
|
||||
// The tree head ("root") data write is deferred until all other writes have succeeded.
|
||||
func (c *ddbConn) Commit() error {
|
||||
if c.readonly {
|
||||
panic("connection is readonly")
|
||||
}
|
||||
start := time.Now()
|
||||
iters := 0
|
||||
|
||||
defer func() { c.batch = map[string][]byte{} }()
|
||||
|
||||
// Build the initial set of write requests to make.
|
||||
reqs := make([]types.WriteRequest, 0)
|
||||
for key, value := range c.batch {
|
||||
if key == "root" {
|
||||
continue
|
||||
}
|
||||
reqs = append(reqs, types.WriteRequest{PutRequest: &types.PutRequest{
|
||||
Item: map[string]types.AttributeValue{
|
||||
keyLabel: &types.AttributeValueMemberS{Value: key},
|
||||
valueLabel: &types.AttributeValueMemberB{Value: value},
|
||||
},
|
||||
}})
|
||||
}
|
||||
|
||||
// Submit requests to database in batches, looping until all writes have propagated to the database.
|
||||
for len(reqs) > 0 {
|
||||
iters++
|
||||
|
||||
var err error
|
||||
reqs, err = c.batchWriteParallel(reqs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Write the new root to the database last.
|
||||
if value, ok := c.batch["root"]; ok {
|
||||
_, err := c.conn.PutItem(context.Background(), &dynamodb.PutItemInput{
|
||||
TableName: &c.table,
|
||||
Item: map[string]types.AttributeValue{
|
||||
keyLabel: &types.AttributeValueMemberS{Value: "root"},
|
||||
valueLabel: &types.AttributeValueMemberB{Value: value},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metrics.IncrCounter([]string{"dynamodb", "write_capacity"}, 1)
|
||||
}
|
||||
metrics.MeasureSinceWithLabels(
|
||||
[]string{"dynamodb", "commit_duration"},
|
||||
start,
|
||||
[]metrics.Label{{Name: "iters", Value: fmt.Sprint(iters)}},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// batchWriteParallel splits writes across multiple goroutines and returns a list of unfulfilled write requests.
|
||||
func (c *ddbConn) batchWriteParallel(reqs []types.WriteRequest) ([]types.WriteRequest, error) {
|
||||
type dynamoWriteRes struct {
|
||||
unprocessed []types.WriteRequest
|
||||
err error
|
||||
}
|
||||
ch := make(chan dynamoWriteRes)
|
||||
|
||||
goroutines := 0
|
||||
for len(reqs) > 0 {
|
||||
var now []types.WriteRequest
|
||||
if len(reqs) > 25 {
|
||||
now, reqs = reqs[:25], reqs[25:]
|
||||
} else {
|
||||
now, reqs = reqs, nil
|
||||
}
|
||||
go func() {
|
||||
unprocessed, err := c.batchWrite(now)
|
||||
ch <- dynamoWriteRes{unprocessed, err}
|
||||
}()
|
||||
goroutines++
|
||||
}
|
||||
|
||||
results := make([]dynamoWriteRes, goroutines)
|
||||
for i := 0; i < goroutines; i++ {
|
||||
results[i] = <-ch
|
||||
}
|
||||
var unprocessed []types.WriteRequest
|
||||
for _, res := range results {
|
||||
if res.err != nil {
|
||||
return nil, res.err
|
||||
}
|
||||
unprocessed = append(unprocessed, res.unprocessed...)
|
||||
}
|
||||
return unprocessed, nil
|
||||
}
|
||||
|
||||
// batchWrite makes a single batch write request to DynamoDB and returns any unfulfilled write requests.
|
||||
func (c *ddbConn) batchWrite(reqs []types.WriteRequest) ([]types.WriteRequest, error) {
|
||||
out, err := c.conn.BatchWriteItem(context.Background(), &dynamodb.BatchWriteItemInput{
|
||||
RequestItems: map[string][]types.WriteRequest{c.table: reqs},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unprocessed := out.UnprocessedItems[c.table]
|
||||
metrics.IncrCounter([]string{"dynamodb", "write_capacity"}, float32(len(reqs)-len(unprocessed)))
|
||||
return unprocessed, nil
|
||||
}
|
||||
|
||||
// Clone returns a read-only copy of the DynamoDB connection.
|
||||
func (c *ddbConn) Clone() *ddbConn {
|
||||
if len(c.batch) > 0 {
|
||||
panic("no outstanding writes are allowed in a cloning ddbConn")
|
||||
}
|
||||
return &ddbConn{
|
||||
conn: c.conn,
|
||||
table: c.table,
|
||||
ch: c.ch,
|
||||
readonly: true,
|
||||
batch: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// ddbTransparencyStore implements the TransparencyStore interface over a
|
||||
// DynamoDB connection.
|
||||
type ddbTransparencyStore struct {
|
||||
conn *ddbConn
|
||||
}
|
||||
|
||||
var (
|
||||
adaptiveRetryer = retry.NewAdaptiveMode(func(opts *retry.AdaptiveModeOptions) {
|
||||
opts.StandardOptions = append(opts.StandardOptions, func(opts *retry.StandardOptions) {
|
||||
// Start with a larger token bucket and reduce the cost for non-timeout errors.
|
||||
// The default is 500 and 5, respectively.
|
||||
// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/aws/retry#StandardOptions
|
||||
opts.RateLimiter = ratelimit.NewTokenRateLimit(1000)
|
||||
opts.RetryCost = 1
|
||||
opts.MaxAttempts = 500
|
||||
opts.MaxBackoff = time.Minute * 30
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
func NewDynamoDBTransparencyStore(table string, parallel int) (TransparencyStore, error) {
|
||||
cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRetryer(func() aws.Retryer {
|
||||
return adaptiveRetryer
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ddbTransparencyStore{newDDBConn(dynamodb.NewFromConfig(cfg), table, parallel)}, nil
|
||||
}
|
||||
|
||||
// Clone returns a read-only DynamoDB connection for transparency tree data.
|
||||
func (ddb *ddbTransparencyStore) Clone() TransparencyStore {
|
||||
return &ddbTransparencyStore{ddb.conn.Clone()}
|
||||
}
|
||||
|
||||
// GetHead fetches and deserializes the latest tree head data from DynamoDB.
|
||||
func (ddb *ddbTransparencyStore) GetHead() (*TransparencyTreeHead, map[string]*AuditorTreeHead, error) {
|
||||
latest, err := ddb.conn.Get("root")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if latest == nil {
|
||||
return &TransparencyTreeHead{}, make(map[string]*AuditorTreeHead), nil
|
||||
}
|
||||
return deserializeStoredTreeHead(latest)
|
||||
}
|
||||
|
||||
// Get gets the requested transparency tree data.
|
||||
func (ddb *ddbTransparencyStore) Get(key uint64) ([]byte, error) {
|
||||
return ddb.conn.Get("t" + fmt.Sprint(key))
|
||||
}
|
||||
|
||||
// Put adds the specified transparency tree data to the map of outstanding writes.
|
||||
func (ddb *ddbTransparencyStore) Put(key uint64, data []byte) {
|
||||
ddb.conn.Put("t"+fmt.Sprint(key), data)
|
||||
}
|
||||
|
||||
// LogStore returns a DynamoDB connection for log tree data.
|
||||
func (ddb *ddbTransparencyStore) LogStore() LogStore {
|
||||
return &ddbLogStore{ddb.conn}
|
||||
}
|
||||
|
||||
// PrefixStore returns a DynamoDB connection for prefix tree data.
|
||||
func (ddb *ddbTransparencyStore) PrefixStore() PrefixStore {
|
||||
return &ddbPrefixStore{ddb.conn}
|
||||
}
|
||||
|
||||
// StreamStore returns a DynamoDB connection for stream data.
|
||||
func (ddb *ddbTransparencyStore) StreamStore() StreamStore {
|
||||
return &ddbStreamStore{ddb.conn.conn, ddb.conn.table}
|
||||
}
|
||||
|
||||
// Commit takes a transparency tree head and auditor tree head as input and commits all outstanding writes
|
||||
// to DynamoDB.
|
||||
func (ddb *ddbTransparencyStore) Commit(head *TransparencyTreeHead, auditors map[string]*AuditorTreeHead) error {
|
||||
raw, err := json.Marshal(&storedTreeHead{head, auditors})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ddb.conn.Put("root", raw)
|
||||
return ddb.conn.Commit()
|
||||
}
|
||||
|
||||
// ddbLogStore implements the LogStore interface over DynamoDB.
|
||||
type ddbLogStore struct {
|
||||
conn *ddbConn
|
||||
}
|
||||
|
||||
// BatchGet fetches the requested log tree data from DynamoDB.
|
||||
func (ls *ddbLogStore) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
sKeys := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
sKeys[i] = "l" + fmt.Sprint(key)
|
||||
}
|
||||
data, err := ls.conn.BatchGet(sKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(map[uint64][]byte)
|
||||
for i, key := range keys {
|
||||
if val, ok := data[sKeys[i]]; ok {
|
||||
out[key] = val
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// BatchPut adds the specified log tree data to the map of outstanding writes.
|
||||
func (ls *ddbLogStore) BatchPut(data map[uint64][]byte) {
|
||||
for key, value := range data {
|
||||
ls.conn.Put("l"+fmt.Sprint(key), value)
|
||||
}
|
||||
}
|
||||
|
||||
// ddbPrefixStore implements the PrefixStore interface over DynamoDB.
|
||||
type ddbPrefixStore struct {
|
||||
conn *ddbConn
|
||||
}
|
||||
|
||||
// BatchGet fetches the requested prefix tree data from DynamoDB.
|
||||
func (ps *ddbPrefixStore) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
sKeys := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
sKeys[i] = "p" + fmt.Sprint(key)
|
||||
}
|
||||
data, err := ps.conn.BatchGet(sKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(map[uint64][]byte)
|
||||
for i, key := range keys {
|
||||
if val, ok := data[sKeys[i]]; ok {
|
||||
out[key] = val
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetCached fetches the requested prefix tree data from the map of outstanding writes
|
||||
// and returns nil if the key does not exist.
|
||||
func (ps *ddbPrefixStore) GetCached(key uint64) []byte {
|
||||
if value, ok := ps.conn.batch["p"+fmt.Sprint(key)]; ok {
|
||||
return dup(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put adds the specified prefix tree data to the map of outstanding writes.
|
||||
func (ps *ddbPrefixStore) Put(key uint64, data []byte) {
|
||||
ps.conn.Put("p"+fmt.Sprint(key), data)
|
||||
}
|
||||
|
||||
// ddbStreamStore implements the StreamStore interface over DynamoDB.
|
||||
type ddbStreamStore struct {
|
||||
conn *dynamodb.Client
|
||||
table string
|
||||
}
|
||||
|
||||
// key returns a map containing a primary key for the specified shard.
|
||||
func (ss *ddbStreamStore) key(streamName, shardID string) map[string]types.AttributeValue {
|
||||
return map[string]types.AttributeValue{
|
||||
keyLabel: &types.AttributeValueMemberS{
|
||||
Value: fmt.Sprintf("stream=%v,shardID=%v", streamName, shardID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetCheckpoint returns the last sequence number stored for the specified shard, which allows the stream consumer
|
||||
// to pick up from where it left off previously in the Kinesis stream.
|
||||
func (ss *ddbStreamStore) GetCheckpoint(streamName, shardID string) (string, error) {
|
||||
consistent := true
|
||||
out, err := ss.conn.GetItem(context.Background(), &dynamodb.GetItemInput{
|
||||
Key: ss.key(streamName, shardID),
|
||||
TableName: &ss.table,
|
||||
ConsistentRead: &consistent,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
metrics.IncrCounterWithLabels(
|
||||
[]string{"dynamodb", "read_capacity"},
|
||||
1,
|
||||
[]metrics.Label{{Name: "consistent", Value: fmt.Sprint(consistent)}},
|
||||
)
|
||||
|
||||
if len(out.Item) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
value, ok := out.Item[valueLabel].(*types.AttributeValueMemberB)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("malformed database entry")
|
||||
}
|
||||
return string(value.Value), nil
|
||||
}
|
||||
|
||||
// SetCheckpoint stores a sequence number for the specified shard.
|
||||
func (ss *ddbStreamStore) SetCheckpoint(streamName, shardID string, sequenceNumber string) error {
|
||||
item := ss.key(streamName, shardID)
|
||||
item[valueLabel] = &types.AttributeValueMemberB{Value: []byte(sequenceNumber)}
|
||||
|
||||
_, err := ss.conn.PutItem(context.Background(), &dynamodb.PutItemInput{
|
||||
TableName: &ss.table,
|
||||
Item: item,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metrics.IncrCounter([]string{"dynamodb", "write_capacity"}, 1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ddbAccount fetches account info from DynamoDB
|
||||
type ddbAccount struct {
|
||||
conn *dynamodb.Client
|
||||
accountDb string
|
||||
}
|
||||
|
||||
func NewAccountDB(accountDb string) (AccountDB, error) {
|
||||
cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRetryer(func() aws.Retryer {
|
||||
return adaptiveRetryer
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ddbAccount{dynamodb.NewFromConfig(cfg), accountDb}, nil
|
||||
}
|
||||
|
||||
func (a *ddbAccount) GetAccountByAci(aci []byte) (*Account, error) {
|
||||
proj := expression.NamesList(expression.Name(attrCanonicallyDiscoverable), expression.Name(attrUnidentifiedAccessKey))
|
||||
expr, err := expression.NewBuilder().WithProjection(proj).Build()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := a.conn.GetItem(context.Background(), &dynamodb.GetItemInput{
|
||||
Key: map[string]types.AttributeValue{
|
||||
attrAci: &types.AttributeValueMemberB{Value: aci[:]},
|
||||
},
|
||||
TableName: aws.String(a.accountDb),
|
||||
ConsistentRead: aws.Bool(true),
|
||||
ExpressionAttributeNames: expr.Names(),
|
||||
ProjectionExpression: expr.Projection(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Item) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
account := Account{}
|
||||
err = attributevalue.UnmarshalMap(result.Item, &account)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
210
db/leveldb.go
Normal file
210
db/leveldb.go
Normal file
@ -0,0 +1,210 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
)
|
||||
|
||||
// ldbConn is a wrapper around a base LevelDB database that handles batching
|
||||
// writes between commits transparently.
|
||||
//
|
||||
// In this service, it is intended to be used for local development.
|
||||
type ldbConn struct {
|
||||
conn *leveldb.DB
|
||||
readonly bool
|
||||
batch map[string][]byte
|
||||
}
|
||||
|
||||
func newLDBConn(conn *leveldb.DB) *ldbConn {
|
||||
return &ldbConn{conn, false, make(map[string][]byte)}
|
||||
}
|
||||
|
||||
func (c *ldbConn) Get(key string) ([]byte, error) {
|
||||
if value, ok := c.batch[key]; ok {
|
||||
return dup(value), nil
|
||||
}
|
||||
return c.conn.Get([]byte(key), nil)
|
||||
}
|
||||
|
||||
func (c *ldbConn) Put(key string, value []byte) {
|
||||
if c.readonly {
|
||||
panic("connection is readonly")
|
||||
}
|
||||
c.batch[key] = dup(value)
|
||||
}
|
||||
|
||||
func (c *ldbConn) Commit() error {
|
||||
if c.readonly {
|
||||
panic("connection is readonly")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
c.batch = make(map[string][]byte)
|
||||
}()
|
||||
|
||||
b := new(leveldb.Batch)
|
||||
for key, value := range c.batch {
|
||||
if key == "root" {
|
||||
continue
|
||||
}
|
||||
b.Put([]byte(key), value)
|
||||
}
|
||||
if err := c.conn.Write(b, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if value, ok := c.batch["root"]; ok {
|
||||
if err := c.conn.Put([]byte("root"), value, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ldbTransparencyStore implements the TransparencyStore interface over a
|
||||
// LevelDB database.
|
||||
type ldbTransparencyStore struct {
|
||||
conn *ldbConn
|
||||
}
|
||||
|
||||
func NewLDBTransparencyStore(file string) (TransparencyStore, error) {
|
||||
conn, err := leveldb.OpenFile(file, nil)
|
||||
if errors.IsCorrupted(err) {
|
||||
conn, err = leveldb.RecoverFile(file, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ldbTransparencyStore{newLDBConn(conn)}, nil
|
||||
}
|
||||
|
||||
func (ldb *ldbTransparencyStore) Clone() TransparencyStore {
|
||||
return &ldbTransparencyStore{&ldbConn{ldb.conn.conn, true, nil}}
|
||||
}
|
||||
|
||||
func (ldb *ldbTransparencyStore) GetHead() (*TransparencyTreeHead, map[string]*AuditorTreeHead, error) {
|
||||
latest, err := ldb.conn.Get("root")
|
||||
if err == leveldb.ErrNotFound {
|
||||
return &TransparencyTreeHead{}, make(map[string]*AuditorTreeHead), nil
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return deserializeStoredTreeHead(latest)
|
||||
}
|
||||
|
||||
func (ldb *ldbTransparencyStore) Get(key uint64) ([]byte, error) {
|
||||
return ldb.conn.Get("t" + fmt.Sprint(key))
|
||||
}
|
||||
|
||||
func (ldb *ldbTransparencyStore) Put(key uint64, data []byte) {
|
||||
ldb.conn.Put("t"+fmt.Sprint(key), data)
|
||||
}
|
||||
|
||||
func (ldb *ldbTransparencyStore) LogStore() LogStore {
|
||||
return &ldbLogStore{ldb.conn}
|
||||
}
|
||||
|
||||
func (ldb *ldbTransparencyStore) PrefixStore() PrefixStore {
|
||||
return &ldbPrefixStore{ldb.conn}
|
||||
}
|
||||
|
||||
func (ldb *ldbTransparencyStore) StreamStore() StreamStore {
|
||||
return &ldbStreamStore{ldb.conn.conn}
|
||||
}
|
||||
|
||||
func (ldb *ldbTransparencyStore) Commit(head *TransparencyTreeHead, auditors map[string]*AuditorTreeHead) error {
|
||||
raw, err := json.Marshal(&storedTreeHead{head, auditors})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ldb.conn.Put("root", raw)
|
||||
return ldb.conn.Commit()
|
||||
}
|
||||
|
||||
// ldbLogStore implements the LogStore interface over LevelDB.
|
||||
type ldbLogStore struct {
|
||||
conn *ldbConn
|
||||
}
|
||||
|
||||
func (ls *ldbLogStore) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
out := make(map[uint64][]byte)
|
||||
|
||||
for _, key := range keys {
|
||||
value, err := ls.conn.Get("l" + fmt.Sprint(key))
|
||||
if err == leveldb.ErrNotFound {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (ls *ldbLogStore) BatchPut(data map[uint64][]byte) {
|
||||
for key, value := range data {
|
||||
ls.conn.Put("l"+fmt.Sprint(key), value)
|
||||
}
|
||||
}
|
||||
|
||||
// ldbPrefixStore implements the PrefixStore interface over LevelDB.
|
||||
type ldbPrefixStore struct {
|
||||
conn *ldbConn
|
||||
}
|
||||
|
||||
func (ps *ldbPrefixStore) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
out := make(map[uint64][]byte)
|
||||
|
||||
for _, key := range keys {
|
||||
value, err := ps.conn.Get("p" + fmt.Sprint(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (ps *ldbPrefixStore) GetCached(key uint64) []byte {
|
||||
if value, ok := ps.conn.batch["p"+fmt.Sprint(key)]; ok {
|
||||
return dup(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *ldbPrefixStore) Put(key uint64, data []byte) {
|
||||
ps.conn.Put("p"+fmt.Sprint(key), data)
|
||||
}
|
||||
|
||||
// ldbStreamStore implements the StreamStore interface over LevelDB.
|
||||
type ldbStreamStore struct {
|
||||
conn *leveldb.DB
|
||||
}
|
||||
|
||||
func (ss *ldbStreamStore) key(streamName, shardID string) []byte {
|
||||
return []byte(fmt.Sprintf("stream=%v,shardID=%v", streamName, shardID))
|
||||
}
|
||||
|
||||
func (ss *ldbStreamStore) GetCheckpoint(streamName, shardID string) (string, error) {
|
||||
val, err := ss.conn.Get(ss.key(streamName, shardID), nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return "", nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(val), nil
|
||||
}
|
||||
|
||||
func (ss *ldbStreamStore) SetCheckpoint(streamName, shardID, sequenceNumber string) error {
|
||||
return ss.conn.Put(ss.key(streamName, shardID), []byte(sequenceNumber), nil)
|
||||
}
|
||||
283
db/lru.go
Normal file
283
db/lru.go
Normal file
@ -0,0 +1,283 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
metrics "github.com/hashicorp/go-metrics"
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
func countCacheHit(typ string, hit bool) {
|
||||
lbls := []metrics.Label{{Name: "type", Value: typ}}
|
||||
var name []string
|
||||
if hit {
|
||||
name = []string{"lru", "cache_hit"}
|
||||
} else {
|
||||
name = []string{"lru", "cache_miss"}
|
||||
}
|
||||
metrics.IncrCounterWithLabels(name, 1, lbls)
|
||||
}
|
||||
|
||||
const (
|
||||
TransparencyCache = 1 << iota
|
||||
LogCache
|
||||
PrefixCache
|
||||
HeadCache
|
||||
)
|
||||
|
||||
type headPair struct {
|
||||
tree *TransparencyTreeHead
|
||||
auditors map[string]*AuditorTreeHead
|
||||
}
|
||||
|
||||
type cachedTransparencyStore struct {
|
||||
db TransparencyStore
|
||||
|
||||
head *atomic.Value
|
||||
topCache *lru.Cache[uint64, []byte]
|
||||
logCache *lru.Cache[uint64, []byte]
|
||||
prefixCache *lru.Cache[uint64, []byte]
|
||||
}
|
||||
|
||||
type Bitmask uint32
|
||||
|
||||
func NewCachedTransparencyStore(db TransparencyStore, cachesToEnable Bitmask, topCacheSize, logCacheSize, prefixCacheSize int) TransparencyStore {
|
||||
cache := &cachedTransparencyStore{db: db}
|
||||
|
||||
var err error
|
||||
if cachesToEnable&TransparencyCache != 0 {
|
||||
cache.topCache, err = lru.New[uint64, []byte](topCacheSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if cachesToEnable&LogCache != 0 {
|
||||
cache.logCache, err = lru.New[uint64, []byte](logCacheSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if cachesToEnable&PrefixCache != 0 {
|
||||
cache.prefixCache, err = lru.New[uint64, []byte](prefixCacheSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if cachesToEnable&HeadCache != 0 {
|
||||
cache.head = &atomic.Value{}
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
func (c *cachedTransparencyStore) Clone() TransparencyStore {
|
||||
return &cachedTransparencyStore{
|
||||
db: c.db.Clone(),
|
||||
|
||||
head: c.head,
|
||||
topCache: c.topCache,
|
||||
logCache: c.logCache,
|
||||
prefixCache: c.prefixCache,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cachedTransparencyStore) GetHead() (*TransparencyTreeHead, map[string]*AuditorTreeHead, error) {
|
||||
if c.head != nil {
|
||||
if head, ok := c.head.Load().(headPair); ok {
|
||||
countCacheHit("head", true)
|
||||
return head.tree.Clone(), cloneAuditorTreeHeadMap(head.auditors), nil
|
||||
}
|
||||
countCacheHit("head", false)
|
||||
}
|
||||
|
||||
head, auditors, err := c.db.GetHead()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if c.head != nil {
|
||||
c.head.Store(headPair{head.Clone(), cloneAuditorTreeHeadMap(auditors)})
|
||||
}
|
||||
|
||||
return head, auditors, nil
|
||||
}
|
||||
|
||||
func (c *cachedTransparencyStore) Get(key uint64) ([]byte, error) {
|
||||
if c.topCache != nil {
|
||||
if val, ok := c.topCache.Get(key); ok {
|
||||
countCacheHit("transparency", true)
|
||||
return dup(val), nil
|
||||
}
|
||||
countCacheHit("transparency", false)
|
||||
}
|
||||
|
||||
val, err := c.db.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.topCache != nil {
|
||||
c.topCache.ContainsOrAdd(key, dup(val))
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (c *cachedTransparencyStore) Put(key uint64, data []byte) {
|
||||
if c.topCache != nil {
|
||||
c.topCache.Add(key, dup(data))
|
||||
}
|
||||
c.db.Put(key, data)
|
||||
}
|
||||
|
||||
func (c *cachedTransparencyStore) LogStore() LogStore {
|
||||
return &cachedLogStore{
|
||||
db: c.db.LogStore(),
|
||||
cache: c.logCache,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cachedTransparencyStore) PrefixStore() PrefixStore {
|
||||
return &cachedPrefixStore{
|
||||
db: c.db.PrefixStore(),
|
||||
cache: c.prefixCache,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cachedTransparencyStore) StreamStore() StreamStore { return c.db.StreamStore() }
|
||||
|
||||
func (c *cachedTransparencyStore) Commit(head *TransparencyTreeHead, auditors map[string]*AuditorTreeHead) error {
|
||||
err := c.db.Commit(head, auditors)
|
||||
if err == nil && c.head != nil {
|
||||
c.head.Store(headPair{head.Clone(), cloneAuditorTreeHeadMap(auditors)})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type cachedLogStore struct {
|
||||
db LogStore
|
||||
cache *lru.Cache[uint64, []byte]
|
||||
}
|
||||
|
||||
func (c *cachedLogStore) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
remaining := make([]uint64, 0)
|
||||
data := make(map[uint64][]byte)
|
||||
|
||||
if c.cache != nil {
|
||||
for _, key := range keys {
|
||||
if val, ok := c.cache.Get(key); ok {
|
||||
countCacheHit("log", true)
|
||||
data[key] = dup(val)
|
||||
} else {
|
||||
countCacheHit("log", false)
|
||||
remaining = append(remaining, key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remaining = keys
|
||||
}
|
||||
|
||||
if len(remaining) > 0 {
|
||||
partial, err := c.db.BatchGet(remaining)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var readonly bool
|
||||
switch ls := c.db.(type) {
|
||||
case *ldbLogStore:
|
||||
readonly = ls.conn.readonly
|
||||
case *ddbLogStore:
|
||||
readonly = ls.conn.readonly
|
||||
default:
|
||||
readonly = true
|
||||
}
|
||||
|
||||
for key, val := range partial {
|
||||
// Only cache fully-formed trees, or trees from a write-enabled connection
|
||||
// because it does not allow concurrent reads and writes.
|
||||
cacheable := len(val) == 256 || !readonly
|
||||
if cacheable && c.cache != nil {
|
||||
c.cache.ContainsOrAdd(key, dup(val))
|
||||
}
|
||||
data[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *cachedLogStore) BatchPut(data map[uint64][]byte) {
|
||||
if c.cache != nil {
|
||||
for key, val := range data {
|
||||
c.cache.Add(key, dup(val))
|
||||
}
|
||||
}
|
||||
c.db.BatchPut(data)
|
||||
}
|
||||
|
||||
type cachedPrefixStore struct {
|
||||
db PrefixStore
|
||||
cache *lru.Cache[uint64, []byte]
|
||||
}
|
||||
|
||||
func (c *cachedPrefixStore) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
remaining := make([]uint64, 0)
|
||||
data := make(map[uint64][]byte)
|
||||
|
||||
if c.cache != nil {
|
||||
for _, key := range keys {
|
||||
if val, ok := c.cache.Get(key); ok {
|
||||
countCacheHit("prefix", true)
|
||||
data[key] = dup(val)
|
||||
} else {
|
||||
countCacheHit("prefix", false)
|
||||
remaining = append(remaining, key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remaining = keys
|
||||
}
|
||||
|
||||
if len(remaining) > 0 {
|
||||
partial, err := c.db.BatchGet(remaining)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, val := range partial {
|
||||
if c.cache != nil {
|
||||
c.cache.ContainsOrAdd(key, dup(val))
|
||||
}
|
||||
data[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *cachedPrefixStore) GetCached(key uint64) []byte {
|
||||
if c.cache != nil {
|
||||
if val, ok := c.cache.Get(key); ok {
|
||||
countCacheHit("prefix", true)
|
||||
return dup(val)
|
||||
}
|
||||
countCacheHit("prefix", false)
|
||||
}
|
||||
|
||||
return c.db.GetCached(key)
|
||||
}
|
||||
|
||||
func (c *cachedPrefixStore) Put(key uint64, data []byte) {
|
||||
if c.cache != nil {
|
||||
c.cache.Add(key, dup(data))
|
||||
}
|
||||
c.db.Put(key, data)
|
||||
}
|
||||
47
db/lru_test.go
Normal file
47
db/lru_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testNewCachedTransparencyStore = []struct {
|
||||
cachesToEnable Bitmask
|
||||
expectPrefixCache bool
|
||||
expectLogCache bool
|
||||
expectTopCache bool
|
||||
expectHeadCache bool
|
||||
}{
|
||||
{PrefixCache, true, false, false, false},
|
||||
{TransparencyCache | PrefixCache | LogCache | HeadCache, true, true, true, true},
|
||||
{TransparencyCache | PrefixCache | LogCache, true, true, true, false},
|
||||
}
|
||||
|
||||
func TestNewCachedTransparencyStore(t *testing.T) {
|
||||
mockDb := NewMemoryTransparencyStore()
|
||||
|
||||
for _, p := range testNewCachedTransparencyStore {
|
||||
cachedStore := NewCachedTransparencyStore(mockDb, p.cachesToEnable, 2000, 2000, 20000).(*cachedTransparencyStore)
|
||||
|
||||
if p.expectPrefixCache != (cachedStore.prefixCache != nil) {
|
||||
t.Fatalf("Expect prefix cache: %v, cachedStore.prefixCache != nil is %v", p.expectPrefixCache, cachedStore.prefixCache != nil)
|
||||
}
|
||||
|
||||
if p.expectLogCache != (cachedStore.logCache != nil) {
|
||||
t.Fatalf("Expect log cache: %v, cachedStore.logCache != nil is %v", p.expectLogCache, cachedStore.logCache != nil)
|
||||
}
|
||||
|
||||
if p.expectTopCache != (cachedStore.topCache != nil) {
|
||||
t.Fatalf("Expect top cache: %v, cachedStore.topCache != nil is %v", p.expectTopCache, cachedStore.topCache != nil)
|
||||
}
|
||||
|
||||
if p.expectHeadCache != (cachedStore.head != nil) {
|
||||
t.Fatalf("Expect head cache: %v, cachedStore.head != nil is %v", p.expectHeadCache, cachedStore.head != nil)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
106
db/memory.go
Normal file
106
db/memory.go
Normal file
@ -0,0 +1,106 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import "errors"
|
||||
|
||||
type memoryTransparencyStore struct {
|
||||
latest *TransparencyTreeHead
|
||||
auditors map[string]*AuditorTreeHead
|
||||
top map[uint64][]byte
|
||||
log map[uint64][]byte
|
||||
prefix map[uint64][]byte
|
||||
}
|
||||
|
||||
func NewMemoryTransparencyStore() TransparencyStore {
|
||||
return &memoryTransparencyStore{
|
||||
latest: &TransparencyTreeHead{},
|
||||
top: make(map[uint64][]byte),
|
||||
log: make(map[uint64][]byte),
|
||||
prefix: make(map[uint64][]byte),
|
||||
auditors: make(map[string]*AuditorTreeHead),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memoryTransparencyStore) Clone() TransparencyStore { panic("not implemented") }
|
||||
|
||||
func (m *memoryTransparencyStore) GetHead() (*TransparencyTreeHead, map[string]*AuditorTreeHead, error) {
|
||||
return m.latest, m.auditors, nil
|
||||
}
|
||||
|
||||
func (m *memoryTransparencyStore) Get(key uint64) ([]byte, error) {
|
||||
return m.top[key], nil
|
||||
}
|
||||
|
||||
func (m *memoryTransparencyStore) Put(key uint64, data []byte) {
|
||||
m.top[key] = data
|
||||
}
|
||||
|
||||
func (m *memoryTransparencyStore) LogStore() LogStore {
|
||||
return &memoryLogStore{data: m.log}
|
||||
}
|
||||
|
||||
func (m *memoryTransparencyStore) PrefixStore() PrefixStore {
|
||||
return &memoryPrefixStore{data: m.prefix}
|
||||
}
|
||||
|
||||
func (m *memoryTransparencyStore) StreamStore() StreamStore { panic("not implemented") }
|
||||
|
||||
func (m *memoryTransparencyStore) Commit(head *TransparencyTreeHead, auditors map[string]*AuditorTreeHead) error {
|
||||
m.latest = head
|
||||
m.auditors = auditors
|
||||
return nil
|
||||
}
|
||||
|
||||
type memoryLogStore struct {
|
||||
data map[uint64][]byte
|
||||
}
|
||||
|
||||
func (m *memoryLogStore) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
out := make(map[uint64][]byte)
|
||||
|
||||
for _, key := range keys {
|
||||
if d, ok := m.data[key]; ok {
|
||||
out[key] = d
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *memoryLogStore) BatchPut(data map[uint64][]byte) {
|
||||
for key, d := range data {
|
||||
buf := make([]byte, len(d))
|
||||
copy(buf, d)
|
||||
m.data[key] = buf
|
||||
}
|
||||
}
|
||||
|
||||
type memoryPrefixStore struct {
|
||||
data map[uint64][]byte
|
||||
}
|
||||
|
||||
func (m *memoryPrefixStore) BatchGet(keys []uint64) (map[uint64][]byte, error) {
|
||||
out := make(map[uint64][]byte)
|
||||
|
||||
for _, key := range keys {
|
||||
value, ok := m.data[key]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *memoryPrefixStore) GetCached(key uint64) []byte { return nil }
|
||||
|
||||
func (m *memoryPrefixStore) Put(key uint64, data []byte) {
|
||||
buf := make([]byte, len(data))
|
||||
copy(buf, data)
|
||||
m.data[key] = buf
|
||||
}
|
||||
16
db/mock_account_db.go
Normal file
16
db/mock_account_db.go
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
var (
|
||||
UnidentifiedAccessKey = []byte("unidentifiedAccessKey")
|
||||
)
|
||||
|
||||
type MockAccountDB struct{}
|
||||
|
||||
func (m *MockAccountDB) GetAccountByAci(aci []byte) (*Account, error) {
|
||||
return &Account{DiscoverableByPhoneNumber: true, UnidentifiedAccessKey: UnidentifiedAccessKey}, nil
|
||||
}
|
||||
16
db/shared.go
Normal file
16
db/shared.go
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func deserializeStoredTreeHead(raw []byte) (*TransparencyTreeHead, map[string]*AuditorTreeHead, error) {
|
||||
data := &storedTreeHead{}
|
||||
if err := json.Unmarshal(raw, data); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return data.TreeHead, data.AuditorHeads, nil
|
||||
}
|
||||
63
db/shared_test.go
Normal file
63
db/shared_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/kinbiko/jsonassert"
|
||||
)
|
||||
|
||||
func TestSerializeNewStoredTreeHead(t *testing.T) {
|
||||
// The new stored tree head no longer populates its AuditorHead or TreeHead.Signature fields
|
||||
newStoredTreeHead := &storedTreeHead{
|
||||
TreeHead: &TransparencyTreeHead{
|
||||
TreeSize: 123,
|
||||
Timestamp: 123456,
|
||||
//Signature: random(32),
|
||||
Signatures: []*Signature{
|
||||
{random(32), random(32)},
|
||||
},
|
||||
},
|
||||
//AuditorHead: &AuditorTreeHead{},
|
||||
AuditorHeads: map[string]*AuditorTreeHead{
|
||||
"example-auditor": {
|
||||
AuditorTransparencyTreeHead: AuditorTransparencyTreeHead{
|
||||
TreeSize: 234,
|
||||
Timestamp: 234567,
|
||||
Signature: random(32),
|
||||
},
|
||||
RootValue: random(32),
|
||||
Consistency: [][]byte{random(32), random(32)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(newStoredTreeHead)
|
||||
if err != nil {
|
||||
t.Fatal("expected no error marshaling new stored tree head")
|
||||
}
|
||||
|
||||
actualJsonString := string(bytes)
|
||||
expectedJsonString := `{
|
||||
"head": {"n": 123, "ts": 123456, "sigs": "<<PRESENCE>>"},
|
||||
"auditor-heads": {
|
||||
"example-auditor": {"n": 234, "ts": 234567, "sig": "<<PRESENCE>>", "root": "<<PRESENCE>>", "consistency": "<<PRESENCE>>"}
|
||||
}}`
|
||||
|
||||
// go 1.24 requires a constant format string to Printf-like functions
|
||||
jsonassert.New(t).Assertf(actualJsonString, "%s", expectedJsonString)
|
||||
}
|
||||
|
||||
func random(numBytes int) []byte {
|
||||
out := make([]byte, numBytes)
|
||||
if _, err := rand.Read(out); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
15
db/util.go
Normal file
15
db/util.go
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package db
|
||||
|
||||
func dup(in []byte) []byte {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]byte, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
}
|
||||
20
docker-local/README.md
Normal file
20
docker-local/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# docker-local
|
||||
|
||||
Docker Compose configuration to support local testing with AWS services.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker
|
||||
|
||||
## Usage
|
||||
|
||||
1. Start the DynamoDB container in the background: `docker compose -f docker-local/docker-compose.yml up -d`
|
||||
2. Update config.yaml for DynamoDB
|
||||
- `db.table: kt_local`
|
||||
- `db.parallel: 2`
|
||||
3. Run the server
|
||||
- `AWS_ACCESS_KEY_ID=local AWS_SECRET_ACCESS_KEY=local AWS_ENDPOINT_URL=http://localhost:8000 AWS_REGION=local-kt go run github.com/signalapp/keytransparency/cmd/kt-server -config ./example/config.yaml `
|
||||
4. Test with kt-client
|
||||
5. Stop the server
|
||||
6. Stop the container
|
||||
- `docker compose -f docker-local/docker-compose.yml down`
|
||||
26
docker-local/docker-compose.yml
Normal file
26
docker-local/docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
||||
name: kt-dynamodb
|
||||
services:
|
||||
dynamodb:
|
||||
image: amazon/dynamodb-local:2.5.2
|
||||
ports:
|
||||
- "8000:8000"
|
||||
dynamodb-init:
|
||||
image: amazon/aws-cli:latest
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: local
|
||||
AWS_SECRET_ACCESS_KEY: local
|
||||
AWS_ENDPOINT_URL: http://dynamodb:8000
|
||||
AWS_REGION: local-kt
|
||||
command:
|
||||
- dynamodb
|
||||
- create-table
|
||||
- --table-name
|
||||
- kt_local
|
||||
- --attribute-definitions
|
||||
- AttributeName=k,AttributeType=S
|
||||
- --key-schema
|
||||
- AttributeName=k,KeyType=HASH
|
||||
- --billing-mode
|
||||
- PAY_PER_REQUEST
|
||||
restart: on-failure
|
||||
|
||||
8
docker/Dockerfile
Normal file
8
docker/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
ARG GO_VERSION
|
||||
FROM --platform=linux/amd64 golang:${GO_VERSION}-alpine AS build
|
||||
COPY . /src
|
||||
RUN cd /src/cmd/kt-server && go build
|
||||
|
||||
FROM --platform=linux/amd64 alpine:latest AS run
|
||||
COPY --from=build /src/cmd/kt-server/kt-server /bin/kt-server
|
||||
ENTRYPOINT ["/bin/kt-server"]
|
||||
BIN
docs/PersistentSearchTree.png
Normal file
BIN
docs/PersistentSearchTree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/StratumDepth3.png
Normal file
BIN
docs/StratumDepth3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
44
docs/database.md
Normal file
44
docs/database.md
Normal file
@ -0,0 +1,44 @@
|
||||
Database Formats
|
||||
----------------
|
||||
|
||||
This document briefly describes the structure of data that the Key Transparency Server
|
||||
stores in its database. This server is designed to be run on a key-value database,
|
||||
which may be eventually consistent.
|
||||
|
||||
|
||||
### Log Tree
|
||||
|
||||
The log tree is stored in the database in "chunks", which are 8-node-wide (or
|
||||
4-node-deep) subtrees. Chunks are addressed by the id of the root node in the
|
||||
chunk. Only the leaf values of each chunk are stored, which in the context of
|
||||
the full tree is either a leaf or a cached intermediate hash. These values are
|
||||
stored concatenated. Intermediate values of a chunk are recomputed from the leaf
|
||||
nodes as needed.
|
||||
|
||||

|
||||
|
||||
Once a chunk is full, it will never be modified again. However the chunks that
|
||||
represent the "frontier" of the log will be modified to append new hash values
|
||||
as the tree grows. Because the hashes in a chunk that represent the frontier of
|
||||
the log are subject to change, they're explicitly not used when computing things
|
||||
like the root of the log.
|
||||
|
||||
### Prefix Tree
|
||||
|
||||
The prefix tree is designed to follow a log structure, where each modification
|
||||
to the prefix tree produces a new entry in the database addressed by a counter.
|
||||
Log entries contain the key that was modified by this request and pointers to
|
||||
the log entries on the key's copath, among other data.
|
||||
|
||||

|
||||
|
||||
Once an entry is created, it will never be modified again.
|
||||
|
||||
### Consistency
|
||||
|
||||
All reads to the database are controlled by the "tree size" parameter of the
|
||||
most recent tree head. The server only accesses entries in the database that it
|
||||
expects to exist, and that it expects to be done changing, based on the given
|
||||
tree size. As long as the database prevents a server from observing a tree head
|
||||
before the server is also able to observe the tree data associated with that
|
||||
tree head, operations should be able to proceed as expected.
|
||||
48
example/config.yaml
Normal file
48
example/config.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
kt:
|
||||
server-addr: localhost:8082
|
||||
authorized-headers:
|
||||
ExampleHeader1:
|
||||
- example value one
|
||||
- example value two
|
||||
header-value-to-auditor-name:
|
||||
example value one: example-auditor-1
|
||||
example value two: example-auditor-2
|
||||
|
||||
kt-query:
|
||||
server-addr: localhost:8080
|
||||
# at least one header-value mapping in this map will be required to be present on inbound requests with the configured value
|
||||
authorized-headers:
|
||||
ExampleHeader1:
|
||||
- example value one
|
||||
- example value two
|
||||
header-value-to-auditor-name:
|
||||
example value one: example-auditor-1
|
||||
example value two: example-auditor-2
|
||||
|
||||
kt-test:
|
||||
server-addr: localhost:8081
|
||||
|
||||
metrics-addr: localhost:8083
|
||||
health-addr: localhost:8084
|
||||
|
||||
# Paste in the keys generated via `go run github.com/signalapp/keytransparency/cmd/generate-keys`
|
||||
api:
|
||||
signing-key: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
vrf-key: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
prefix-key: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
opening-key: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
# fake:
|
||||
# count: 1
|
||||
# interval: 10s
|
||||
distinguished: 1m
|
||||
auditors:
|
||||
example-auditor-1: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
example-auditor-2: abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcd
|
||||
min-search-delay: 1s
|
||||
min-monitor-delay: 1s
|
||||
jitter-percent: 10
|
||||
|
||||
db:
|
||||
file: example/db
|
||||
|
||||
account-db: mock
|
||||
1396
filter-key-updates/.editorconfig
Normal file
1396
filter-key-updates/.editorconfig
Normal file
File diff suppressed because it is too large
Load Diff
2
filter-key-updates/.gitignore
vendored
Normal file
2
filter-key-updates/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.idea
|
||||
target
|
||||
9
filter-key-updates/.mvn/extensions.xml
Normal file
9
filter-key-updates/.mvn/extensions.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
|
||||
<extension>
|
||||
<groupId>fr.brouillard.oss</groupId>
|
||||
<artifactId>jgitver-maven-plugin</artifactId>
|
||||
<version>1.9.0</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
14
filter-key-updates/.mvn/jgitver.config.xml
Normal file
14
filter-key-updates/.mvn/jgitver.config.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<configuration xmlns="http://jgitver.github.io/maven/configuration/1.1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://jgitver.github.io/maven/configuration/1.1.0 https://jgitver.github.io/maven/configuration/jgitver-configuration-v1_1_0.xsd">
|
||||
<useDirty>true</useDirty>
|
||||
<useDefaultBranchingPolicy>false</useDefaultBranchingPolicy>
|
||||
<branchPolicies>
|
||||
<branchPolicy>
|
||||
<pattern>(.*)</pattern>
|
||||
<transformations>
|
||||
<transformation>IGNORE</transformation>
|
||||
</transformations>
|
||||
</branchPolicy>
|
||||
</branchPolicies>
|
||||
</configuration>
|
||||
BIN
filter-key-updates/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
filter-key-updates/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
20
filter-key-updates/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
20
filter-key-updates/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
||||
distributionSha256Sum=e896b60329a71b719d77bb4388b251a50aebcd73c62f69d510c858ce360afe0f
|
||||
wrapperSha256Sum=e63a53cfb9c4d291ebe3c2b0edacb7622bbc480326beaa5a0456e412f52f066a
|
||||
308
filter-key-updates/mvnw
vendored
Executable file
308
filter-key-updates/mvnw
vendored
Executable file
@ -0,0 +1,308 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
fi
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
# Remove \r in case we run on Windows within Git Bash
|
||||
# and check out the repository with auto CRLF management
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
log() {
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
else
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
fi
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
else
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c - > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
exit 1
|
||||
fi
|
||||
if [ $wrapperSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
# shellcheck disable=SC2086 # safe args
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
205
filter-key-updates/mvnw.cmd
vendored
Normal file
205
filter-key-updates/mvnw.cmd
vendored
Normal file
@ -0,0 +1,205 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath %WRAPPER_JAR% ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||
|
||||
cmd /C exit /B %ERROR_CODE%
|
||||
202
filter-key-updates/pom.xml
Normal file
202
filter-key-updates/pom.xml
Normal file
@ -0,0 +1,202 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>filter-key-updates-lambda</artifactId>
|
||||
<version>JGITVER</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<junit.version>5.13.1</junit.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>bom</artifactId>
|
||||
<version>2.31.69</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-core</artifactId>
|
||||
<version>1.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-events</artifactId>
|
||||
<version>3.16.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>kinesis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- resolve a dependency resolution conflict by preferring the newer, direct,
|
||||
versions from software.amazon.awssdk:apache-client. When apache-client updates the httpclient version,
|
||||
dependency convergence will fail, and we can re-evaluate these exclusions.
|
||||
-->
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.13</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<!--
|
||||
+-org.signal:filter-key-updates-lambda:jar
|
||||
+-software.amazon.awssdk:kinesis:jar:2.31.18:compile
|
||||
+-software.amazon.awssdk:apache-client:jar:2.31.18:runtime
|
||||
+-org.apache.httpcomponents:httpclient:jar:4.5.13:runtime
|
||||
+-org.apache.httpcomponents:httpcore:jar:4.4.13:runtime
|
||||
and
|
||||
+-org.signal:filter-key-updates-lambda:jar
|
||||
+-software.amazon.awssdk:kinesis:jar:2.31.18:compile
|
||||
+-software.amazon.awssdk:apache-client:jar:2.31.18:runtime
|
||||
+-org.apache.httpcomponents:httpcore:jar:4.4.16:runtime
|
||||
-->
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<!--
|
||||
+-org.signal:filter-key-updates-lambda
|
||||
+-software.amazon.awssdk:kinesis:2.31.18
|
||||
+-software.amazon.awssdk:apache-client:2.31.18
|
||||
+-org.apache.httpcomponents:httpclient:4.5.13
|
||||
+-commons-codec:commons-codec:1.11
|
||||
and
|
||||
+-org.signal:filter-key-updates-lambda
|
||||
+-software.amazon.awssdk:kinesis:2.31.18
|
||||
+-software.amazon.awssdk:apache-client:2.31.18
|
||||
+-commons-codec:commons-codec:1.17.1
|
||||
-->
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.19.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.4.8-jre</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.18.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-lambda-java-tests</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.bazaarvoice.maven.plugins</groupId>
|
||||
<artifactId>s3-upload-maven-plugin</artifactId>
|
||||
<version>2.0.3</version>
|
||||
<configuration>
|
||||
<source>${project.build.directory}/${project.build.finalName}.jar</source>
|
||||
<bucketName>${bucketName}</bucketName>
|
||||
<destination>${bucketKey}/${project.build.finalName}.jar</destination>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>deploy-to-s3</id>
|
||||
<phase>deploy</phase>
|
||||
<goals>
|
||||
<goal>s3-upload</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<dependencyConvergence/>
|
||||
<requireMavenVersion>
|
||||
<version>3.9.4</version>
|
||||
</requireMavenVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>3.1.4</version>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
111
filter-key-updates/src/main/java/org/signal/lambda/Account.java
Normal file
111
filter-key-updates/src/main/java/org/signal/lambda/Account.java
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.lambda;
|
||||
|
||||
import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
record Account(
|
||||
String number,
|
||||
byte[] aci,
|
||||
byte[] aciIdentityKey,
|
||||
byte[] usernameHash) {
|
||||
|
||||
@VisibleForTesting
|
||||
static final String KEY_ACCOUNT_UUID = "U";
|
||||
@VisibleForTesting
|
||||
static final String ATTR_ACCOUNT_E164 = "P";
|
||||
@VisibleForTesting
|
||||
static final String ATTR_ACCOUNT_USERNAME_HASH = "N";
|
||||
@VisibleForTesting
|
||||
static final String ATTR_ACCOUNT_DATA = "D";
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
Account account = (Account) o;
|
||||
return Objects.equals(number, account.number) &&
|
||||
Arrays.equals(aci, account.aci) &&
|
||||
Arrays.equals(aciIdentityKey, account.aciIdentityKey) &&
|
||||
Arrays.equals(usernameHash, account.usernameHash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Account{" +
|
||||
"number='" + number + '\'' +
|
||||
", aci=" + Arrays.toString(aci) +
|
||||
", aciIdentityKey=" + Arrays.toString(aciIdentityKey) +
|
||||
", usernameHash=" + Arrays.toString(usernameHash) +
|
||||
'}';
|
||||
}
|
||||
|
||||
public record Pair(Account prev, Account next) {
|
||||
|
||||
/** Return a partition key used by Kinesis to group distributed updates.
|
||||
*
|
||||
* @return a string that Kinesis uses to group distributed updates. If two Pairs have the same key,
|
||||
* their updates will go into the same kinesis shard, so their ordering will be maintained. We simply
|
||||
* make the partition key reliant on the ACI, such that updates to the same account (and thus the same ACI)
|
||||
* are ordered. Updates to different ACIs may go to different shards in the case where our Kinesis output
|
||||
* is sharded, and ordering across shards cannot be guaranteed.
|
||||
*/
|
||||
public String partitionKey() {
|
||||
final byte[] aci = prev != null ? prev.aci : next.aci;
|
||||
return HexFormat.of().formatHex(aci, 0, 4);
|
||||
}
|
||||
}
|
||||
|
||||
/** Private class used to parse the dynamodb 'D' field containing a base64 encoded JSON with account data in it. */
|
||||
private record AccountData(String identityKey) { }
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
|
||||
|
||||
static Account fromItem(Map<String, AttributeValue> item) {
|
||||
Preconditions.checkNotNull(item.get(KEY_ACCOUNT_UUID));
|
||||
Preconditions.checkNotNull(item.get(ATTR_ACCOUNT_E164));
|
||||
Preconditions.checkNotNull(item.get(ATTR_ACCOUNT_DATA));
|
||||
final byte[] uuid = new byte[16];
|
||||
item.get(KEY_ACCOUNT_UUID).getB().get(uuid);
|
||||
final ByteBuffer data = item.get(ATTR_ACCOUNT_DATA).getB().asReadOnlyBuffer();
|
||||
final byte[] identityKey;
|
||||
try {
|
||||
identityKey = Base64.getDecoder()
|
||||
.decode(objectMapper.readValue(new ByteBufferInputStream(data), AccountData.class).identityKey);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("IOException from reading bytes array", e);
|
||||
}
|
||||
final String number = item.get(ATTR_ACCOUNT_E164).getS();
|
||||
final byte[] usernameHash;
|
||||
final AttributeValue usernameHashAv = item.get(ATTR_ACCOUNT_USERNAME_HASH);
|
||||
if (usernameHashAv != null) {
|
||||
usernameHash = new byte[32];
|
||||
usernameHashAv.getB().asReadOnlyBuffer().get(usernameHash);
|
||||
} else {
|
||||
usernameHash = null;
|
||||
}
|
||||
return new Account(
|
||||
number,
|
||||
uuid,
|
||||
identityKey,
|
||||
usernameHash);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.lambda;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ByteBufferInputStream extends InputStream {
|
||||
|
||||
public ByteBufferInputStream(ByteBuffer buf) {
|
||||
this.buf = buf.slice();
|
||||
}
|
||||
|
||||
private final ByteBuffer buf;
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (!buf.hasRemaining())
|
||||
return -1;
|
||||
return buf.get() & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int offset, int length) throws IOException {
|
||||
if (length > buf.remaining()) length = buf.remaining();
|
||||
buf.get(b, offset, length);
|
||||
return length;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.lambda;
|
||||
|
||||
import com.amazonaws.services.lambda.runtime.Context;
|
||||
import com.amazonaws.services.lambda.runtime.RequestHandler;
|
||||
import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
|
||||
import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse;
|
||||
import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue;
|
||||
import com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import software.amazon.awssdk.core.SdkBytes;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.kinesis.KinesisClient;
|
||||
import software.amazon.awssdk.services.kinesis.model.PutRecordRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Filters DynamoDb record updates for the subset relevant to key transparency, outputting them to Kinesis
|
||||
*/
|
||||
public class FilterKTUpdatesHandler implements RequestHandler<DynamodbEvent, Serializable> {
|
||||
|
||||
private static final String KINESIS_OUTPUT_STREAM_ENVIRONMENT_VARIABLE = "KINESIS_OUTPUT_STREAM";
|
||||
private static final String KINESIS_OUTPUT_REGION_ENVIRONMENT_VARIABLE = "KINESIS_OUTPUT_REGION";
|
||||
|
||||
@VisibleForTesting
|
||||
static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
private final KinesisClient kinesisClient;
|
||||
private final String kinesisOutputStream;
|
||||
|
||||
public FilterKTUpdatesHandler() {
|
||||
this(KinesisClient.builder()
|
||||
.region(Region.of(System.getenv(KINESIS_OUTPUT_REGION_ENVIRONMENT_VARIABLE)))
|
||||
.build(),
|
||||
System.getenv(KINESIS_OUTPUT_STREAM_ENVIRONMENT_VARIABLE));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
FilterKTUpdatesHandler(KinesisClient kinesisClient, String outputStream) {
|
||||
this.kinesisClient = kinesisClient;
|
||||
this.kinesisOutputStream = outputStream;
|
||||
}
|
||||
|
||||
// https://docs.aws.amazon.com/lambda/latest/dg/with-ddb-create-package.html
|
||||
@Override
|
||||
public StreamsEventResponse handleRequest(final DynamodbEvent dbUpdate, final Context context) {
|
||||
List<StreamsEventResponse.BatchItemFailure> batchItemFailures = new ArrayList<>();
|
||||
String curRecordSequenceNumber = "";
|
||||
|
||||
for (DynamodbEvent.DynamodbStreamRecord record : dbUpdate.getRecords()) {
|
||||
StreamRecord dbRecord = record.getDynamodb();
|
||||
curRecordSequenceNumber = dbRecord.getSequenceNumber();
|
||||
try {
|
||||
processRecord(dbRecord);
|
||||
} catch (Exception e) {
|
||||
batchItemFailures.add(new StreamsEventResponse.BatchItemFailure(curRecordSequenceNumber));
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return new StreamsEventResponse(batchItemFailures);
|
||||
}
|
||||
|
||||
// Modeled after https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/kds_gettingstarted.html
|
||||
@VisibleForTesting
|
||||
void processRecord(StreamRecord dbRecord) throws IOException {
|
||||
Account.Pair update = dbUpdateFor(dbRecord);
|
||||
if (update == null) return;
|
||||
kinesisClient.putRecord(PutRecordRequest
|
||||
.builder()
|
||||
.data(SdkBytes.fromByteArray(OBJECT_MAPPER.writeValueAsBytes(update)))
|
||||
.partitionKey(update.partitionKey())
|
||||
.streamName(kinesisOutputStream)
|
||||
.build());
|
||||
}
|
||||
|
||||
private Account.Pair dbUpdateFor(StreamRecord dbRecord) {
|
||||
Map<String, AttributeValue> oldImage = dbRecord.getOldImage();
|
||||
Map<String, AttributeValue> newImage = dbRecord.getNewImage();
|
||||
if (oldImage == null || oldImage.isEmpty()) {
|
||||
return new Account.Pair(null, Account.fromItem(newImage));
|
||||
} else if (newImage == null || newImage.isEmpty()) {
|
||||
return new Account.Pair(Account.fromItem(oldImage), null);
|
||||
}
|
||||
Account oldAccount = Account.fromItem(oldImage);
|
||||
Account newAccount = Account.fromItem(newImage);
|
||||
if (oldAccount.equals(newAccount)) {
|
||||
return null;
|
||||
} else {
|
||||
return new Account.Pair(oldAccount, newAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.lambda;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ByteBufferInputStreamTest {
|
||||
@Test
|
||||
void testReadByte() throws Exception {
|
||||
ByteBuffer b = ByteBuffer.allocate(4);
|
||||
b.put(new byte[]{1,2,3,4});
|
||||
ByteBufferInputStream bbis = new ByteBufferInputStream(b.flip().asReadOnlyBuffer());
|
||||
assertEquals(1, bbis.read());
|
||||
assertEquals(2, bbis.read());
|
||||
assertEquals(3, bbis.read());
|
||||
assertEquals(4, bbis.read());
|
||||
assertEquals(-1, bbis.read());
|
||||
}
|
||||
@Test
|
||||
void testReadByteArray() throws Exception {
|
||||
ByteBuffer b = ByteBuffer.allocate(4);
|
||||
b.put(new byte[]{1,2,3,4});
|
||||
ByteBufferInputStream bbis = new ByteBufferInputStream(b.flip().asReadOnlyBuffer());
|
||||
byte output[] = new byte[10];
|
||||
assertEquals(4, bbis.read(output));
|
||||
assertArrayEquals(new byte[]{1,2,3,4,0,0,0,0,0,0}, output);
|
||||
assertEquals(0, bbis.read(output));
|
||||
}
|
||||
@Test
|
||||
void testReadByteArrayOffsetLength() throws Exception {
|
||||
ByteBuffer b = ByteBuffer.allocate(4);
|
||||
b.put(new byte[]{1,2,3,4});
|
||||
ByteBufferInputStream bbis = new ByteBufferInputStream(b.flip().asReadOnlyBuffer());
|
||||
byte output[] = new byte[10];
|
||||
assertEquals(2, bbis.read(output, 2, 2));
|
||||
assertEquals(2, bbis.read(output, 4, 6));
|
||||
assertArrayEquals(new byte[]{0,0,1,2,3,4,0,0,0,0}, output);
|
||||
assertEquals(0, bbis.read(output));
|
||||
}
|
||||
@Test
|
||||
void testReadNegativeByte() throws Exception {
|
||||
ByteBuffer b = ByteBuffer.allocate(4);
|
||||
b.put(new byte[]{1,-1});
|
||||
ByteBufferInputStream bbis = new ByteBufferInputStream(b.flip().asReadOnlyBuffer());
|
||||
assertEquals(1, bbis.read());
|
||||
assertEquals(255, bbis.read());
|
||||
assertEquals(-1, bbis.read());
|
||||
}
|
||||
@Test
|
||||
void testReadNegativeByteInArray() throws Exception {
|
||||
ByteBuffer b = ByteBuffer.allocate(4);
|
||||
b.put(new byte[]{1,-1});
|
||||
ByteBufferInputStream bbis = new ByteBufferInputStream(b.flip().asReadOnlyBuffer());
|
||||
byte output[] = new byte[10];
|
||||
assertEquals(2, bbis.read(output, 0, 10));
|
||||
assertArrayEquals(new byte[]{1, -1, 0, 0, 0, 0, 0, 0, 0, 0}, output);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.lambda;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.amazonaws.services.lambda.runtime.Context;
|
||||
import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
|
||||
import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse;
|
||||
import com.amazonaws.services.lambda.runtime.tests.EventLoader;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import software.amazon.awssdk.core.SdkBytes;
|
||||
import software.amazon.awssdk.services.kinesis.KinesisClient;
|
||||
import software.amazon.awssdk.services.kinesis.model.PutRecordRequest;
|
||||
|
||||
// Modeled after https://aws.amazon.com/blogs/opensource/testing-aws-lambda-functions-written-in-java/
|
||||
class FilterKTUpdatesHandlerTest {
|
||||
|
||||
private static byte[] b64(String b) {
|
||||
return Base64.getDecoder().decode(b);
|
||||
}
|
||||
|
||||
static final byte[] PREV_ACI = b64("IiIiIiIiIiIiIiIiIiIiIg==");
|
||||
static final byte[] PREV_ACI_KEY = b64("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
static final String PREV_NUM = "+111111111";
|
||||
static final byte[] NEXT_ACI_KEY = b64("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ");
|
||||
static final String NEXT_NUM = "+999999999";
|
||||
static final byte[] PREV_USERHASH = b64("DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD");
|
||||
static final byte[] NEXT_USERHASH = b64("EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE");
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void handleRequest(final String filename, final Account.Pair expected) {
|
||||
final DynamodbEvent event = EventLoader.loadDynamoDbEvent(filename);
|
||||
KinesisClient mockClient = mock(KinesisClient.class);
|
||||
FilterKTUpdatesHandler handler = new FilterKTUpdatesHandler(mockClient, "mystream");
|
||||
Context contextMock = mock(Context.class);
|
||||
final StreamsEventResponse streamsEventResponse = handler.handleRequest(event, contextMock);
|
||||
assertTrue(streamsEventResponse.getBatchItemFailures().isEmpty());
|
||||
ArgumentCaptor<PutRecordRequest> captor = ArgumentCaptor.forClass(PutRecordRequest.class);
|
||||
verify(mockClient, times(expected == null ? 0 : 1)).putRecord(captor.capture());
|
||||
if (expected != null) {
|
||||
List<Account.Pair> accounts = captor.getAllValues().stream().map(c -> mapWithoutException(c.data())).toList();
|
||||
assertEquals(expected, accounts.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> handleRequest() {
|
||||
return Stream.of(
|
||||
Arguments.of(
|
||||
"testevent_numberchange.json",
|
||||
new Account.Pair(
|
||||
new Account(PREV_NUM, PREV_ACI, PREV_ACI_KEY, null),
|
||||
new Account(NEXT_NUM, PREV_ACI, PREV_ACI_KEY, null))),
|
||||
Arguments.of(
|
||||
"testevent_acikeychange.json",
|
||||
new Account.Pair(
|
||||
new Account(PREV_NUM, PREV_ACI, PREV_ACI_KEY, null),
|
||||
new Account(PREV_NUM, PREV_ACI, NEXT_ACI_KEY, null))),
|
||||
Arguments.of(
|
||||
"testevent_nochange.json", null),
|
||||
Arguments.of(
|
||||
"testevent_registration1.json",
|
||||
new Account.Pair(
|
||||
null,
|
||||
new Account(PREV_NUM, PREV_ACI, PREV_ACI_KEY, null))),
|
||||
Arguments.of(
|
||||
"testevent_registration2.json",
|
||||
new Account.Pair(
|
||||
new Account(PREV_NUM, PREV_ACI, PREV_ACI_KEY, null),
|
||||
new Account(PREV_NUM, PREV_ACI, NEXT_ACI_KEY, null))),
|
||||
Arguments.of(
|
||||
"testevent_userhashchange.json",
|
||||
new Account.Pair(
|
||||
new Account(PREV_NUM, PREV_ACI, PREV_ACI_KEY, PREV_USERHASH),
|
||||
new Account(PREV_NUM, PREV_ACI, PREV_ACI_KEY, NEXT_USERHASH))),
|
||||
Arguments.of(
|
||||
"testevent_userhashadd.json",
|
||||
new Account.Pair(
|
||||
new Account(PREV_NUM, PREV_ACI, PREV_ACI_KEY, null),
|
||||
new Account(PREV_NUM, PREV_ACI, PREV_ACI_KEY, NEXT_USERHASH))));
|
||||
}
|
||||
|
||||
Account.Pair mapWithoutException(SdkBytes in) {
|
||||
try {
|
||||
return FilterKTUpdatesHandler.OBJECT_MAPPER.readValue(in.asInputStream(), Account.Pair.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("mapping", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"awsRegion": "us-east-1",
|
||||
"eventID": "88888888-85b6-4a4b-a74e-bf4688888888",
|
||||
"eventName": "MODIFY",
|
||||
"userIdentity": null,
|
||||
"recordFormat": "application/json",
|
||||
"tableName": "Signal_Accounts_Staging",
|
||||
"dynamodb": {
|
||||
"ApproximateCreationDateTime": 1630110857000,
|
||||
"Keys": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W10sImlkZW50aXR5S2V5IjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEiLCJiYWRnZXMiOltdLCJyZWdpc3RyYXRpb25Mb2NrIjpudWxsLCJyZWdpc3RyYXRpb25Mb2NrU2FsdCI6bnVsbCwidmVyc2lvbiI6MTMsInBuaSI6ImNjY2NjY2NjLWNjY2MtY2NjYy1kZGRkLWRkZGRkZGRkZGRkZCIsInBuaUlkZW50aXR5S2V5IjoiQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkIiLCJjcHYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwidWFrIjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYj09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9Cg=="
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W10sImlkZW50aXR5S2V5IjoiWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWloiLCJiYWRnZXMiOltdLCJyZWdpc3RyYXRpb25Mb2NrIjpudWxsLCJyZWdpc3RyYXRpb25Mb2NrU2FsdCI6bnVsbCwidmVyc2lvbiI6MTMsInBuaSI6ImNjY2NjY2NjLWNjY2MtY2NjYy1kZGRkLWRkZGRkZGRkZGRkZCIsInBuaUlkZW50aXR5S2V5IjoiQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkIiLCJjcHYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwidWFrIjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYj09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9Cg=="
|
||||
}
|
||||
},
|
||||
"SizeBytes": 7001
|
||||
},
|
||||
"eventSource": "aws:dynamodb"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"awsRegion": "us-east-1",
|
||||
"eventID": "88888888-85b6-4a4b-a74e-bf4688888888",
|
||||
"eventName": "MODIFY",
|
||||
"userIdentity": null,
|
||||
"recordFormat": "application/json",
|
||||
"tableName": "Signal_Accounts_Staging",
|
||||
"dynamodb": {
|
||||
"ApproximateCreationDateTime": 1630110857000,
|
||||
"Keys": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W10sImlkZW50aXR5S2V5IjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEiLCJiYWRnZXMiOltdLCJyZWdpc3RyYXRpb25Mb2NrIjpudWxsLCJyZWdpc3RyYXRpb25Mb2NrU2FsdCI6bnVsbCwidmVyc2lvbiI6MTMsInBuaSI6ImNjY2NjY2NjLWNjY2MtY2NjYy1kZGRkLWRkZGRkZGRkZGRkZCIsInBuaUlkZW50aXR5S2V5IjoiQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkIiLCJjcHYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwidWFrIjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYj09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9Cg=="
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W10sImlkZW50aXR5S2V5IjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEiLCJiYWRnZXMiOltdLCJyZWdpc3RyYXRpb25Mb2NrIjpudWxsLCJyZWdpc3RyYXRpb25Mb2NrU2FsdCI6bnVsbCwidmVyc2lvbiI6MTMsInBuaSI6ImNjY2NjY2NjLWNjY2MtY2NjYy1kZGRkLWRkZGRkZGRkZGRkZCIsInBuaUlkZW50aXR5S2V5IjoiQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkIiLCJjcHYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwidWFrIjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYj09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9Cg=="
|
||||
}
|
||||
},
|
||||
"SizeBytes": 7001
|
||||
},
|
||||
"eventSource": "aws:dynamodb"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"awsRegion": "us-east-1",
|
||||
"eventID": "88888888-85b6-4a4b-a74e-bf4688888888",
|
||||
"eventName": "MODIFY",
|
||||
"userIdentity": null,
|
||||
"recordFormat": "application/json",
|
||||
"tableName": "Signal_Accounts_Staging",
|
||||
"dynamodb": {
|
||||
"ApproximateCreationDateTime": 1630110857000,
|
||||
"Keys": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W10sImlkZW50aXR5S2V5IjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEiLCJiYWRnZXMiOltdLCJyZWdpc3RyYXRpb25Mb2NrIjpudWxsLCJyZWdpc3RyYXRpb25Mb2NrU2FsdCI6bnVsbCwidmVyc2lvbiI6MTMsInBuaSI6ImNjY2NjY2NjLWNjY2MtY2NjYy1kZGRkLWRkZGRkZGRkZGRkZCIsInBuaUlkZW50aXR5S2V5IjoiQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkIiLCJjcHYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwidWFrIjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYj09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9Cg=="
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+999999999"
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrOTk5OTk5OTk5IiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W10sImlkZW50aXR5S2V5IjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEiLCJiYWRnZXMiOltdLCJyZWdpc3RyYXRpb25Mb2NrIjpudWxsLCJyZWdpc3RyYXRpb25Mb2NrU2FsdCI6bnVsbCwidmVyc2lvbiI6MTMsInBuaSI6ImNjY2NjY2NjLWNjY2MtY2NjYy1kZGRkLWRkZGRkZGRkZGRkZCIsInBuaUlkZW50aXR5S2V5IjoiQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkIiLCJjcHYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwidWFrIjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYj09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9Cg=="
|
||||
}
|
||||
},
|
||||
"SizeBytes": 7001
|
||||
},
|
||||
"eventSource": "aws:dynamodb"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"eventID": "de07800b8d02f0649ee34aa542ee94d0",
|
||||
"eventName": "INSERT",
|
||||
"eventVersion": "1.1",
|
||||
"eventSource": "aws:dynamodb",
|
||||
"awsRegion": "us-east-1",
|
||||
"dynamodb": {
|
||||
"ApproximateCreationDateTime": "2023-09-20T02:01:56-06:00",
|
||||
"Keys": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W3siaWQiOjEsIm5hbWUiOm51bGwsImF1dGhUb2tlbiI6IjIuYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsInNhbHQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdjbUlkIjpudWxsLCJhcG5JZCI6bnVsbCwidm9pcEFwbklkIjpudWxsLCJwdXNoVGltZXN0YW1wIjowLCJ1bmluc3RhbGxlZEZlZWRiYWNrIjowLCJmZXRjaGVzTWVzc2FnZXMiOnRydWUsInJlZ2lzdHJhdGlvbklkIjoxMTExLCJzaWduZWRQcmVLZXkiOm51bGwsImxhc3RTZWVuIjoxMTExMTExMTExMTExLCJjcmVhdGVkIjoxMTExMTExMTExMTExLCJ1c2VyQWdlbnQiOiJTaWduYWwtQW5kcm9pZC82LjI4LjEgc2lnbmFsLWNsaS8wLjEyLjAiLCJjYXBhYmlsaXRpZXMiOnsic3RvcmFnZSI6ZmFsc2UsInRyYW5zZmVyIjpmYWxzZSwicG5pIjpmYWxzZSwicGF5bWVudEFjdGl2YXRpb24iOmZhbHNlfSwicG5pUmVnaXN0cmF0aW9uSWQiOjExMTExLCJwbmlTaWduZWRQcmVLZXkiOm51bGx9XSwiaWRlbnRpdHlLZXkiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsImJhZGdlcyI6W10sInJlZ2lzdHJhdGlvbkxvY2siOm51bGwsInJlZ2lzdHJhdGlvbkxvY2tTYWx0IjpudWxsLCJ2ZXJzaW9uIjowLCJwbmkiOiJjY2NjY2NjYy1jY2NjLWNjY2MtZGRkZC1kZGRkZGRkZGRkZGQiLCJldSI6bnVsbCwicG5pSWRlbnRpdHlLZXkiOm51bGwsImNwdiI6bnVsbCwidWFrIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9"
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"SequenceNumber": "131421792200000000021909195349",
|
||||
"SizeBytes": 952,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"eventID": "45f94b43b82bffb83ea4056358205a7c",
|
||||
"eventName": "MODIFY",
|
||||
"eventVersion": "1.1",
|
||||
"eventSource": "aws:dynamodb",
|
||||
"awsRegion": "us-east-1",
|
||||
"dynamodb": {
|
||||
"ApproximateCreationDateTime": "2023-09-20T02:01:56-06:00",
|
||||
"Keys": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W3siaWQiOjEsIm5hbWUiOm51bGwsImF1dGhUb2tlbiI6IjIuYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsInNhbHQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdjbUlkIjpudWxsLCJhcG5JZCI6bnVsbCwidm9pcEFwbklkIjpudWxsLCJwdXNoVGltZXN0YW1wIjowLCJ1bmluc3RhbGxlZEZlZWRiYWNrIjowLCJmZXRjaGVzTWVzc2FnZXMiOnRydWUsInJlZ2lzdHJhdGlvbklkIjoxMTExLCJzaWduZWRQcmVLZXkiOnsia2V5SWQiOjEsInB1YmxpY0tleSI6IkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBIiwic2lnbmF0dXJlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEifSwibGFzdFNlZW4iOjExMTExMTExMTExMTEsImNyZWF0ZWQiOjExMTExMTExMTExMTEsInVzZXJBZ2VudCI6IlNpZ25hbC1BbmRyb2lkLzYuMjguMSBzaWduYWwtY2xpLzAuMTIuMCIsImNhcGFiaWxpdGllcyI6eyJzdG9yYWdlIjpmYWxzZSwidHJhbnNmZXIiOmZhbHNlLCJwbmkiOmZhbHNlLCJwYXltZW50QWN0aXZhdGlvbiI6ZmFsc2V9LCJwbmlSZWdpc3RyYXRpb25JZCI6MTExMTEsInBuaVNpZ25lZFByZUtleSI6eyJrZXlJZCI6MSwicHVibGljS2V5IjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEiLCJzaWduYXR1cmUiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSJ9fV0sImlkZW50aXR5S2V5IjoiWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWloiLCJiYWRnZXMiOltdLCJyZWdpc3RyYXRpb25Mb2NrIjpudWxsLCJyZWdpc3RyYXRpb25Mb2NrU2FsdCI6bnVsbCwidmVyc2lvbiI6MCwicG5pIjoiY2NjY2NjY2MtY2NjYy1jY2NjLWRkZGQtZGRkZGRkZGRkZGRkIiwiZXUiOm51bGwsInBuaUlkZW50aXR5S2V5IjoiWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVkiLCJjcHYiOm51bGwsInVhayI6IkFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9PSIsInV1YSI6ZmFsc2UsImluQ2RzIjp0cnVlfQo="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W3siaWQiOjEsIm5hbWUiOm51bGwsImF1dGhUb2tlbiI6IjIuYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsInNhbHQiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdjbUlkIjpudWxsLCJhcG5JZCI6bnVsbCwidm9pcEFwbklkIjpudWxsLCJwdXNoVGltZXN0YW1wIjowLCJ1bmluc3RhbGxlZEZlZWRiYWNrIjowLCJmZXRjaGVzTWVzc2FnZXMiOnRydWUsInJlZ2lzdHJhdGlvbklkIjoxMTExLCJzaWduZWRQcmVLZXkiOm51bGwsImxhc3RTZWVuIjoxMTExMTExMTExMTExLCJjcmVhdGVkIjoxMTExMTExMTExMTExLCJ1c2VyQWdlbnQiOiJTaWduYWwtQW5kcm9pZC82LjI4LjEgc2lnbmFsLWNsaS8wLjEyLjAiLCJjYXBhYmlsaXRpZXMiOnsic3RvcmFnZSI6ZmFsc2UsInRyYW5zZmVyIjpmYWxzZSwicG5pIjpmYWxzZSwicGF5bWVudEFjdGl2YXRpb24iOmZhbHNlfSwicG5pUmVnaXN0cmF0aW9uSWQiOjExMTExLCJwbmlTaWduZWRQcmVLZXkiOm51bGx9XSwiaWRlbnRpdHlLZXkiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsImJhZGdlcyI6W10sInJlZ2lzdHJhdGlvbkxvY2siOm51bGwsInJlZ2lzdHJhdGlvbkxvY2tTYWx0IjpudWxsLCJ2ZXJzaW9uIjowLCJwbmkiOiJjY2NjY2NjYy1jY2NjLWNjY2MtZGRkZC1kZGRkZGRkZGRkZGQiLCJldSI6bnVsbCwicG5pSWRlbnRpdHlLZXkiOm51bGwsImNwdiI6bnVsbCwidWFrIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9"
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"SequenceNumber": "131421792300000000021909195420",
|
||||
"SizeBytes": 2306,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"awsRegion": "us-east-1",
|
||||
"eventID": "88888888-85b6-4a4b-a74e-bf4688888888",
|
||||
"eventName": "MODIFY",
|
||||
"userIdentity": null,
|
||||
"recordFormat": "application/json",
|
||||
"tableName": "Signal_Accounts_Staging",
|
||||
"dynamodb": {
|
||||
"ApproximateCreationDateTime": 1630110857000,
|
||||
"Keys": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjpudWxsLCJyZXNlcnZlZFVzZXJuYW1lSGFzaCI6bnVsbCwiZGV2aWNlcyI6W10sImlkZW50aXR5S2V5IjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEiLCJiYWRnZXMiOltdLCJyZWdpc3RyYXRpb25Mb2NrIjpudWxsLCJyZWdpc3RyYXRpb25Mb2NrU2FsdCI6bnVsbCwidmVyc2lvbiI6MTMsInBuaSI6ImNjY2NjY2NjLWNjY2MtY2NjYy1kZGRkLWRkZGRkZGRkZGRkZCIsInBuaUlkZW50aXR5S2V5IjoiQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkIiLCJjcHYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwidWFrIjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYj09IiwidXVhIjpmYWxzZSwiaW5DZHMiOnRydWV9Cg=="
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"N": {
|
||||
"B": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE="
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjoiRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRSIsInJlc2VydmVkVXNlcm5hbWVIYXNoIjpudWxsLCJkZXZpY2VzIjpbXSwiaWRlbnRpdHlLZXkiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsImJhZGdlcyI6W10sInJlZ2lzdHJhdGlvbkxvY2siOm51bGwsInJlZ2lzdHJhdGlvbkxvY2tTYWx0IjpudWxsLCJ2ZXJzaW9uIjoxMywicG5pIjoiY2NjY2NjY2MtY2NjYy1jY2NjLWRkZGQtZGRkZGRkZGRkZGRkIiwicG5pSWRlbnRpdHlLZXkiOiJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQiIsImNwdiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ1YWsiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiPT0iLCJ1dWEiOmZhbHNlLCJpbkNkcyI6dHJ1ZX0K"
|
||||
}
|
||||
},
|
||||
"SizeBytes": 7001
|
||||
},
|
||||
"eventSource": "aws:dynamodb"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"awsRegion": "us-east-1",
|
||||
"eventID": "88888888-85b6-4a4b-a74e-bf4688888888",
|
||||
"eventName": "MODIFY",
|
||||
"userIdentity": null,
|
||||
"recordFormat": "application/json",
|
||||
"tableName": "Signal_Accounts_Staging",
|
||||
"dynamodb": {
|
||||
"ApproximateCreationDateTime": 1630110857000,
|
||||
"Keys": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
}
|
||||
},
|
||||
"OldImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"N": {
|
||||
"B": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDA="
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjoiRERERERERERERERERERERERERERERERERERERERERERERERERERERERERCIsInJlc2VydmVkVXNlcm5hbWVIYXNoIjpudWxsLCJkZXZpY2VzIjpbXSwiaWRlbnRpdHlLZXkiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsImJhZGdlcyI6W10sInJlZ2lzdHJhdGlvbkxvY2siOm51bGwsInJlZ2lzdHJhdGlvbkxvY2tTYWx0IjpudWxsLCJ2ZXJzaW9uIjoxMywicG5pIjoiY2NjY2NjY2MtY2NjYy1jY2NjLWRkZGQtZGRkZGRkZGRkZGRkIiwicG5pSWRlbnRpdHlLZXkiOiJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQiIsImNwdiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ1YWsiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiPT0iLCJ1dWEiOmZhbHNlLCJpbkNkcyI6dHJ1ZX0K"
|
||||
}
|
||||
},
|
||||
"NewImage": {
|
||||
"U": {
|
||||
"B": "IiIiIiIiIiIiIiIiIiIiIg=="
|
||||
},
|
||||
"P": {
|
||||
"S": "+111111111"
|
||||
},
|
||||
"N": {
|
||||
"B": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE="
|
||||
},
|
||||
"D": {
|
||||
"B": "eyJudW1iZXIiOiIrMTExMTExMTExIiwidXNlcm5hbWVIYXNoIjoiRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRSIsInJlc2VydmVkVXNlcm5hbWVIYXNoIjpudWxsLCJkZXZpY2VzIjpbXSwiaWRlbnRpdHlLZXkiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsImJhZGdlcyI6W10sInJlZ2lzdHJhdGlvbkxvY2siOm51bGwsInJlZ2lzdHJhdGlvbkxvY2tTYWx0IjpudWxsLCJ2ZXJzaW9uIjoxMywicG5pIjoiY2NjY2NjY2MtY2NjYy1jY2NjLWRkZGQtZGRkZGRkZGRkZGRkIiwicG5pSWRlbnRpdHlLZXkiOiJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQiIsImNwdiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ1YWsiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiPT0iLCJ1dWEiOmZhbHNlLCJpbkNkcyI6dHJ1ZX0K"
|
||||
}
|
||||
},
|
||||
"SizeBytes": 7001
|
||||
},
|
||||
"eventSource": "aws:dynamodb"
|
||||
}
|
||||
]
|
||||
}
|
||||
70
go.mod
Normal file
70
go.mod
Normal file
@ -0,0 +1,70 @@
|
||||
module github.com/signalapp/keytransparency
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.85
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.35.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/harlow/kinesis-consumer v0.3.6
|
||||
github.com/hashicorp/go-metrics v0.5.4
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/kinbiko/jsonassert v1.2.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/text v0.26.0
|
||||
google.golang.org/grpc v1.73.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
|
||||
github.com/aws/smithy-go v1.22.4 // indirect
|
||||
github.com/awslabs/kinesis-aggregation/go v0.0.0-20241004223953-c2774b1ab29b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
484
go.sum
Normal file
484
go.sum
Normal file
@ -0,0 +1,484 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q=
|
||||
github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
|
||||
github.com/apex/log v1.6.0/go.mod h1:x7s+P9VtvFBXge9Vbn+8TrqKmuzmD35TTkeBHul8UtY=
|
||||
github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
||||
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
|
||||
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/aws/aws-sdk-go v1.19.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.8.1/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.6.1/go.mod h1:t/y3UPu0XEDy0cEw6mvygaBQaPzWiYAxfP2SzgtvclA=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.3.3/go.mod h1:oVieKMT3m9BSfqhOfuQ+E0j/yN84ZAJ7Qv8Sfume/ak=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.2.0/go.mod h1:UVFtSYSWCHj2+brBLDHUdlJXmz8LxUpZhA+Ewypc+xQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3 h1:xQYRnbQ+ypDMCLiFlLw5cF7Xd6K+oaL7jco2zwIMqTs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3/go.mod h1:X7RC8FFkx0bjNJRBddd3xdoDaDmNLSxICFdIdJ7asqw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.85 h1:2iCEhB7qQIxjvaSwal54ySXisTrpgVAT3oAKlzo723A=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.85/go.mod h1:WIlhfHIvyaXSoQg70L3iXZzKihNu0r4HQg6Abf/y0bQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.1/go.mod h1:+GTydg3uHmVlQdkRoetz6VHKbOMEYof70m19IpMLifc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.0.4/go.mod h1:W5gGbtNXFpF9/ssYZTaItzG/B+j0bjTnwStiCP2AtWU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.1/go.mod h1:Pv3WenDjI0v2Jl7UaMFIIbPOBbhn33RmmAmGgkXDoqY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.5.0/go.mod h1:XY5YhCS9SLul3JSQ08XG/nfxXxrkh6RR21XPq/J//NY=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4 h1:Rv6o9v2AfdEIKoAa7pQpJ5ch9ji2HevFUvGY6ufawlI=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.4.0/go.mod h1:bYsEP8w5YnbYyrx/Zi5hy4hTwRRQISSJS3RWrsGRijg=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 h1:QHaS/SHXfyNycuu4GiWb+AfW5T3bput6X5E3Ai/Q31M=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.1.0/go.mod h1:enkU5tq2HoXY+ZMiQprgF3Q83T3PbO77E83yXXzRZWE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.3/go.mod h1:7gcsONBmFoCcKrAqrm95trrMd2+C/ReYKP7Vfu8yHHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.6.0/go.mod h1:9O7UG2pELnP0hq35+Gd7XDjOLBkg7tmgRQ0y14ZjoJI=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.35.3 h1:aAi9YBNpYMEX52Z9qy1YP2t3RhDqMcP67Ep/C4q5RiQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.35.3/go.mod h1:DH0TzTbBG82HKNpBQlplRNSS4bGz0dsbJvxdK9f6rUY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.3.3/go.mod h1:Jgw5O+SK7MZ2Yi9Yvzb4PggAPYaFSliiQuWR0hNjexk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.6.2/go.mod h1:RBhoMJB8yFToaCnbe0jNq5Dcdy0jp6LhHqg55rjClkM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
|
||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/awslabs/kinesis-aggregation/go v0.0.0-20210630091500-54e17340d32f/go.mod h1:SghidfnxvX7ribW6nHI7T+IBbc9puZ9kk5Tx/88h8P4=
|
||||
github.com/awslabs/kinesis-aggregation/go v0.0.0-20241004223953-c2774b1ab29b h1:wB+kTYjUTTEpd7HBzetdxlk92dBLhhN04T5nd4fa7Nk=
|
||||
github.com/awslabs/kinesis-aggregation/go v0.0.0-20241004223953-c2774b1ab29b/go.mod h1:CQGhQ8Rf1WF5Ke8XuUjcd4PRb+mFTjzKR/pm3EWKaQw=
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-redis/redis/v9 v9.0.0-rc.2/go.mod h1:cgBknjwcBJa2prbnuHH/4k/Mlj4r0pWNV2HBanHujfY=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/harlow/kinesis-consumer v0.3.6 h1:aAg48nNknQllcz3wZZcZVPy8dFAiv6c3pyQP+ngAlUY=
|
||||
github.com/harlow/kinesis-consumer v0.3.6/go.mod h1:jTE9kH7IVx841D0GgxjykKieSP1yDSckuEg5ceSCjEU=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
|
||||
github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kinbiko/jsonassert v1.2.0 h1:+/JthIVXdIrThrOtSN9ry0mNtWKXMWuvxR0nU7gQ+tI=
|
||||
github.com/kinbiko/jsonassert v1.2.0/go.mod h1:pCc3uudOt+lVAbkji9O0uw8MSVt4s+1ZJ0y8Ux2F1Og=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
||||
github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
|
||||
github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
|
||||
github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
|
||||
github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
|
||||
github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
|
||||
github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
|
||||
github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
|
||||
github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
||||
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
|
||||
github.com/tj/go-buffer v1.0.1/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
|
||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v0.0.0-20200603152657-dc2b0ca8b37e/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
239
tree/log/chunk.go
Normal file
239
tree/log/chunk.go
Normal file
@ -0,0 +1,239 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/signalapp/keytransparency/tree/log/math"
|
||||
"github.com/signalapp/keytransparency/tree/sharedmath"
|
||||
)
|
||||
|
||||
// The log tree implementation is designed to work with a standard key-value
|
||||
// database. The tree is stored in the database in "chunks", which are
|
||||
// 8-node-wide (or 4-node-deep) subtrees. Chunks are addressed by the id of the
|
||||
// root node in the chunk. Only the leaf values of each chunk are stored, which
|
||||
// in the context of the full tree is either a leaf or a cached intermediate
|
||||
// hash. These values are stored concatenated.
|
||||
|
||||
// nodeData is the primary wrapper struct for representing a single node (leaf
|
||||
// or intermediate) in the tree.
|
||||
type nodeData struct {
|
||||
leaf bool
|
||||
value []byte
|
||||
}
|
||||
|
||||
// validate checks that value is the expected length
|
||||
func (nd *nodeData) validate() error {
|
||||
if len(nd.value) != 32 {
|
||||
return fmt.Errorf("node value is wrong length: %v", len(nd.value))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshal returns a slice with the node's value for hashing
|
||||
func (nd *nodeData) marshal() []byte {
|
||||
out := make([]byte, 33)
|
||||
if !nd.leaf {
|
||||
out[0] = 1
|
||||
}
|
||||
copy(out[1:33], nd.value)
|
||||
return out
|
||||
}
|
||||
|
||||
func (nd *nodeData) isEmpty() bool {
|
||||
return nd.value == nil
|
||||
}
|
||||
|
||||
// nodeChunk is a helper struct that handles computing/caching the intermediate
|
||||
// nodes of a chunk.
|
||||
type nodeChunk struct {
|
||||
ids []uint64
|
||||
nodes []*nodeData
|
||||
}
|
||||
|
||||
func newChunk(chunkRootNodeId uint64, data []byte) (*nodeChunk, error) {
|
||||
// Create a map that shows the node id represented by each element of the
|
||||
// nodes array. This code is a little bit verbose, but I like that it's easy
|
||||
// to check it's correct: run with id = 7 and the output is [0, 1, ..., 14].
|
||||
ids := make([]uint64, 15)
|
||||
ids[7] = chunkRootNodeId
|
||||
ids[3] = sharedmath.Left(ids[7])
|
||||
ids[1] = sharedmath.Left(ids[3])
|
||||
ids[0] = sharedmath.Left(ids[1])
|
||||
ids[2] = sharedmath.RightStep(ids[1])
|
||||
ids[5] = sharedmath.RightStep(ids[3])
|
||||
ids[4] = sharedmath.Left(ids[5])
|
||||
ids[6] = sharedmath.RightStep(ids[5])
|
||||
ids[11] = sharedmath.RightStep(ids[7])
|
||||
ids[9] = sharedmath.Left(ids[11])
|
||||
ids[8] = sharedmath.Left(ids[9])
|
||||
ids[10] = sharedmath.RightStep(ids[9])
|
||||
ids[13] = sharedmath.RightStep(ids[11])
|
||||
ids[12] = sharedmath.Left(ids[13])
|
||||
ids[14] = sharedmath.RightStep(ids[13])
|
||||
|
||||
// Parse the serialized data.
|
||||
leafChunk := sharedmath.Level(chunkRootNodeId) == 3
|
||||
nodes := make([]*nodeData, 0, 15)
|
||||
|
||||
for len(data) > 0 {
|
||||
if len(data) < 32 {
|
||||
return nil, fmt.Errorf("unable to parse chunk")
|
||||
}
|
||||
if len(nodes) > 0 {
|
||||
nodes = append(nodes, &nodeData{leaf: false, value: nil})
|
||||
}
|
||||
nodes = append(nodes, &nodeData{
|
||||
leaf: leafChunk,
|
||||
value: data[:32],
|
||||
})
|
||||
data = data[32:]
|
||||
}
|
||||
for len(nodes) < 15 {
|
||||
// if the chunk is not full, initialize remainder with stub data
|
||||
nodes = append(nodes, &nodeData{
|
||||
leaf: sharedmath.IsLeaf(ids[len(nodes)]),
|
||||
value: nil,
|
||||
})
|
||||
}
|
||||
if len(nodes) != 15 {
|
||||
return nil, fmt.Errorf("unable to parse chunk")
|
||||
}
|
||||
|
||||
return &nodeChunk{ids: ids, nodes: nodes}, nil
|
||||
}
|
||||
|
||||
// findIndex returns the index of the node ID in the chunk's ids slice
|
||||
func (c *nodeChunk) findIndex(nodeId uint64) uint64 {
|
||||
// Since c.ids is sorted in increasing order, we can just BinarySearch it.
|
||||
// This should be a little faster than iterating over IDs.
|
||||
if n, found := slices.BinarySearch(c.ids, nodeId); found {
|
||||
return uint64(n)
|
||||
}
|
||||
panic("requested hash not available in this chunk")
|
||||
}
|
||||
|
||||
// get returns the data of nodeId with the value populated.
|
||||
func (c *nodeChunk) get(nodeId, numLeaves uint64, set *chunkSet) *nodeData {
|
||||
i := c.findIndex(nodeId)
|
||||
if !sharedmath.IsLeaf(nodeId) && c.nodes[i].isEmpty() {
|
||||
l, r := sharedmath.Left(nodeId), math.Right(nodeId, numLeaves)
|
||||
c.nodes[i].value = treeHash(set.get(l), set.get(r))
|
||||
}
|
||||
return c.nodes[i]
|
||||
}
|
||||
|
||||
// set updates nodeId to contain the given value and, if necessary, nullifies the values stored by its parent nodes
|
||||
func (c *nodeChunk) set(nodeId uint64, value []byte) {
|
||||
nd := &nodeData{
|
||||
leaf: sharedmath.IsLeaf(nodeId),
|
||||
value: value,
|
||||
}
|
||||
|
||||
i := c.findIndex(nodeId)
|
||||
c.nodes[i] = nd
|
||||
for i != 7 {
|
||||
i = sharedmath.ParentStep(i)
|
||||
c.nodes[i].value = nil
|
||||
}
|
||||
}
|
||||
|
||||
// marshal returns the serialized chunk.
|
||||
func (c *nodeChunk) marshal() []byte {
|
||||
out := make([]byte, 0)
|
||||
|
||||
for i := 0; i < len(c.nodes); i += 2 { // += 2 because only leaves get serialized
|
||||
|
||||
if c.nodes[i].isEmpty() {
|
||||
// We've reached an empty leaf. Because this is a left-balanced tree, the remaining leaves must also be
|
||||
// empty, so check that there are no other populated nodes.
|
||||
for ; i < len(c.nodes); i++ {
|
||||
if !c.nodes[i].isEmpty() {
|
||||
panic("chunk has gaps")
|
||||
}
|
||||
}
|
||||
|
||||
// The invariant is true, and there's nothing left to write, so we can exit now
|
||||
break
|
||||
}
|
||||
|
||||
out = append(out, c.nodes[i].value...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// chunkSet is a helper struct for directing operations to the correct nodeChunk
|
||||
// in a set.
|
||||
type chunkSet struct {
|
||||
numLeaves uint64 // The number of leaves in the log tree when this chunk set was initialized
|
||||
chunks map[uint64]*nodeChunk
|
||||
modified map[uint64]struct{} // tracks all chunkIds that have been modified and need to be persisted
|
||||
}
|
||||
|
||||
// newChunkSet creates a new chunkSet from a raw LogStore.BatchGet() response. numLeaves is the number of leaves in
|
||||
// the *full* tree. Each []byte slice in data is the concatenated data for the nodes in that chunk.
|
||||
func newChunkSet(numLeaves uint64, data map[uint64][]byte) (*chunkSet, error) {
|
||||
chunks := make(map[uint64]*nodeChunk, len(data))
|
||||
for id, raw := range data {
|
||||
c, err := newChunk(id, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chunks[id] = c
|
||||
}
|
||||
|
||||
return &chunkSet{
|
||||
numLeaves: numLeaves,
|
||||
chunks: chunks,
|
||||
modified: make(map[uint64]struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// get returns node x.
|
||||
func (s *chunkSet) get(nodeId uint64) *nodeData {
|
||||
c, ok := s.chunks[math.Chunk(nodeId)]
|
||||
if !ok {
|
||||
panic("requested hash is not available in this chunk set")
|
||||
}
|
||||
return c.get(nodeId, s.numLeaves, s)
|
||||
}
|
||||
|
||||
// add initializes a new empty chunk for node x.
|
||||
func (s *chunkSet) add(nodeId uint64) {
|
||||
chunkId := math.Chunk(nodeId)
|
||||
if _, ok := s.chunks[chunkId]; ok {
|
||||
panic("cannot add chunk that already exists in set")
|
||||
}
|
||||
c, err := newChunk(chunkId, make([]byte, 0))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s.chunks[chunkId] = c
|
||||
}
|
||||
|
||||
// set changes node to the given value.
|
||||
func (s *chunkSet) set(nodeId uint64, value []byte) {
|
||||
chunkId := math.Chunk(nodeId)
|
||||
c, ok := s.chunks[chunkId]
|
||||
if !ok {
|
||||
panic("requested hash is not available in this chunk set")
|
||||
}
|
||||
c.set(nodeId, value)
|
||||
s.modified[chunkId] = struct{}{}
|
||||
}
|
||||
|
||||
// marshalChanges returns the set of changes that have been done on this chunkSet
|
||||
// since its creation, for persisting back in our storage medium.
|
||||
func (s *chunkSet) marshalChanges() map[uint64][]byte {
|
||||
out := make(map[uint64][]byte, 0)
|
||||
for id := range s.modified {
|
||||
out[id] = s.chunks[id].marshal()
|
||||
}
|
||||
return out
|
||||
}
|
||||
269
tree/log/log.go
Normal file
269
tree/log/log.go
Normal file
@ -0,0 +1,269 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Package log implements a log-based Merkle tree where new data is added
|
||||
// as the right-most leaf.
|
||||
package log
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
"github.com/signalapp/keytransparency/tree/log/math"
|
||||
"github.com/signalapp/keytransparency/tree/sharedmath"
|
||||
)
|
||||
|
||||
// treeHash returns the intermediate hash of left and right.
|
||||
func treeHash(left, right *nodeData) []byte {
|
||||
if err := left.validate(); err != nil {
|
||||
panic(err)
|
||||
} else if err := right.validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
input := append(left.marshal(), right.marshal()...)
|
||||
output := sha256.Sum256(input)
|
||||
return output[:]
|
||||
}
|
||||
|
||||
// Tree is an implementation of a log-based Merkle tree where all new data is
|
||||
// added as the right-most leaf.
|
||||
type Tree struct {
|
||||
tx db.LogStore
|
||||
}
|
||||
|
||||
func NewTree(tx db.LogStore) *Tree {
|
||||
return &Tree{tx: tx}
|
||||
}
|
||||
|
||||
// fetch loads the chunks for the requested nodes from the database. It returns
|
||||
// an error if not all chunks are found.
|
||||
func (t *Tree) fetchChunkSets(numLeaves uint64, nodes []uint64) (*chunkSet, error) {
|
||||
dedup := make(map[uint64]struct{})
|
||||
for _, id := range nodes {
|
||||
dedup[math.Chunk(id)] = struct{}{}
|
||||
}
|
||||
chunkIds := make([]uint64, 0, len(dedup))
|
||||
for id := range dedup {
|
||||
chunkIds = append(chunkIds, id)
|
||||
}
|
||||
|
||||
data, err := t.tx.BatchGet(chunkIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range chunkIds {
|
||||
if _, ok := data[id]; !ok {
|
||||
return nil, fmt.Errorf("chunkId %d not found in database", id)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse chunk set.
|
||||
set, err := newChunkSet(numLeaves, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// fetchSpecific returns the values for the requested nodes, accounting for the
|
||||
// ragged right-edge of the tree.
|
||||
func (t *Tree) fetchSpecific(numLeaves uint64, nodes []uint64) ([][]byte, error) {
|
||||
lookup := make([]uint64, 0)
|
||||
|
||||
// Add the nodes that we need to compute the requested hashes.
|
||||
rightEdge := make(map[uint64][]uint64)
|
||||
for _, id := range nodes {
|
||||
if math.IsFullSubtree(id, numLeaves) {
|
||||
lookup = append(lookup, id)
|
||||
} else {
|
||||
subtrees := math.FullSubtrees(id, numLeaves)
|
||||
rightEdge[id] = subtrees
|
||||
lookup = append(lookup, subtrees...)
|
||||
}
|
||||
}
|
||||
|
||||
// Load everything from the database in one roundtrip.
|
||||
set, err := t.fetchChunkSets(numLeaves, lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract the data we want to return.
|
||||
out := make([][]byte, len(nodes))
|
||||
for i, id := range nodes {
|
||||
if subtrees, ok := rightEdge[id]; ok {
|
||||
out[i] = t.computeRootFromSet(subtrees, set)
|
||||
} else {
|
||||
out[i] = set.get(id).value
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Get returns the value for the given `entry`, along with its proof of inclusion.
|
||||
func (t *Tree) Get(entry, treeSize uint64) ([]byte, [][]byte, error) {
|
||||
if treeSize == 0 {
|
||||
return nil, nil, fmt.Errorf("empty tree")
|
||||
} else if entry >= treeSize {
|
||||
return nil, nil, fmt.Errorf("can not get leaf beyond right edge of tree: %d >= %d", entry, treeSize)
|
||||
}
|
||||
|
||||
leaf := 2 * entry
|
||||
copath := math.Copath(leaf, treeSize)
|
||||
data, err := t.fetchSpecific(treeSize, append([]uint64{leaf}, copath...))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("fetching: %w", err)
|
||||
}
|
||||
|
||||
return data[0], data[1:], nil
|
||||
}
|
||||
|
||||
// GetBatchProof returns a batch proof for the given set of log entries.
|
||||
func (t *Tree) GetBatchProof(entries []uint64, treeSize uint64) ([][]byte, error) {
|
||||
if treeSize == 0 {
|
||||
return nil, fmt.Errorf("empty tree")
|
||||
} else if len(entries) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
for _, x := range entries {
|
||||
if x >= treeSize {
|
||||
return nil, fmt.Errorf("can not get leaf beyond right edge of tree")
|
||||
}
|
||||
}
|
||||
return t.fetchSpecific(treeSize, math.BatchCopath(entries, treeSize))
|
||||
}
|
||||
|
||||
// GetConsistencyProof returns a proof that the current log with n elements is
|
||||
// an extension of a previous log root with m elements, 0 < m < n.
|
||||
func (t *Tree) GetConsistencyProof(m, n uint64) ([][]byte, error) {
|
||||
if m >= n {
|
||||
return nil, fmt.Errorf("second parameter must be greater than first")
|
||||
}
|
||||
return t.fetchSpecific(n, math.ConsistencyProof(m, n))
|
||||
}
|
||||
|
||||
// GetRoot gets the root value of the log with the given number of entries
|
||||
func (t *Tree) GetRoot(treeSize uint64) ([]byte, error) {
|
||||
return t.computeRoot(treeSize, nil)
|
||||
}
|
||||
|
||||
// computeRoot computes the root value from a given chunk set.
|
||||
func (t *Tree) computeRoot(treeSize uint64, set *chunkSet) ([]byte, error) {
|
||||
subtrees := math.FullSubtrees(math.Root(treeSize), treeSize)
|
||||
if set == nil {
|
||||
var err error
|
||||
set, err = t.fetchChunkSets(treeSize, subtrees)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return t.computeRootFromSet(subtrees, set), nil
|
||||
}
|
||||
|
||||
// computeRootFromSet computes the root value from a list of subtree node IDs and chunk set
|
||||
func (t *Tree) computeRootFromSet(subtrees []uint64, set *chunkSet) []byte {
|
||||
nd := set.get(subtrees[len(subtrees)-1])
|
||||
for i := len(subtrees) - 2; i >= 0; i-- {
|
||||
nd = &nodeData{
|
||||
leaf: false,
|
||||
value: treeHash(set.get(subtrees[i]), nd),
|
||||
}
|
||||
}
|
||||
return nd.value
|
||||
}
|
||||
|
||||
// Append adds a new element to the end of the log and returns the new root
|
||||
// value. treeSize is the current size; after this operation is complete, methods to
|
||||
// this object should be called with treeSize+1.
|
||||
func (t *Tree) Append(treeSize uint64, value []byte) ([]byte, error) {
|
||||
return t.BatchAppend(treeSize, [][]byte{value})
|
||||
}
|
||||
|
||||
// BatchAppend adds several new elements to the end of the log and returns the
|
||||
// new root value. The treeSize parameter is the current size; after this operation is complete,
|
||||
// methods on this object should be called with treeSize+len(values).
|
||||
func (t *Tree) BatchAppend(treeSize uint64, values [][]byte) ([]byte, error) {
|
||||
if len(values) == 0 {
|
||||
return nil, fmt.Errorf("no values to append provided")
|
||||
}
|
||||
for _, value := range values {
|
||||
if len(value) != 32 {
|
||||
return nil, fmt.Errorf("value has wrong length: %v", len(value))
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the set of nodes that we'll need to update / create.
|
||||
newTreeSize := treeSize + uint64(len(values))
|
||||
touched := make([]uint64, 0, len(values)*2) // Node ids that are full subtrees after appending the batch, and may need to have their value stored.
|
||||
for i := range values {
|
||||
leaf := 2 * (treeSize + uint64(i))
|
||||
touched = append(touched, leaf)
|
||||
for _, id := range math.DirectPath(leaf, newTreeSize) {
|
||||
if math.IsFullSubtree(id, newTreeSize) {
|
||||
touched = append(touched, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var toFetch []uint64 // These are dedup'ed by fetch.
|
||||
createChunks := map[uint64]struct{}{}
|
||||
for _, id := range touched {
|
||||
chunkId := math.Chunk(id)
|
||||
if _, ok := createChunks[chunkId]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// a chunk has a height of 4, so go left three times
|
||||
leftmost := sharedmath.Left(sharedmath.Left(sharedmath.Left(chunkId)))
|
||||
if id == leftmost {
|
||||
// Because this is a left-balanced tree, if we touched the leftmost node in a chunk, the chunk is new
|
||||
createChunks[chunkId] = struct{}{}
|
||||
} else {
|
||||
toFetch = append(toFetch, chunkId)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the chunks we'll need to update along with nodes we'll need to know to compute the new root or updated
|
||||
// intermediates. We only need to fetch the copath for the first new node, 2*treeSize, because it provides the
|
||||
// values we need for each subsequent update of the intermediate nodes when appending the remaining entries from
|
||||
// the batch.
|
||||
for _, id := range math.Copath(2*treeSize, treeSize+1) {
|
||||
if math.IsFullSubtree(id, treeSize+1) {
|
||||
toFetch = append(toFetch, id)
|
||||
}
|
||||
}
|
||||
set, err := t.fetchChunkSets(newTreeSize, toFetch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add any new chunks to the set and set the correct hashes everywhere.
|
||||
for chunkId := range createChunks {
|
||||
set.add(chunkId)
|
||||
}
|
||||
|
||||
for i, value := range values {
|
||||
set.set(2*(treeSize+uint64(i)), value)
|
||||
}
|
||||
for _, nodeId := range touched {
|
||||
if sharedmath.IsLeaf(nodeId) {
|
||||
continue
|
||||
} else if sharedmath.Level(nodeId)%4 == 0 {
|
||||
l, r := sharedmath.Left(nodeId), math.Right(nodeId, newTreeSize)
|
||||
intermediate := treeHash(set.get(l), set.get(r))
|
||||
set.set(nodeId, intermediate)
|
||||
}
|
||||
}
|
||||
|
||||
// Push to database.
|
||||
t.tx.BatchPut(set.marshalChanges())
|
||||
|
||||
// Compute the new root from the set we've already got.
|
||||
return t.computeRoot(newTreeSize, set)
|
||||
}
|
||||
255
tree/log/log_test.go
Normal file
255
tree/log/log_test.go
Normal file
@ -0,0 +1,255 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
mrand "math/rand"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/signalapp/keytransparency/db"
|
||||
)
|
||||
|
||||
func assert(ok bool) {
|
||||
if !ok {
|
||||
panic("Assertion failed.")
|
||||
}
|
||||
}
|
||||
|
||||
func random() []byte {
|
||||
out := make([]byte, 32)
|
||||
if _, err := rand.Read(out); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dup(in []byte) []byte {
|
||||
out := make([]byte, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestInclusionProof(t *testing.T) {
|
||||
tree := NewTree(db.NewMemoryTransparencyStore().LogStore())
|
||||
calc := newSimpleRootCalculator()
|
||||
var (
|
||||
nodes [][]byte
|
||||
roots [][]byte
|
||||
)
|
||||
|
||||
checkTree := func(entry, treeSize uint64) {
|
||||
value, proof, err := tree.Get(entry, treeSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert(bytes.Equal(value, nodes[entry]))
|
||||
if err := VerifyInclusionProof(entry, treeSize, value, proof, roots[treeSize-1]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 2000; i++ {
|
||||
leaf := random()
|
||||
nodes = append(nodes, leaf)
|
||||
|
||||
// Append to the tree.
|
||||
root, err := tree.Append(uint64(i), leaf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
roots = append(roots, dup(root))
|
||||
n := i + 1
|
||||
|
||||
calc.Add(leaf)
|
||||
if calculated, err := calc.Root(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert(bytes.Equal(root, calculated))
|
||||
}
|
||||
|
||||
// Do inclusion proofs for a few random entries.
|
||||
if n < 5 {
|
||||
continue
|
||||
}
|
||||
for j := 0; j < 5; j++ {
|
||||
x := mrand.Intn(n)
|
||||
checkTree(uint64(x), uint64(n))
|
||||
|
||||
m := mrand.Intn(int(n-1)) + 1
|
||||
x = mrand.Intn(m)
|
||||
checkTree(uint64(x), uint64(m))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchInclusionProof(t *testing.T) {
|
||||
tree := NewTree(db.NewMemoryTransparencyStore().LogStore())
|
||||
var (
|
||||
leaves [][]byte
|
||||
root []byte
|
||||
err error
|
||||
)
|
||||
for i := 0; i < 2000; i++ {
|
||||
leaf := random()
|
||||
leaves = append(leaves, leaf)
|
||||
|
||||
root, err = tree.Append(uint64(i), leaf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
dedup := make(map[uint64]struct{})
|
||||
for i := 0; i < 10; i++ {
|
||||
dedup[uint64(mrand.Intn(2000))] = struct{}{}
|
||||
}
|
||||
entries := make([]uint64, 0)
|
||||
for id := range dedup {
|
||||
entries = append(entries, id)
|
||||
}
|
||||
slices.Sort(entries)
|
||||
|
||||
values := make([][]byte, 0)
|
||||
for _, id := range entries {
|
||||
values = append(values, leaves[id])
|
||||
}
|
||||
|
||||
proof, err := tree.GetBatchProof(entries, 2000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := VerifyBatchProof(entries, 2000, values, proof, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsistencyProof(t *testing.T) {
|
||||
tree := NewTree(db.NewMemoryTransparencyStore().LogStore())
|
||||
|
||||
var roots [][]byte
|
||||
for i := 0; i < 2000; i++ {
|
||||
leaf := random()
|
||||
|
||||
// Append to the tree.
|
||||
root, err := tree.Append(uint64(i), leaf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
roots = append(roots, dup(root))
|
||||
n := i + 1
|
||||
|
||||
// Do consistency proofs for a few random revisions.
|
||||
if n < 5 {
|
||||
continue
|
||||
}
|
||||
for j := 0; j < 5; j++ {
|
||||
m := mrand.Intn(n-1) + 1
|
||||
proof, err := tree.GetConsistencyProof(uint64(m), uint64(n))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = VerifyConsistencyProof(uint64(m), uint64(n), proof, roots[m-1], roots[n-1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if m > 1 {
|
||||
p := mrand.Intn(m-1) + 1
|
||||
proof, err := tree.GetConsistencyProof(uint64(p), uint64(m))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = VerifyConsistencyProof(uint64(p), uint64(m), proof, roots[p-1], roots[m-1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchAppend(t *testing.T) {
|
||||
leaves := make([][]byte, 100)
|
||||
for i := range leaves {
|
||||
leaves[i] = random()
|
||||
}
|
||||
|
||||
// Add leaves to tree in batches.
|
||||
tree1 := NewTree(db.NewMemoryTransparencyStore().LogStore())
|
||||
_, err := tree1.BatchAppend(0, leaves[:50])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root1, err := tree1.BatchAppend(50, leaves[50:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Add leaves to tree one-by-one.
|
||||
tree2 := NewTree(db.NewMemoryTransparencyStore().LogStore())
|
||||
var root2 []byte
|
||||
for i, leaf := range leaves {
|
||||
root2, err = tree2.Append(uint64(i), leaf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that roots are the same.
|
||||
if !bytes.Equal(root1, root2) {
|
||||
t.Fatal("log roots were not equal")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppend(b *testing.B) {
|
||||
tree := NewTree(db.NewMemoryTransparencyStore().LogStore())
|
||||
for i := 0; i < 100; i++ {
|
||||
_, err := tree.Append(uint64(i), random())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
leaves := make([][]byte, b.N)
|
||||
for i := range leaves {
|
||||
leaves[i] = random()
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := tree.Append(100+uint64(i), leaves[i])
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBatchAppend(b *testing.B) {
|
||||
const batchSize = 10
|
||||
|
||||
tree := NewTree(db.NewMemoryTransparencyStore().LogStore())
|
||||
for i := 0; i < 100; i++ {
|
||||
_, err := tree.Append(uint64(i), random())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
leaves := make([][]byte, batchSize*b.N)
|
||||
for i := range leaves {
|
||||
leaves[i] = random()
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
start := batchSize * i
|
||||
end := batchSize * (i + 1)
|
||||
_, err := tree.BatchAppend(100+uint64(start), leaves[start:end])
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
210
tree/log/math/math.go
Normal file
210
tree/log/math/math.go
Normal file
@ -0,0 +1,210 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
// Package math implements the mathematical operations for a log-based Merkle
|
||||
// tree.
|
||||
package math
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/signalapp/keytransparency/tree/sharedmath"
|
||||
)
|
||||
|
||||
// NodeWidth returns the number of nodes needed to store a tree with n leaves.
|
||||
func NodeWidth(numLeaves uint64) uint64 {
|
||||
if numLeaves == 0 {
|
||||
return 0
|
||||
}
|
||||
return 2*(numLeaves-1) + 1
|
||||
}
|
||||
|
||||
// Root returns the id of the root node of a tree with n leaves.
|
||||
func Root(numLeaves uint64) uint64 {
|
||||
w := NodeWidth(numLeaves)
|
||||
return (1 << sharedmath.Log2(w)) - 1
|
||||
}
|
||||
|
||||
// Right returns the right child of an intermediate node.
|
||||
func Right(nodeId, numLeaves uint64) uint64 {
|
||||
r := sharedmath.RightStep(nodeId)
|
||||
w := NodeWidth(numLeaves)
|
||||
for r >= w {
|
||||
r = sharedmath.Left(r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Parent returns the id of the parent node, if there are n leaves in the tree
|
||||
// total.
|
||||
func Parent(nodeId, numLeaves uint64) uint64 {
|
||||
if nodeId == Root(numLeaves) {
|
||||
panic("root node has no parent")
|
||||
}
|
||||
|
||||
width := NodeWidth(numLeaves)
|
||||
return sharedmath.Parent(nodeId, width)
|
||||
}
|
||||
|
||||
// Sibling returns the other child of the node's parent.
|
||||
func Sibling(nodeId, numLeaves uint64) uint64 {
|
||||
p := Parent(nodeId, numLeaves)
|
||||
if nodeId < p {
|
||||
return Right(p, numLeaves)
|
||||
} else {
|
||||
return sharedmath.Left(p)
|
||||
}
|
||||
}
|
||||
|
||||
// DirectPath returns the direct path of a node, ordered from leaf to root.
|
||||
func DirectPath(nodeId, numLeaves uint64) []uint64 {
|
||||
rootNodeId := Root(numLeaves)
|
||||
if nodeId == rootNodeId {
|
||||
return []uint64{}
|
||||
}
|
||||
|
||||
d := []uint64{}
|
||||
for nodeId != rootNodeId {
|
||||
nodeId = Parent(nodeId, numLeaves)
|
||||
d = append(d, nodeId)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Copath returns the copath of a node, ordered from leaf to root.
|
||||
func Copath(nodeId, numLeaves uint64) []uint64 {
|
||||
if nodeId > 2*(numLeaves-1) {
|
||||
panic("nodeId does not exist in the given tree")
|
||||
}
|
||||
if nodeId == Root(numLeaves) {
|
||||
return []uint64{}
|
||||
}
|
||||
|
||||
d := append([]uint64{nodeId}, DirectPath(nodeId, numLeaves)...)
|
||||
// remove the root
|
||||
d = d[:len(d)-1]
|
||||
for i := 0; i < len(d); i++ {
|
||||
// replace with the sibling
|
||||
d[i] = Sibling(d[i], numLeaves)
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// IsFullSubtree returns true if nodeId represents a full subtree.
|
||||
func IsFullSubtree(nodeId, numLeaves uint64) bool {
|
||||
rightmost := 2 * (numLeaves - 1)
|
||||
expected := nodeId + (1 << sharedmath.Level(nodeId)) - 1
|
||||
|
||||
return expected <= rightmost
|
||||
}
|
||||
|
||||
// FullSubtrees returns the list of full subtree root node IDs, starting from nodeId and traversing down the right side.
|
||||
func FullSubtrees(nodeId, numLeaves uint64) []uint64 {
|
||||
out := []uint64{}
|
||||
|
||||
for !IsFullSubtree(nodeId, numLeaves) {
|
||||
out = append(out, sharedmath.Left(nodeId))
|
||||
nodeId = Right(nodeId, numLeaves)
|
||||
}
|
||||
out = append(out, nodeId)
|
||||
return out
|
||||
}
|
||||
|
||||
// ConsistencyProof returns the list of node ids to return for a consistency
|
||||
// proof between m and n, based on the algorithm from RFC 6962.
|
||||
func ConsistencyProof(m, n uint64) []uint64 {
|
||||
return subProof(m, n, true)
|
||||
}
|
||||
|
||||
// subProof implements the algorithm from RFC 6962. `b` indicates that `m` is the value for which the proof was
|
||||
// originally requested, since `m` might change during recursive calls to generate subProofs
|
||||
func subProof(m, n uint64, b bool) []uint64 {
|
||||
if m == n {
|
||||
if b {
|
||||
return []uint64{}
|
||||
}
|
||||
// m must be a power of two if it is not its original value, because k (now n) is always a power of 2
|
||||
return []uint64{Root(m)}
|
||||
}
|
||||
|
||||
k := uint64(1) << sharedmath.Log2(n)
|
||||
if k == n {
|
||||
k = k / 2
|
||||
}
|
||||
if m <= k {
|
||||
proof := subProof(m, k, b)
|
||||
proof = append(proof, Right(Root(n), n))
|
||||
return proof
|
||||
}
|
||||
|
||||
proof := subProof(m-k, n-k, false)
|
||||
for i := 0; i < len(proof); i++ {
|
||||
proof[i] = proof[i] + 2*k
|
||||
}
|
||||
proof = append([]uint64{sharedmath.Left(Root(n))}, proof...)
|
||||
return proof
|
||||
}
|
||||
|
||||
// BatchCopath returns the copath nodes of a batch of leaves. The input leaves are *log entry* IDs, not *node* IDs, sorted in increasing order.
|
||||
func BatchCopath(leaves []uint64, numLeaves uint64) []uint64 {
|
||||
// Convert the leaf indices to node indices.
|
||||
nodes := make([]uint64, len(leaves))
|
||||
for i, x := range leaves {
|
||||
nodes[i] = 2 * x
|
||||
}
|
||||
slices.Sort(nodes)
|
||||
|
||||
// Iteratively combine nodes until there's only one entry in the list (being
|
||||
// the root), keeping track of the extra nodes we needed to get there.
|
||||
out := make([]uint64, 0)
|
||||
root := Root(numLeaves)
|
||||
for {
|
||||
if len(nodes) == 1 && nodes[0] == root {
|
||||
break
|
||||
}
|
||||
|
||||
nextLevel := make([]uint64, 0)
|
||||
for len(nodes) > 1 {
|
||||
p := Parent(nodes[0], numLeaves)
|
||||
if Right(p, numLeaves) == nodes[1] { // Sibling is already here.
|
||||
nodes = nodes[2:]
|
||||
} else { // Need to fetch sibling.
|
||||
out = append(out, Sibling(nodes[0], numLeaves))
|
||||
nodes = nodes[1:]
|
||||
}
|
||||
nextLevel = append(nextLevel, p)
|
||||
}
|
||||
if len(nodes) == 1 {
|
||||
if len(nextLevel) > 0 && sharedmath.Level(Parent(nodes[0], numLeaves)) > sharedmath.Level(nextLevel[0]) {
|
||||
// the parent of the last, rightmost, node skips one or more levels, so we must hold on to it until
|
||||
// the current level contains its parent's sibling
|
||||
nextLevel = append(nextLevel, nodes[0])
|
||||
} else {
|
||||
out = append(out, Sibling(nodes[0], numLeaves))
|
||||
nextLevel = append(nextLevel, Parent(nodes[0], numLeaves))
|
||||
}
|
||||
}
|
||||
|
||||
nodes = nextLevel
|
||||
}
|
||||
slices.Sort(out)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Chunk takes a node id as input and returns the id of the chunk that the node
|
||||
// would be stored in, in the database.
|
||||
//
|
||||
// Chunks store 8 consecutive nodes from the same level of the tree,
|
||||
// representing a subtree of height 4. The chunk is identified by the root of
|
||||
// this subtree.
|
||||
func Chunk(nodeId uint64) uint64 {
|
||||
c := nodeId
|
||||
for sharedmath.Level(c)%4 != 3 {
|
||||
c = sharedmath.ParentStep(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
47
tree/log/math/math_test.go
Normal file
47
tree/log/math/math_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package math
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assert(ok bool) {
|
||||
if !ok {
|
||||
panic("Assertion failed.")
|
||||
}
|
||||
}
|
||||
|
||||
func slicesEq(left, right []uint64) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(left); i++ {
|
||||
if left[i] != right[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestMath(t *testing.T) {
|
||||
assert(Root(5) == 7)
|
||||
assert(Right(7, 8) == 11)
|
||||
|
||||
assert(Parent(1, 4) == 3)
|
||||
assert(Parent(5, 4) == 3)
|
||||
|
||||
assert(Sibling(13, 8) == 9)
|
||||
assert(Sibling(9, 8) == 13)
|
||||
|
||||
assert(slicesEq(DirectPath(4, 8), []uint64{5, 3, 7}))
|
||||
assert(slicesEq(Copath(4, 8), []uint64{6, 1, 11}))
|
||||
|
||||
assert(slicesEq(BatchCopath([]uint64{0, 2, 3, 4}, 8), []uint64{2, 10, 13}))
|
||||
assert(slicesEq(BatchCopath([]uint64{0, 2, 3}, 8), []uint64{2, 11}))
|
||||
|
||||
assert(slicesEq(FullSubtrees(7, 6), []uint64{3, 9}))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user