pax_global_header 0000666 0000000 0000000 00000000064 15146734002 0014514 g ustar 00root root 0000000 0000000 52 comment=ba45204806daeb5b1b249d7b35ca0035cba3d641
liboprf-0.9.4/ 0000775 0000000 0000000 00000000000 15146734002 0013163 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/.github/ 0000775 0000000 0000000 00000000000 15146734002 0014523 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/.github/workflows/ 0000775 0000000 0000000 00000000000 15146734002 0016560 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/.github/workflows/codeql-analysis.yml 0000664 0000000 0000000 00000002561 15146734002 0022377 0 ustar 00root root 0000000 0000000 name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 3 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
#- name: Autobuild
# uses: github/codeql-action/autobuild@v1
# βΉοΈ Command-line programs to run using the OS shell.
# π https://git.io/JvXDl
# βοΈ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
- run: |
sudo apt update
sudo apt install -y libsodium-dev pkgconf # build-essential git
# main liboprf
cd src
make test
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
liboprf-0.9.4/.github/workflows/python-app.yml 0000664 0000000 0000000 00000002511 15146734002 0021401 0 ustar 00root root 0000000 0000000 # This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python application
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
sudo apt update
sudo apt install -y libsodium-dev pkgconf # build-essential git
pip install python/ pysodium SecureString
cd src
sudo PREFIX=/usr make install
cd ..
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest -s -v python/tests/test.py
liboprf-0.9.4/.gitignore 0000664 0000000 0000000 00000000245 15146734002 0015154 0 ustar 00root root 0000000 0000000 liboprf*.a
*.o
.arch
bench
*.pdf
matrices
liboprf.so
dkg
thmult
toprf
tuokms
uokms
attack
tp-dkg
python/tests/__pycache__/
src/liboprf-corrupt-dkg.so
src/liboprf.pc
liboprf-0.9.4/LICENSE 0000664 0000000 0000000 00000016744 15146734002 0014204 0 ustar 00root root 0000000 0000000 GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser 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
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
liboprf-0.9.4/README.md 0000664 0000000 0000000 00000010452 15146734002 0014444 0 ustar 00root root 0000000 0000000 # liboprf
## Overview
liboprf is a library for Oblivious Pseudorandom Functions (OPRFs), including support for Threshold OPRFs. It is designed to make advanced cryptographic protocols easy to integrate across applications.
## What is an OPRF?
An Oblivious Pseudorandom Function (OPRF) is a two-party cryptographic primitive involving a sender and receiver who jointly compute a function, `F`, in such a way that:
- The sender holds a secret key `k`
- The receiver provides an input `x`
- The receiver learns `F(k, x)` but nothing about `k`
- The sender learns nothing about `x` or `F(k, x)`
OPRFs are the foundation for many privacy-preserving protocols including:
- Password-based authentication without exposing passwords
- Private set intersection, which allows two parties to find the intersection of their private sets without revealing the full sets
- Privacy-preserving information retrieval, allowing users to get specific information from a database without revealing what information is being retrieved
## Features
### Basic OPRF
liboprf implements the basic OPRF(ristretto255, SHA-512) variant from the [IRTF CFRG Draft](https://github.com/cfrg/draft-irtf-cfrg-voprf/), "Oblivious Pseudorandom Functions (OPRFs) using Prime-Order Groups".
### Threshold OPRF
liboprf implements a threshold OPRF variant based on [Krawczyk et al. (2017)](https://eprint.iacr.org/2017/363) which is compatible with the [CFRG OPRF(ristretto255, SHA-512) variant](#basic-oprf). A threshold implementation distributes trust among multiple servers, requiring a minimum number (threshold) to cooperate for operation. It uses Distributed Key Generation (DKG) protocols, as described below, to distribute secret key shares among multiple servers.
### 3hashTDH
This library also implements the 3hashTDH from [Gu, Jarecki, Kedzior, Nazarian, Xu (2024)](https://eprint.iacr.org/2024/1455) "Threshold PAKE with Security against Compromise of all Servers". This implementation is compatible with the aforementioned [IRTF CFRG OPRF(ristretto255, SHA-512)](#basic-oprf) variant.
### Distributed Key Generation (DKG)
For the [threshold OPRF](#threshold-oprf), liboprf provides:
- **Trusted Party DKG**: An implementation based on Joint Feldman DKG (JF-DKG) from the paper "[Secure Distributed Key Generation for Discrete-Log Based Cryptosystems](https://link.springer.com/article/10.1007/s00145-006-0347-3)" by R. Gennaro, S. Jarecki, Hugo Krawczyk & T. Rabin.
- **Semi-trusted DKG**: Implements Fast-Track Joint Verifiable Secret Sharing (FT-Joint-DL-VSS) described in R. Gennaro, M. O. Rabin, and T. Rabin, "[Simplified VSS and fast-track multiparty computations with applications to threshold cryptography](https://dl.acm.org/doi/10.1145/277697.277716)" In B. A. Coan and Y. Afek, editors, 17th ACM PODC, pages 101β111. ACM, June/July 1998.
### Threshold OPRF Updates
To update a threshold OPRF instantiation, liboprf contains multi-party multiplication described in R. Gennaro, M. O. Rabin, and T. Rabin, "[Simplified VSS and fast-track multiparty computations with applications to threshold cryptography](https://dl.acm.org/doi/10.1145/277697.277716)" In B. A. Coan and Y. Afek, editors, 17th ACM PODC, pages 101β111. ACM, June/July 1998.
## Installation
### Dependencies
- **libsodium**: You must install [libsodium](https://github.com/jedisct1/libsodium) first. libsodium is a cryptographic library that provides a range of cryptographic operations including encryption, decryption, digital signatures, and secure password hashing.
- **pkgconf**: Needed for building the library.
### Building from source
```bash
git clone https://github.com/stef/liboprf.git
cd liboprf/src
make
sudo make install
```
### Python Wrapper
A Python wrapper, `pyoprf`, is provided. Look at [its README](/python/README.md) for installation and usage instructions.
## Funding
This project is funded through [NGI0 Entrust](https://nlnet.nl/entrust), a fund
established by [NLnet](https://nlnet.nl) with financial support from the
European Commission's [Next Generation Internet](https://ngi.eu) program. Learn
more at the [NLnet project page](https://nlnet.nl/project/ThresholdOPRF).
[
](https://nlnet.nl)
[
](https://nlnet.nl/entrust)
liboprf-0.9.4/build.zig 0000664 0000000 0000000 00000005256 15146734002 0015005 0 ustar 00root root 0000000 0000000 const std = @import("std");
const fmt = std.fmt;
const fs = std.fs;
const heap = std.heap;
const mem = std.mem;
const Compile = std.Build.Step.Compile;
fn initLibConfig(b: *std.Build, lib: *Compile) void {
lib.linkLibC();
lib.addIncludePath(b.path("src/"));
lib.addIncludePath(b.path("src/noise_xk/include"));
lib.addIncludePath(b.path("src/noise_xk/include/karmel"));
lib.addIncludePath(b.path("src/noise_xk/include/karmel/minimal"));
//lib.want_lto = false;
}
pub fn build(b: *std.Build) !void {
const root_path = b.pathFromRoot(".");
var cwd = try fs.openDirAbsolute(root_path, .{});
defer cwd.close();
const src_path = "src/";
const src_dir = try fs.Dir.openDir(cwd, src_path, .{ .iterate = true, .no_follow = true });
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const static_lib = b.addLibrary(.{
.name = "liboprf",
.linkage = .static,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
const libsodium_package = b.dependency("libsodium", .{
.target = target,
.optimize = optimize,
.@"test" = false, // `test` is a keyword in zig
.static = true,
.shared = false
});
static_lib.linkLibrary(libsodium_package.artifact("sodium"));
static_lib.addIncludePath(libsodium_package.path("include"));
b.installArtifact(static_lib);
initLibConfig(b, static_lib);
const flags = &.{
"-fvisibility=hidden",
"-fPIC",
"-fwrapv",
};
static_lib.installHeadersDirectory(b.path(src_path ++ "/noise_xk/include"), "oprf/noise_xk", .{});
const allocator = heap.page_allocator;
var walker = try src_dir.walk(allocator);
while (try walker.next()) |entry| {
if(mem.startsWith(u8, entry.path, "tests")) continue;
const name = entry.basename;
if(mem.eql(u8, name, "xk-ex.c")) continue;
if(mem.eql(u8, name, "jni.c")) continue;
if (mem.endsWith(u8, name, ".c")) {
const full_path = try fmt.allocPrint(allocator, "{s}/{s}", .{ src_path, entry.path });
static_lib.addCSourceFile(.{
.file = b.path(full_path),
.flags = flags,
});
} else if (mem.endsWith(u8, name, ".h")) {
const full_path = try fmt.allocPrint(allocator, "{s}/{s}", .{ src_path, entry.path });
if(!mem.startsWith(u8, entry.path, "noise_xk")) {
const full_dest = try fmt.allocPrint(allocator, "oprf/{s}", .{ name });
static_lib.installHeader(b.path(full_path), full_dest);
}
}
}
}
liboprf-0.9.4/build.zig.zon 0000664 0000000 0000000 00000003026 15146734002 0015603 0 ustar 00root root 0000000 0000000 .{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save `, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .liboprf,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.9.1",
.minimum_zig_version = "0.14.1",
.fingerprint = 0x931fd34c8830d0d8,
// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
.libsodium = .{
.url = "git+https://github.com/jedisct1/libsodium.git#09e995c0c85a0026510704b8ce7f5867a09a3841",
.hash = "N-V-__8AAONJYQCM2EvfTRcbTy_p9_e6hTjX34KAk0MItmwe",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}
liboprf-0.9.4/docs/ 0000775 0000000 0000000 00000000000 15146734002 0014113 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/docs/stp-dkg.txt 0000664 0000000 0000000 00000100445 15146734002 0016231 0 ustar 00root root 0000000 0000000 Semi-Trusted Party (STP) Distributed Key Generation (DKG)
This document specifies a proposal for a robust DKG that can work for
small deployments with a small number of parties and infrequent DKG
executions. Robust means that the protocol even succeeds if some
parties cheat and this is detected, but no party aborts. If someone
aborts then the protocol needs to run again, possibly after kicking
out misbehaving parties. This protocol does support maximum 127
peers. This is probably already too much for a non-robust protocol,
but it might work in very special circumstances.
Broadcast is implemented by the semi-trusted party (STP) opening a
channel to each peer secured by the peers long-term encryption
key. Every message - both broadcast and private - are routed through
the STP.
Peer long-term encryption keys can be either TLS-based, or
Noise_XK-based (https://noiseexplorer.com/patterns/XK/). In the latter
case the long-term public keys must be known and validated in advance
by the STP.
Peer long-term signature keys must be known by all participating
parties.
The basis for this protocol is based on the FT-Joint-DL-VSS (fig 7.)
protocol from [GRR98].
------<=[ Rationale ]=>-----------
Traditionally DKGs are used in setting where all parties are equal and
are using the distributed key together, without having any one party
having a different role in the protocol utilizing the shared key. This
does not translate entirely to threshold OPRFs (tOPRF) and protocols
based on these.
In an OPRF there is normally two parties, one holding the key, and
another one holding the input and learning the output. In a tOPRF the
party holding the key is a group of peers that hold shares of the key
in a threshold setting. In a special case, that of updatable threshold
OPRFs the updating might be done by a semi-trusted 3rd party. In that
case the semi-trusted 3rd party is merely honest-but-curious, but
unable to learn anything about the input nor really the output of the
OPRF, while being able to update the key of the OPRF. This can be
handy for automated, regular key-updates. For updating the key, the
participants must generate a new key, and this can be orchestrated by
a STP acting as the broadcast and general communication medium between
the parties.
------<=[ Difference to the [GRR98] paper ]=>-----------
In the original paper fig. 7 describing the FT-Joint-DL-VSS protocol,
does not explicitly check whether the π(Ξ±_i,Ο_i) commitments match
those broadcast. However the algorithm New-VSS from fig. 1 does
so. And since FT-Joint-DL-VSS is supposedly based on New-VSS we do
check the commitment, broadcast and check complaints and dealers of
verified complaints are disqualified.
P_i after generating C_ij and share s_ij,r_ij, broadcasts a hash of all
(concatenated) values C_i1,...,C_in.
After broadcasting C_ij and sharing s_ij,r_ij, before checking VSPS
all participants check that the hash value broadcast in Round 1 by
P_i, for i=1,...,n, corresponds to the C_ij values broadcast by P_i in
Round 2. If this is not the case, P_j aborts.
------<=[ Prototocol Phases ]=>-----------
The protocol has the following phases:
1. Initialization and introduction: announcement of parameters n, t,
long-term signing public keys of all participants, their
ordering, and encryption public keys, Establishment of a jointly
generated session id.
2. AKE - Setup secure P2P channels: to establish protected channels
between all peers.
3. core DKG
4. Complaint analysis: In case of invalid commitments establishment
of identity of cheaters.
5. Final verification of transcript and completion of
protocol
------<=[ Simplified API ]=>-----------
Since the protocol consists of many steps, the API is abstracted to
the following schema:
0. Initialize
While not done and not error:
1. Allocate input buffer(s)
2. input = receive()
3. allocate output buffer
4. run next step of protocol
5. if there is output: send(output)
6. Post-processing
This simple schema simplifies the load of an implementer using this
protocol, while at the same time reducing opportunities for errors and
provides strict security. It also allows full abstraction of the
underlying communication media.
The reference implementation in stp-dkg.c follows this schema for both
the STP and the peers.
------<=[ Protocol transcript ]=>-----------
Transcript - all broadcast messages are accumulated into a transcript
by each peer and the semi-trusted party, at the end of the protocol
all parties except for the STP publish their signed transcripts and
only if all signatures are correct and the transcripts match, is the
protocol successful. The STP uses its own transcript to learn the
of the parties agreement.
The transcript is a hash, which is initialized with the string:
"stp vss dkg session transcript"
in pseudo-code:
transcript_state = hash_init("stp vss dkg session transcript")
Updating the transcript first updates the hash with the canonical
32bit size of the message to be added to the transcript, then the
message itself is added to the hash.
transcript_state = hash_update(transcript_state, I2OSP(len(msg))
transcript_state = hash_update(transcript_state, msg)
The signature of each message is similarly added to the transcript.
A function `update_ts` can be used as a high-level interface to
updating the transcript with messages and their signatures:
```
update_ts(state,msg,sig)
state = hash_update(state, I2OSP(len(msg))
state = hash_update(state, msg)
state = hash_update(state, I2OSP(len(sig))
state = hash_update(state, sig)
return state
```
------<=[ Session id ]=>-----------
Every execution of the protocol starts by the participants
establishing a unique and fresh session id, this is to ensure that no
messages can be replayed. The session id is a 256 bit (32B) random
value of cryptographic quality entropy.
Each peer P_i chooses a 256 bit nonce, signs it, broadcast it. Then
everyone (including the STP) verifies the signatures, aborts if any
signatures fail. And then everyone uses the hash of the concatenation
of all nonces as the session identifier.
The session_id is established as early as possible in the first
(initialization) phase of protocol. The STP learns (and starts using)
it in step 2, and the peers verify if it is correct and start using it
in step 3. Every message sent after step 3 MUST contain a valid
session_id.
```
nonce_i = random_bytes(32)
signed_nonce_i = sign(i | nonce, ltsigkey_i)
broadcast(signed_nonce_i)
acc = ""
for i in 1..n:
signed_nonce_i = recv(i)
i', nonce_i = verify(signed_nonce_i, ltsigpub_i) or abort()
assert i == i'
acc = acc | nonce_i
sessionid = h(acc)
```
------<=[ Message header ]=>-----------
All messages have a message header:
uint8 signature[32]
uint0 sign_here[0]
uint8 type = 0x80
uint8 version = 0
uint8 messageno
uint32 len
uint8 from
uint8 to
uint64 timestamp
uint8 sessionid[32]
The header begins with the signature of the sender over the rest of
the header and all of the data.
The field sign_here is a zero-bit field, only used for addressing the
start of the data to be signed or verified.
The next field is the protocol type identifier. STP-DKG has an
identifier value of 128 (0x80). And a version number of 0 for the current
version.
The following field in the header is really a state identifier. A
recipient MUST verify that the messageno is matching with the expected
number related to the state of the protocol.
The len field MUST be equal to the size of the packet received on the
network including the packet header.
The `from` field is simply the index of the peer, since peers are
indexed starting from 1, the value 0 is used for the semi-trusted
party. Any value greater than 128 is invalid. The state defines from
whom to receive messages, and thus the from field MUST be validated
against these expectations.
The `to` field is similar to the `from` field, with the difference
that the value 0xff is reserved for broadcast messages. The peer (or
STP) MUST validate that it is indeed the recipient of a given message.
The timestamp field is just a 64bit timestamp as seconds elapsed since
1970/01/01, for peers that have no accurate clock themselves but do
have an RTC, the first initiating message from the STP SHOULD be used
as a reference for synchronizing during the protocol.
------<=[ Message signatures ]=>-----------
Every message MUST be signed using the sender peers long-term signing
key. The signature is made over the complete message including the
header, excluding the signature field itself, starting from the
zero-width sign_here field.
------<=[ Verifying messages ]=>-----------
Whenever a message is received by any participant, they first MUST
check the correctness of the signature:
```
msg = recv()
sign_pk = sign_keys[expected_sender_id]
assert(verify(sign_pk, msg.sign_her, msg.signature))
```
The recipient MUST also assert the correctness of all the other header
fields:
```
assert(msg.type == 0x80)
assert(msg.version == 0)
assert(msg.sessionid == sessionid)
assert(msg.messageno == expected_messageno)
assert(msg.from == expected_sender_id)
assert(msg.to == (own_peer_id or 0xff))
assert(ref_ts <= msg.ts < ref_ts + timeout))
ref_ts = msg.ts
```
The value `timeout` should be configurable and be set to the smallest
value that doesn't cause protocol aborts due to slow responses.
If at any step of the protocol any participant receives one or more
messages that fail these checks, the participant MUST abort the
protocol and log all violations and if possible alert the user.
------<=[ Message transmission ]=>-----------
A higher level message transmission interface can be provided, for
sending:
```
msg = send_msg(msgno, from, to, sign_sk, session_id, data)
ts = timestamp()
msg = type: 0x80, version: 0, messageno: msgno, len: len(header) + len(data) + len(sig), from: from, to: to, ts: ts, data
msg.sig = sign(sign_sk, msg.sign_here)
return msg
```
And for validating incoming messages:
```
data = recv_msg(msgno, from, to, ref_ts, sign_pk, session_id, msg)
assert(verify(sign_pk, msg.sign_here, msg.sig)
assert(msg.type == 0x80)
assert(msg.version == 0)
assert(msg.messageno == msgno)
assert(msg.len == len(msg|sig))
assert(msg.from == from)
assert(msg.to == to)
assert(ref_ts < msg.ts < ref_ts + timeout))
if msg.to == 0xff:
update_ts(state,msg,sig)
```
The parameters `msgno`, `from`, `to`, `session_id` should be the
values expected according to the current protocol state.
------<=[ Cheater detection ]=>-----------
The STP and the peers MUST report to the user all errors that can
identify cheating peers in any given step. For each detected cheating
peer the STP MUST record the following information:
- the current protocol step,
- the violating peer,
- the other peer involved, and
- the type of violation
In order to detect other misbehaving peers in the current step,
processing for the rest of the SHOULD peers continue until the end of
the current step. Any further violations should be recorded as above.
Before the next message to the peers is sent, the STP MUST check if
there are no noted hard violations, if so the STP aborts and reports
all violators with their parameters to the user. Soft violations -
corruptions robustly handled by the protocol - only need to be
reported, they do not cause an abort.
Abort conditions include any errors detected by recv_msg(), failure to
verify the hash of the commitments, or when the number of complaints
is more than t for one peer, or more than t^2 in total. Soft
violations are failures of dealers VSPS checks and shares not matching
their commitments.
Participants should log all broadcast interactions, so that any
post-mortem investigation can identify cheaters.
------<=[ Second generator point ]=>-----------
For the homomorphic commitment this protocol requires a second
generator on the group. We generate it in the following manner:
h = voprf_hash_to_group(blake2b("nothing up my sleeve number"))
Where voprf_hash_to_groups is according to [RFC9497].
------<=[ The protocol ]=>-----------
------<=[ 0. Precondition ]=>-----------
Peers use TLS or STP knows long-term encryption keys for all peers.
STP and peers MUST know long-term signing keys of all peers.
------<=[ 1. DKG Announcement - STP(peers, t, proto_name) ]=>----------
The protocol starts by asking the semi-trusted party (STP) to initiate
a new run of the DKG protocol by providing it with:
- a list of the peers,
- a threshold value, and
- protocol instance name used as a domain separation token.
The STP then sanity checks these parameters:
```
n = len(peers)
assert(10)
```
The STP then generates a hash of the DST.
The STP then creates a broadcast message containing the hash (so that
the message is always of fixed size) of the DST, the values n and t
and its own public signing key:
```
dst_str = "STP VSS DKG for protocol %s" % proto_name
dst = hash(I2OSP(len(dst_str)) | dst_str | n | t)
sessionid = random_bytes(32)
data = {stp_sign_pk, dst, n, t}
msg_0 = send_msg(0, 0, 0xff, stp_sign_sk, sessionid, data)
broadcast(msg_0)
```
Note that the STP also generates a temporary session id, which is used
until the parties agree on a joint session id.
The STPs copy of the transcript is initialized by the STP, and updated
with the value of the 1st broadcast message:
```
state = hash_init("stp vss dkg session transcript")
state = update_ts(state, msg, sig)
```
Since the order of the peers is random, and important for the protocol
a custom message is created for each peer by the STP and sent
individually notifying each peer of their index in this protocol
run. The msg.to field conveys the index of the peer. Additionally the
hashes of the long-term signing public keys of the other peers are
also sent along so that each of the peers can load the corresponding
long-term signing and noise public keys.
```
# sending each peer its index
pkhashes = ""
for i in 1..n:
pkhashes = pkhashes | hash(ltsigpk[i])
for i in 1..n:
msg_1 = send_msg(1, 0, i, stp_sign_sk, session_id, {pkhashes})
send(i, msg_1)
```
------<=[ 2. each peer(msg_0) ]=>------------
In this step each peer receives the initial parameter broadcast,
verifies it, initializes the transcript and adds the initial
message. Then receives the message assigning its index.
```
msg_0 = recv()
assert(recv_msg(0, 0, 0xff, ref_ts, msg.data.stp_sign_pk, msg.sessionid, msg_0))
sessionid = msg.sessionid
```
If the peer has no accurate internal clock but has at least an RTC, it
SHOULD set the ref_ts to the message timestamp:
```
ref_ts = msg_0.ts
```
Furthermore the peer MUST also verify that the n & t parameters are
sane, and if possible the peer SHOULD also check if the temporary
STP-assigned session id is fresh (if it is not possible, isfresh() MAY
always return true.
```
assert(1 < n <= 128)
assert(2 <= msg_0.t < n)
assert(isfresh(msg_0,sessionid))
```
The transcript MUST be initialized by the peer, and updated with the
value of the 1st broadcast message:
```
state = hash_init("stp vss dkg session transcript")
state = update_ts(state, msg, sig)
```
After processing the broadcast message from the STP, the peers also
have to process the second message from the STP in which they are
assigned their index.
```
msg1 = recv()
peerids = recv_msg(1, 0, msg1.to, ref_ts, stp_sign_pk, session_id, msg_1)
assert(msg1.to <= n and msg1.to > 0)
peerid = msg.to
peers_noise_pks = []
peers_sign_pks = []
for i in 1..n
peers_sign_pks[i], peers_noise_pks[i] = keyloader(peerids[i])
```
------<=[ 3. peers broadcast fresh session nonce ]=>-------------
All peers broadcast generate a fresh session nonce for use in the
session_id to all peers via the STP.
```
nonce_i = random_bytes(32)
msg_2 = send_msg(2, peerid, 0xff, peer_sign_sk, session_id, nonce_i)
broadcast(msg_2)
```
------<=[ 4. STP collects and broadcasts messages ]=>-------------
Then the STP acts as a broadcast medium on the messages.
This is a recurring pattern where the STP acts in its broadcasting
intermediary role:
1. receives the messages from each peer
2. validates the message using recv_msg()
3. extracts all nonces (or other information depending on the
current step) for usage by the STP in the rest of the protocol
4. concatenates all received messages into a new message
5. signs the message of messages
6. adds this the message of messages and its signature to the transcript
7. sends it to all peers
In this case the STP calculates the session id, which is the hash of
the concatenation of all nonces in order of their sending peers index
(with the STP always having index 0). The STP already uses the joint
session_id to wrap all the peers messages into its broadcast envelope.
```
peer_sig_pks = []
msgs = []
nonces = nonce_stp
for i in 1..N
msg_2 = recv()
nonce_i, = recv_msg(2, i, 0xff, ref_ts, msg_2.data.peer_sign_pk, nonce_i, msg_2)
msgs = msgs | msg_2
nonces = nonces | nonce_i
session_id = hash(nonces)
msg_3 = send_msg(3, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_3)
broadcast(msg_3)
```
------<=[ 5. finish init phase, start AKE phase ]=>-------
In this step all peers process the broadcast nonces received from all
peers, finishing the initial phase.
Every peer also verifies if the joint session_id matches the one in
the STP broadcast envelope. From now on, every participant has this
joint session id and uses it for all further messages.
This step also marks the start of the next phase. In this AKE phase
each peer initiates a noise_xk handshake with all other peers
(including themselves for simplicity and thus security).
```
msg_3 = recv()
msgs = recv_msg(3, 0, 0xff, ref_ts, stp_sign_pk, msg_3.session_id, msg_3)
state = update_ts(state, msg_3)
nonces = []
for i in 1..N
msg, sig = msgs[i]
nonce_i, = recv_msg(2, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg, sig)
nonces = nonces | nonce_i
session_id = hash(nonces)
assert(msg_3.session_id == session_id)
send_session = []
for i in 1..N
send_session[i], handshake1 = noisexk_initiator_session(peer_noise_sk, peers_noise_pks[i])
msg, sig = send_msg(4,peerid,i,peer_sign_sk, session_id, handshake1)
send(msg | sig)
```
------<=[ 6. STP routes handshakes from each peer to each peer ]=>-------
The STP receives all 1st handshake messages from all peers and routes
them correctly to their destination. These messages are not broadcast,
each of them is an encrypted P2P message. The benefit of the STP
forming a star topology here is, that the peers can be on very
different physical networks (wifi, lora, uart, nfc, bluetooth, usb,
etc) and only the STP needs to be able to connect to all of them.
```
for i in 1..N
handshakes = recv(i)
for j in 1..N
send(j, handshakes[j])
```
------<=[ 7. each peer responds to each handshake each peer ]=>-------
Peer receives noise handshake1 from each peer and responds with
handshake2 answer to each peer.
```
for i in 1..N
msg, sig = recv()
handshake1 = recv_msg(4, i, peerid, ref_ts, peers_sign_pks[i], session_id, msg, sig)
receive_session[i], handshake2 = noisexk_responder_session(peer_noise_sk, handshake1)
msg, sig = send_msg(5, peerid, i, peer_sign_sk, session_id, handshake2)
send(msg | sig)
```
------<=[ 8. STP routes handshakes from each peer to each peer ]=>-------
STP just routes all P2P messages from all peers to the correct
recipients of the messages.
```
for i in 1..N
handshakes = recv(i)
for j in 1..N
send(j, handshakes[j])
```
------<=[ 9. end of AKE phase, start of core DKG phase ]=>-------
Peers complete the noise handshake.
```
for i in 1..N
msg, sig = recv()
handshake3 = recv_msg(5, i, peerid, ref_ts, peers_sign_pks[i], session_id, msg, sig)
send_session[i] = noisexk_initiator_session_complete(send_session[i], handshake3)
```
Each peer has a confidential connection with every peer (including
self, for simplicity)
The one time this channel is used, is when distributing the dealer
shares. The sender uses the initiator interface of the noise session,
and the receiver uses the responder interface.
This step starts the core FT-Joint-DL-VSS protocol as per [GRR98]
fig. 7:
Player P_i chooses a random value r_i and shares it using the DL-VSS
protocol, Denote by Ξ±_i,j , Ο_i,j the share player P_i gives to player
P_j, and the value π_i,j = g^(Ξ±_i,j)*h^(Ο_i,j) is the dealers
commitment to share Ξ±_i,j , Ο_i,j.
The Noise handshake is finalized by encrypting one last empty message,
which sets the final shared secret. This is needed in case later
someone accuses this peer with their shares not matching their
commitment, in which case the peer can reveal this final encryption
key and prove everyone that the accuser was lying or not.
The shares Ξ±_i,j ,Ο_i,j are encrypted using the final Noise key for
the recipient, and a key-committing HMAC is also calculated over the
encrypted shares, since the Noise implementation we use does use
Poly1305 which is not key-committing and thus would allow the dealer
to cheat.
The encrypted shares and their commitments π_i,j are stored by the
peer for later distribution.
The HMAC for each encrypted share is broadcast together with
the hash of the concatenation of all A_i,j commitments:
C_i = hash(A_i0 | A_i1 | .. | A_in)
```
encrypted_shares = []
hmacs = []
for i in 1..N
encrypted_shares[i] = noise_send(send_session[i], share[i])
hmacs[i] = hmac(send_session[i].key, encrypted_shares[i])
C_i = hash(commitments)
msg_6 = send_msg(6, peerid, 0xff, peer_sign_sk, session_id, {C_i, hmacs})
```
------<=[ 10. STP collects and broadcasts all C_i commitments ]=>-------
This is another broadcast pattern instance:
receive-verify-collect-sign-transcript-broadcast. The STP keeps a copy
of all commitment-hashes and share-HMACs being broadcast.
The STP keeps a local copy of all commitment hashes and HMACs for
later verification.
```
C_hashes = []
hmacs = []
msgs = []
for i in 1..N
msg_6 = recv(i)
C_hashes[i], hmacs[i] = recv_msg(6, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_6)
msgs = msgs | msg_6
msg_7 = send_msg(7, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_7)
broadcast(msg_7)
```
------<=[ 11. Peers receive commitment hashes & HMACs ]=>-------
The peers receive all C_i commitment hashes and share-HMACs and
broadcast their A commitment vectors:
```
msg_7 = recv()
msgs = recv_msg(7, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_7)
state = update_ts(state, msg_7)
C_hashes = []
share_macs = []
for i in 1..N
msg_6 = msgs[i]
C_hashes[i], share_macs[i] = recv_msg(6, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_6)
msgs = msgs | msg_6
msg_8 = send_msg(8, peerid, 0xff, peer_sign_sk, session_id, A)
```
------<=[ 12. STP broadcasts all commitments ]=>-------
This is a classical STP broadcast step. Besides keeping a copy of all
commitments, the STP does also verify the commitment hashes and does
an FT-VSPS check on the commitments.
The STP verifies the VSPS property of the sum of the shared secrets by
running VSPS-Check on π_i,..,π_n where
π_j = Ξ π_i,j
i
If this check fails the STP runs VSPS-Checks on each individual
sharing. These checks are informational, and should guide the operator
in detecting and deterring cheaters.
```
commitments[][]
msgs = []
for i in 1..N
msg_8 = recv(i)
commitments[i] = recv_msg(8, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_8)
msgs = msgs | msg_6
if C_hashes != hash(commitments[i])
report(i)
C = []
for i in 1..n
C[i] = sum(commitments[j][i] for j in 1..n)
if vsps(C) fails:
for i..n
if vsps(commitments[i]) fails report(i)
msg_9 = send_msg(9, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_9)
broadcast(msg_9)
```
------<=[ 13. Peers receive commitments, send shares ]=>-------
The peers receive the broadcast commitments of all dealers, they check
the commitment hashes and abort if they don't match, otherwise they
store the commitments for the next step.
Peers privately send the shares to each recipient.
```
msg_9 = recv()
msgs = recv_msg(9, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_9)
state = update_ts(state, msg_9)
commitments = [][]
for i in 1..N
msg_8 = msgs[i]
commitments[i] = recv_msg(8, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_8)
assert C_hashes[i] == hash(commitments[i])
msg_10s = []
for i in 1..N
msg, sig = send_msg(9, peerid, i, peer_sign_sk, session_id, encrypted_shares[i])
send(msg,sig)
```
------<=[ 14. STP routes shares to recipients ]=>-------
STP just routes all P2P messages from all peers to the correct
recipients of the messages.
```
for i in 1..N
msgs = recv(i)
for j in 1..N
send(j, msgs[j])
```
------<=[ 15. each peer receives shares & check commitments ]=>-------
The peers receive the private messages containing their shares. The
peers verify the shares against the previously broadcast commitment
vectors. For each
π_i,j == g^(Ξ±_i,j) * h^(Ο_i,j)
pair that fails, a complaint against the peer producing the
conflicting commitment and share is logged in an array, which is
broadcast to everyone.
```
s = []
for i in 1..N
msg = recv()
pkt = recv_msg(9, i, peerid, ref_ts, peer_sign_pks[i], session_id, msg)
encrypted_share, final_noise_handshake = pkt
recv_session[i] = noise_session_decrypt(recv_session[i], final_noise_handshake)
Ξ±[i,peerid],Ο[i,peerid] = noise_recv(receive_session[i], pkt)
complaints = []
for i in 1..N
if commitment[i,peerid] != g^(Ξ±[i,peerid])*h^(Ο[i,peerid])
complaints = complaints | i
msg, sig = send_msg(10, peerid, 0xff, peer_sign_sk, session_id, len(complaints) | complaints)
send(msg | sig)
```
------<=[ 16. STP collects complaints ]=>-------
Another receive-verify-collect-sign-transcribe-broadcast
instantiation. The STP keeps a copy of all complaints.
If any peer complaints about more than t peers, that complaining peer
is a cheater, and must be disqualified. Furthermore if there are in
total more than t^2 complaints there are multiple cheaters and the
protocol must be aborted and new peers must be chosen in case a rerun
is initiated.
```
complaints = []
msgs = []
for i in 1..N
msg_10 = recv(i)
complaints_i = recv_msg(10, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_10)
assert(len(complaints_i) < t)
complaints = complaints | complaints_i
msgs = msgs | msg_10
assert(len(complaints) < t^2)
msg_11 = send_msg(11, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_11)
broadcast(msg_11)
```
The next phase of the protocol depends on the number of complaints
received, if none then the next phase is finishing, otherwise the next
phase is complaint analysis.
If the next STP phase is complaint analysis (there are complaints) the
next input buffer size depends on the number of complaints against
each peer.
Each complaint is answered by the encrypted shares and the symmetric
encryption key used to encrypt these shares of the accused belonging to
the complainer. Each accused packs all answers into one message.
------<=[ 17. Each peer receives all complaints ]=>-------
All complaint messages broadcast are received by each peer. If peer_i
is being complained about by peer_j, peer_i broadcasts the encrypted
shares and the symmetric encryption key that was used to encrypt them.
If there are no complaints at all the peers skip over to the final
phase step 20., otherwise they engage in the complaint analysis phase.
```
msg_11 = recv()
msgs = recv_msg(11, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_11)
state = update_ts(state, msg_11)
defenses = []
for i in 1..N
msg = msgs[i]
complaints_len, complaints = recv_msg(10, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg)
for k in 0..complaints_len
if complaints[k] == peerid
# complaint about current peer, publish key used to encrypt Ξ±_i,j , Ο_i,j
defenses = defenses | {i, send_session[i].key, encrypted_shares[i]}
if len(keys) > 0
msg_12 = send_msg(12, peer, 0xff, peer_sign_sk, session_id, keys)
send(msg_12)
```
------<=[ 18. STP collects all defenses, verifies&broadcasts them ]=>-------
STP checks if all complaints lodged earlier are answered by the
correct encrypted shares and their keys, by first checking if the
previously recorded MAC successfully verifies the encrypted share with
the disclosed key, and then decrypts the share with this key, and
checks if this satisfies the previously recorded commitment for this
share. If it does, the accuser is reported as a cheater, if the
commitment doesn't match the share, then the accused dealer is
disqualified from the protocol and its shares will not contribute to
the final shared secret.
```
msgs = []
for i in 1..N
if len(complaints[i]) < 1
continue
msg = recv(i)
defenses = recv_msg(12, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg)
msgs = msgs | msg
assert(len(defenses) == len(complaints[i]))
for j, key, encrypted_share in defenses
assert j==i
if hmacs[i][peerid] == hmac(key, encrypted_share)
report(i)
s,r=decrypt(key, encrypted_share]) or report(i)
if commitments[i][peerid] != g^s * h^r
report(i)
msg_13 = send_msg(13, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_13)
broadcast(msg_13)
```
------<=[ 19. Peers receive and check all defenses ]=>-------
Peers receive the encrypted shares, and their encryption keys, and
then run essentially the same step as the STP in the previous step,
then they directly skip to the final phase in the next step.
------<=[ 20. Peers VSPS check, calculate shares and finish ]=>-------
Players verify the VSPS property of the sum of the shared secrets by
running VSPS-Check on π_i,..,π_n where
π_j = Ξ π_i,j
i
If this check fails the players run VSPS-Check on each individual
sharing. Any player that fails this check is disqualified. The number
of all qualified peers (from this step, and the complaint analysis) is
checked that is greater than 1 and then number of disqualified peers
is less than t. If this fails the protocol aborts.
The shares dealt by the qualified peers are summed, creating the final
share. The commitment for this final share is calculated.
Each peer calculates the final transcripts and broadcasts this
together with the final commitments to all parties.
```
C = []
for i in 1..n
C[i] = sum(commitments[j][i] for j in 1..n if peer[i] is qualified)
if vsps(C) fails:
for i in 1..n
if vsps(commitments[i]) fails disqualify(i)
s = 0, r = 0
for i in 1..n
if i is disqualfied: continue
s += shares[i]_s
r += shares[i]_r
C = g^s * h^r
transcript = final_ts(state)
msg_20 = send_msg(20, peerid, 0, peer_sign_sk, session_id, {transcript, C})
send(msg_20)
```
------<=[ 20. STP receives all and verifies transcripts ]=>-------
STP receives all transcripts, and asserts that they all match its own
transcript, it reports if any transcript mismatch is detected. It also
does a final VSPS check on the commitments seen.
```
transcript = final_ts(state)
msgs = []
commitments = []
for i in 1..N
msg, sig = recv(i)
ts, c[i] = recv_msg(20, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg, sig)
if ts != transcript
report transcript mismatch
msgs = msgs | {msg, sig}
if vsps(commitments) fails:
report failure
------<=[ 21. END, peers set their share ]=>-------
All peers receive the broadcasts transcripts and commitments, they run
the same check as the STP in the previous step and abort if any of
these fails. Otherwise the protocol completes successfully, and the
peers can store the final shares and commitment together with the list
of long-term public signing keys of all peers.
------<=[ References ]=>-------
[GRR98] R. Gennaro, M. O. Rabin, and T. Rabin. "Simplified VSS and
fact-track multiparty computations with applications to threshold
cryptography" In B. A. Coan and Y. Afek, editors, 17th ACM PODC, pages
101β111. ACM, June / July 1998
[RFC9497] RFC 9497: Oblivious Pseudorandom Functions (OPRFs) Using
Prime-Order Groups
liboprf-0.9.4/docs/stp-update.txt 0000664 0000000 0000000 00000136005 15146734002 0016747 0 ustar 00root root 0000000 0000000 Semi-Trusted-Party (STP) threshold-OPRF key-update Protocol
[[[ note this is a draft, which matches the current implementation, changes are still expected ]]]
This document specifies a proposal for a semi-robust threshold-OPRF
key- update protocol that can work for small deployments with a small
number of parties and infrequent executions. Semi-robust means that
the protocol can fail in the DKG phase if any party aborts, but can
still succeed later if some parties are detected cheating or
aborting. If someone aborts in the first DKG phase then the protocol
needs to run again, possibly after kicking out misbehaving
parties. This protocol does support maximum 127 peers. This is
probably way too much for a generic threshold protocol, but it
might work in very special circumstances.
Broadcast is implemented by the semi-trusted party (STP) opening a
channel to each peer secured by the peers long-term encryption
key. Every message is routed through the STP.
Peer long-term encryption keys can be either TLS-based, or
Noise_XK-based (https://noiseexplorer.com/patterns/XK/). In the latter
case the long-term public keys must be known and validated in advance
by the STP.
The long-term signing keys of peers MUST be known by all parties
before the start of the protocol.
This protocol is based on the threshold updatable OPRF as described in
section 6.2 of [JKR19].
The basic building blocks for this protocol are the FT-Joint-DL-VSS
protocol for the DKGs as specified in Appendix H figure 7 of [GRR98],
and the Multi-party Multiplication protocol which given the sharings
of secret `a` and secret `b` generates a sharing of the product `aΒ·b`
without learning anything about either secret. The multi-party
multiplication is based on the FT-Mult protocol shown in Fig. 6 from
[GRR98].
The update protocol from old key kc to new key kc' works as follows
(quote from the JKR paper):
The distributed update protocol assumes that n servers S1,...,Sn have
a sharing (k1,..., kn) of a key k. To produce a new key kβ² the servers
jointly generate a sharing Ο1,...,Οn of a random secret Ο βZq and run
distributed multiplication FT-Mult(kc,Ο) to generate shares
kβ²1,...,kβ²n of the new key defined as kβ² = Ο Β· k. Finally, each server
Si sends to clients its share Οi from which the recipient reconstructs
Ο and sets β:= Ο [= kβ²/k].
------<=[ Roles ]=>-----------
There is three roles in this protocol:
- semi-trusted party (STP) orchestrating the communication between
all other participants.
- dealers: exactly 2t+1 participants (having shares of both kc and p)
are acting as dealers
- all participants (peers and STP)
------<=[ Prototocol Phases ]=>-----------
Some of the following phases are optional and only executed if the
previous step revealed a misbehaving party, these phases make the
protocol robust to also successfully complete in the presence of
misbehaving parties. Phases 3, 5, 7, and 9 are these optional
corrective phases.
0. pre-condition: all parties know the long-term signing keys of each
other. STP ensures that n >= 2t+1 and at least 2t+1 of them hold
shares of the old key kc, otherwise abort.
1. Initialization, sharing of basic parameters, setup of p2p noise
channels.
2. execute STP-DKG for all peers that hold a share of kc, calculating
sharing of Ο, if a fast-track VSPS check fails disqualify the
cheaters. Abort if some other hard-fail occurs during the DKG.
3. DKG dealers defend against complaints by revealing contested
shares. Based on this peers disqualify any misbehaving peers from
contributing to the result of the DKG.
4. execute the FT-Mult protocol, to calculate FT-Mult(kc, Ο),
generating sharings of kc'.
5. Verify the commitments matching the shares of step 2, if there is
failures to verify run a recovery procedure to correct the failing
shares/commitments.
6. Dealers run ZK proofs on the correctness of their π_i0
commitment. If any of these fails, run a recovery procedure to
correct the commitment.
7. Recover from failed ZK proofs, reconstructing the failing dealer
P_i secret Ξ»_iΞ±_iΞ²_i and recalculating the correct π_i0 commitment.
8. Aggregate the final shares and commitments. Run a Fast-track VSPS
check on the final commitments for the shares of the
multiplications. If any of these fails, run a recovery procedure.
9. Recovery of Ξ»_iΞ±_iΞ²_i for any dealer who fails the VSPS check and
deterministically re-share the recovered secret.
10. Finalization of computation: parties send their Ο_i shares to the
STP which reconstructs Ο and computes β=1/p.Parties replace their
kc share with their kc` share.
------<=[ Simplified API ]=>-----------
Since the protocol consists of many steps, it is recommended to
abstract the API to the following schema:
0. Initialize
While not done and not error:
1. Allocate input buffers
2. input = receive()
3. allocate output buffer
4. run next step of protocol
5. if there is output: send(output)
6. Post-processing
This simple schema simplifies the load of an implementer using this
protocol, reducing opportunities for errors and provides strict
security. It also allows full abstraction of the underlying
communication media.
The reference implementation in toprf-update.c follows this schema for
both the STP and the peers.
------<=[ Protocol transcript ]=>-----------
Transcript - all broadcast messages are accumulated into a transcript
by each peer and the semi-trusted party, at the end of the protocol
all parties publish their signed transcripts and only if all
signatures are correct and the transcripts match, is the protocol
successful.
The transcript is a hash, that is initialized with the string:
"stp update session transcript"
in pseudo-code:
```
transcript_state = hash_init()
transcript_state = hash_update("stp update session transcript")
```
Updating the transcript first updates the hash with the canonical
32bit size of the message to be added to the transcript, then the
message itself is added to the hash.
```
transcript_state = hash_update(transcript_state, I2OSP(len(msg))
transcript_state = hash_update(transcript_state, msg)
```
A function `update_ts` can be used as a high-level interface to
updating the transcript with messages:
```
update_ts(state,msg)
state = hash_update(state, I2OSP(len(msg))
state = hash_update(state, msg)
return state
```
------<=[ Session id ]=>-----------
Every execution of the protocol starts by the participants
establishing a unique and fresh session id, this is to ensure that no
messages can be replayed. The session id is a 256 bit (32B) random
value of cryptographic quality.
Every message sent MUST contain a valid session id.
The session id is established in the very first steps of the protocol,
where each peer generates a 32B nonce, and broadcasts this, and - upon
reception of all peers - hashes the STP session id nonce together with
the concatenation of the peers nonces (in order of the peers) to
establish the final session id.
------<=[ Message header ]=>-----------
All messages have a message header:
uint8 signature[32]
uint0 sign_here[0]
uint8 type = 0x81
uint8 version = 0
uint8 messageno
uint32 len
uint8 from
uint8 to
uint64 timestamp
uint8 sessionid[32]
The header begins with the signature of the sender over the rest of
the header and all of the data.
The field sign_here is a zero-bit field, only used for addressing the
start of the data to be signed or verified.
The next field is the protocol type identifier. STP Update has an
identifier value of 129 (0x81). And currently a version number of 0.
The following field in the header is really a state identifier. A
recipient MUST verify that the messageno is matching with the expected
number related to the state of the protocol.
The len field MUST be equal to the size of the packet received on the
network including the packet header.
The `from` field is simply the index of the peer, since peers are
indexed starting from 1, the value 0 is used for the semi-trusted
party. Any value greater than 128 is invalid. The state defines from
whom to receive messages, and thus the from field MUST be validated
against these expectations.
The `to` field is similar to the `from` field, with the difference
that the value 0xff is reserved for broadcast messages. The peer (or
STP) MUST validate that it is indeed the recipient of a given message.
The timestamp field is just a 64bit timestamp as seconds elapsed since
1970/01/01, for peers that have no accurate clock themselves but do
have an RTC, the first initiating message from the STP SHOULD be used
as a reference for synchronizing during the protocol.
------<=[ Message signatures ]=>-----------
Every message MUST be signed using the sender peers long-term signing
key. The signature is made over the complete message.
------<=[ Verifying messages ]=>-----------
Whenever a message is received by any participant, they first MUST
check the correctness of the signature:
```
msg = recv()
sign_pk = sign_keys[expected_sender_id]
assert(verify(sign_pk, msg))
```
The recipient MUST also assert the correctness of all the other header
fields:
```
assert(msg.type == 0x81)
assert(msg.version == 0)
assert(msg.sessionid == sessionid)
assert(msg.messageno == expected_messageno)
assert(msg.from == expected_sender_id)
assert(msg.to == (own_peer_id or 0xff))
assert(ref_ts <= msg.ts < ref_ts + timeout))
ref_ts = msg.ts
```
The value `timeout` should be configurable and be set to the smallest
value that doesn't cause protocol aborts due to slow responses.
If at any step of the protocol any participant receives one or more
messages that fail these checks, the participant MUST abort the
protocol and log all violations and if possible alert the user.
------<=[ Message transmission ]=>-----------
A higher level message transmission interface can be provided, for
sending:
```
msg = send_msg(msgno, from, to, sign_sk, session_id, data)
ts = timestamp()
msg = type: 0x81, version: 0, messageno: msgno, len: len(header) + len(data) + len(sig), from: from, to: to, ts: ts, data
sig = sign(sign_sk, msg)
return msg
```
And for validating incoming messages:
```
data = recv_msg(msgno, from, to, ref_ts, sign_pk, session_id, msg)
assert(verify(sign_pk, msg)
assert(msg.type == 0x81)
assert(msg.version == 0)
assert(msg.messageno == msgno)
assert(msg.len == len(msg))
assert(msg.from == from)
assert(msg.to == to)
assert(ref_ts < msg.ts < ref_ts + timeout))
if msg.to == 0xff:
update_ts(state,msg)
```
The parameters `msgno`, `from`, `to`, `session_id` should be the
values expected according to the current protocol state.
------<=[ Cheater detection ]=>-----------
All participants MUST report to the user all errors that can identify
cheating peers in any given step. For each detected cheating peer the
participants MUST record and log the following information:
- the current protocol step,
- the violating peer,
- the other peer involved, and
- the type of violation
In order to detect other misbehaving peers in the current step,
processing for the rest of the SHOULD peers continue until the end of
the current step. Any further violations should be recorded as above.
Before the next message to the peers is sent, the STP must
check if there are no noted violations, if so the STP aborts and
reports all violators with their parameters to the user.
Abort conditions include any errors detected by recv_msg(), or when
the number of complaints is more than t for one peer, or more than t^2
in total.
Participants should log all broadcast interactions, so that any
post-mortem investigation can identify cheaters.
------<=[ Second generator point ]=>-----------
FT-Mult and the FT-Joint-DL_VSS DKG require a second generator on the
group. We generate it in the following manner:
h = voprf_hash_to_group(blake2b(hash,"nothing up my sleeve number"))
Where voprf_hash_to_groups is according to [RFC9497].
------<=[ The FT-MULT sub-protocol ]=>-----------
FT-MULT, calculates the product of two values Ξ±,Ξ² both shared in the
same (n,t) threshold sharing setup.
The FT-MULT sub-protocol is based on the "Fast-track multiplication"
as in fig. 6 of [GRR98], page 19. It goes as follows:
------<=[ FT-MULT pre-conditions ]=>-----------
1. All Peers use TLS or the STP knows long-term encryption keys for all
peers.
2. STP and peers MUST know long-term signing keys of everyone in the
protocol.
3. There is at least 2t+1 peers holding shares of both operands
participating in the protocol, 2t+1 of them will be acting as
dealers, the rest of the participants act as passive receivers.
4. Each dealer P_i has
- a share of Ξ±: Ξ±_i = π_Ξ±(i), one of the values to multiply
- a share of Ξ²: Ξ²_i = π_Ξ²(i), the other value to multiply
- a share of π: Ο_i = π(i), a blinding factor for the homomorphic commitment of Ξ±
- a share of π : Ο_i = π (i), blinding factor for the homomorphic commitment of Ξ²
public inputs, for i:=0..n
π_i = π(Ξ±_i,Ο_i) = g^(Ξ±_i)*h^(Ο_i)
π_i = π(Ξ²_i,Ο_i) = g^(Ξ²_i)*h^(Ο_i)
These conditions MUST be guaranteed by the initialization and DKG
phases of this protocol.
------<=[ FT-MULT VSPS check ]=>-----------
A VSPS check on a vector of homomorphic commitments verifies that
these correspond to a polynomial of degree t. The protocol relies on
this check heavily.
VSPS Check on π_i, i:=1..n,
t+1 t+1
Ξ π_i^Ξ_i = Ξ π_(i+t+1)^Ξ_i
i=0 i=0
t
where Ξ'_i = Ξ£ Ξ»_(j,i)*Ξ΄^j
j=0
where Ξ» is an inverted Vandermonde matrix over 0..t for the lhs, and
t+1..2t+1 for the rhs.
------<=[ Phase 1 - Initialization ]=>-----------
In this phase the protocol establishes a joint session id, and
Noise_XK protected channels between all peers.
Until the joint session id is established, the session id of shared in
the very first message of the STP is used.
This phase starts by the STP executing the `toprf_update_start_stp()`
function, which receives the parameters to the protocol, such as the
values N and T, the keyid of the record do update, the long-term
signatures keys of the peers. The result of this function is a initial
message to be sent to the peers, notifying them of the initiation of
the protocol, and all the essential parameters.
This initial message contains:
- the public long-term signature key of the STP,
- a hash of the DST,
- a keyid of the key to be updated,
- and an initial session_id.
The peers receive this message, and check if the public key of the STP
is authorized to operate on this key, they abort if the STP is
unauthorized.
------<=[ 1. Peer shares sid nonce ]=>-----------
The peers all receive the initial message from the STP.
This message contains a keyid, so that the client can check and load
the corresponding kc key share and corresponding long-term signature
keys of the peers, information stored in an internal state.
Finally the peers each start their own protocol loop, in which they
generate their own session id nonce and share the commitment to their
share of kc. This nonce and commitment is broadcast via the STP.
------<=[ 2. STP sets sessionid and forwards keys ]=>-----------
STP receives all messages from the peers, then
- verifies each of them,
- extracts the session id nonce and the commitment for their kc
share from each,
- hashes its own and the peers nonces in order
- sets the final session id to the result
- wraps all peer messages in a STP broadcast envelope message using
the new session id.
- adds the broadcast envelope message to the global transcript, and
- sends this message to all peers.
session_id = hash(stp_session_id |
peer1_sid_nonce |
peer2_sid_nonce |
... |
peerN_sid_nonce)
the stp_session_id is distributed to all peers in the first message of
the protocol.
------<=[ 3. peers set sessionid & noise keys ]=>-----------
After verifying, adding it to the transcript and unwrapping the
broadcast message envelope from the STP, the peers calculate the
session id like the STP did in the previous step. Furthermore they
verify that the sessionid they calculated is the same as the session
id used in the broadcast envelope header.
Finally the peers each initiate a Noise XK handshake message to all
peers.
------<=[ 4. STP routes handshakes between peers ]=>-----------
The TP receives all 1st handshake messages from all peers and routes
them correctly to their destination. These messages are not broadcast,
each of them is a P2P message. The benefit of the STP forming a star
topology here is, that the peers can be on very different physical
networks (wifi, lora, uart, nfc, usb, bluetooth, etc) and only the TP
needs to be able to connect to all of them.
------<=[ 5. each peer responds to each handshake ]=>-----------
Peer receives noise handshake1 from each peer and responds with
handshake2 answer to each peer.
------<=[ 6. STP routes handshakes between peers ]=>-----------
STP just like in step 4. routes all P2P messages from all peers to the
correct recipients of the messages.
------<=[ 7. Peers conclude handshakes, start p DKG ]=>-----------
This step concludes the Initialization phase and starts the DKG phase.
The peers receive the 3rd and final message of the Noise handshakes,
completing the setup of these Noise-protected peer-to-peer channels.
------<=[ Phase 2 - STP DKG of p ]=>-----------
The peers then start the DKG phase during which they collectively
generate the shared delta p factor. The underlying algorithm is
FT-Joint-DL-VSS from fig 7 in Appendix H of [GRR98].
1. Player P_i chooses a random value r_i and shares it using the DL-VSS
protocol, Denote by Ξ±_i,j ,Ο_i,j the share player P_i gives to player P_j. The
value π_i,j = g^(Ξ±_i,j)*h^(Ο_i,j) is public.
Since we might be forced to disclose the shares in case the peer gets
accused with cheating, but we don't want to reveal more information
than necessary we derive a dedicated key for the shares of the p value
to be shared/generated. The key for each of these shares is generated
in the following way:
We extract the shared key from the noise session. And run it through a
```
share_key = HKDF-expand("key for encryption of p share for []", noise_key)
```
Here the [] is replaced by the index of the peer who is the recipient
of this share.
Encryption of the shares is a simple XSalsa20 stream cipher with the
share_key, and for this step an all-zero nonce. Instead of the usual
poly1305 MAC, which is not key-committing (and thus would allow a peer
to cheat) we calculate an HMAC over the ciphertext with the share_key.
The shares Ξ±_i,j ,Ο_i,j for the p DKG are encrypted using the above
specified procedure.
The encrypted shares and their commitments π_i,j are stored by the
peer for later distribution.
The HMAC for each encrypted share is broadcast together with
the hash of the concatenation of all π_i,j commitments:
C_i = hash(π_i0 | π_i1 | .. | π_in)
```
encrypted_shares = []
hmacs = []
for i in 1..N
encrypted_shares[i] = encrypt_share(send_session[i], share[i])
hmacs[i] = hmac(send_session[i].key, encrypted_shares[i])
C_i = hash(commitments)
msg_6 = send_msg(6, peerid, 0xff, peer_sign_sk, session_id, {C_i, hmacs})
```
------<=[ 8. STP collects and broadcasts all C_i commitments ]=>-------
STP standard broadcast procedure expanded here, referred to later:
1. receives the messages from each peer
2. validates the message using recv_msg()
3. concatenates all received messages into a new message
4. signs the message of messages
5. adds this the message of messages and its signature to the transcript
6. sends it to all peers
In this case the STP keeps a copy for later of the broadcast
commitment hashes and of the MACs of the encrypted shares.
```
p_C_hashes = []
p_hmacs = []
msgs = []
for i in 1..N
msg_6 = recv(i)
data = recv_msg(6, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_6)
p_C_hashes[i], p_hmacs[i] = data
msgs = msgs | msg_6
msg_7 = send_msg(7, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_7)
broadcast(msg_7)
```
------<=[ 9. Peers receive commitment hashes & HMACs ]=>-------
The peers receive all C_i commitment hashes and share-HMACs and for
the p DKGs and broadcast their respective π commitment vectors:
```
msg_7 = recv()
msgs = recv_msg(7, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_7)
state = update_ts(state, msg_7)
p_C_hashes = []
p_share_macs = []
for i in 1..N
msg_6 = msgs[i]
data = = recv_msg(6, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_6)
p_C_hashes[i], p_share_macs[i] = data
msgs = msgs | msg_6
msg_8 = send_msg(8, peerid, 0xff, peer_sign_sk, session_id, p_A)
```
------<=[ 12. STP broadcasts all commitments ]=>-------
This is a regular STP broadcast step. Besides keeping a copy of all
commitments, the STP does also verify the commitment hashes and does
an FT-VSPS check on the commitments.
The STP verifies the VSPS property of the sum of the shared secrets by
running VSPS-Check on π_i,..,π_n where
π_j = Ξ π_i,j
i
If this check fails the STP runs VSPS-Checks on each individual
sharing. These checks are informational, and should guide the operator
in detecting and deterring cheaters.
```
p_commitments[][]
msgs = []
for i in 1..N
msg_8 = recv(i)
data = recv_msg(8, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_8)
p_commitments[i] = data
msgs = msgs | msg_6
if p_C_hashes != hash(p_commitments[i])
report(i)
C = []
for i in 1..n
C[i] = sum(p_commitments[j][i] for j in 1..n)
if vsps(C) fails:
for i..n
if vsps(p_commitments[i]) fails report(i)
msg_9 = send_msg(9, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_9)
broadcast(msg_9)
```
------<=[ 11. Peers receive commitments, send shares ]=>-------
The peers receive the broadcast commitments of all dealers for the
DKG, they check the commitment hashes and abort if they don't match,
otherwise they store the commitments for the next step.
Peers privately send the encrypted shares to each recipient.
```
msg_9 = recv()
msgs = recv_msg(9, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_9)
state = update_ts(state, msg_9)
p_commitments = [][]
for i in 1..N
msg_8 = msgs[i]
data = recv_msg(8, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_8)
p_commitments[i] = data
assert p_C_hashes[i] == hash(p_commitments[i])
msg_10s = []
for i in 1..N
msg = send_msg(9, peerid, i, peer_sign_sk, session_id, p_encrypted_shares[i])
send(msg)
```
------<=[ 12. STP routes shares to recipients ]=>-------
STP just routes all P2P messages from all peers to the correct
recipients of the messages.
```
for i in 1..N
msgs = recv(i)
for j in 1..N
send(j, msgs[j])
```
------<=[ 13. each peer receives shares & check commitments ]=>-------
The peers receive the private messages containing their shares. The
peers verify the shares against the previously broadcast commitment
vectors. For each
π_i,j == g^(Ξ±_i,j) * h^(Ο_i,j)
pair that fails, a complaint against the peer producing the
conflicting commitment and share is logged in an array, which is
broadcast to everyone.
```
s = []
for i in 1..N
msg = recv()
pkt = recv_msg(9, i, peerid, ref_ts, peer_sign_pks[i], session_id, msg)
final_noise_handshake, p_encrypted_share = pkt
recv_session[i] = noise_session_decrypt(recv_session[i], final_noise_handshake)
key=derive_key(recv_session[i], i)
p_Ξ±[i,peerid],p_Ο[i,peerid] = share_decrypt(p_encrypted_share)
p_complaints = []
for i in 1..N
if p_commitment[i,peerid] != g^(p_Ξ±[i,peerid])*h^(p_Ο[i,peerid])
p_complaints = p_complaints | i
data = len(p_complaints) | p_complaints
msg = send_msg(10, peerid, 0xff, peer_sign_sk, session_id, data)
send(msg)
```
------<=[ 14. STP collects complaints ]=>-------
Another receive-verify-collect-sign-transcribe-broadcast
instantiation. The STP keeps a copy of all complaints.
If any peer complaints about more than t peers, that complaining peer
is a cheater, and must be disqualified. Furthermore if there are in
total more than t^2 complaints there are multiple cheaters and the
protocol must be aborted and new peers must be chosen in case a rerun
is initiated.
```
p_complaints = []
msgs = []
for i in 1..N
msg_10 = recv(i)
data = recv_msg(10, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_10)
p_complaints_i = data
assert(len(p_complaints_i) < t)
p_complaints = p_complaints | p_complaints_i
msgs = msgs | msg_10
assert(len(complaints) < t^2)
msg_11 = send_msg(11, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_11)
broadcast(msg_11)
```
The next phase of the protocol depends on the number of complaints
received, if none then the next phase is finishing, otherwise the next
phase is complaint analysis.
If the next STP phase is complaint analysis (there are complaints) the
next input buffer size depends on the number of complaints against
each peer.
Each complaint is answered by the encrypted shares and the symmetric
encryption key used to encrypt these shares of the accused belonging to
the complainer. Each accused packs all answers into one message.
------<=[ 15. Each peer receives all complaints ]=>-------
All complaint messages broadcast are received by each peer. If peer_i
is being complained about by peer_j, peer_i broadcasts the
corresponding encrypted shares and the symmetric encryption key that
was used to encrypt them.
If there are no complaints at all the peers skip over to the final
phase step 20., otherwise they engage in the complaint analysis phase.
```
msg_11 = recv()
msgs = recv_msg(11, 0, 0xff, ref_ts, stp_sign_pk, session_id, msg_11)
state = update_ts(state, msg_11)
defenses = []
for i in 1..N
msg = msgs[i]
data = recv_msg(10, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg)
p_complaints_len, p_complaints = data
for k in 0..p_complaints_len
if p_complaints[k] == peerid
# complaint about current peer, publish key used to encrypt Ξ±_i,j , Ο_i,j
derive_key(send_session[i].key, "p", i)
defenses = defenses | {i, key, p_encrypted_shares[i]}
if len(keys) > 0
msg_12 = send_msg(12, peer, 0xff, peer_sign_sk, session_id, defenses)
send(msg_12)
```
------<=[ Phase 3 - STP DKG cheater handling ]=>-----------
If any complaints have been registered in the previous phase,
investigate and neutralize any possible cheaters.
------<=[ 16. STP collects all defenses, verifies&broadcasts them ]=>-------
STP checks if all complaints lodged earlier are answered by the
correct encrypted shares and their keys, by first checking if the
previously recorded MAC successfully verifies the encrypted share with
the disclosed key, and then decrypts the share with this key, and
checks if this satisfies the previously recorded commitment for this
share. If it does, the accuser is reported as a cheater, if the
commitment doesn't match the share, then the accused dealer is
disqualified from the protocol and its shares will not contribute to
the final shared secret.
```
msgs = []
for i in 1..N
if len(complaints[i]) < 1
continue
msg = recv(i)
p_defenses = recv_msg(12, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg)
msgs = msgs | msg
assert(len(p_defenses) == len(p_complaints[i]))
for j, key, encrypted_share in p_defenses
assert j==i
if p_hmacs[i][peerid] == hmac(key, encrypted_share)
report(i)
s,r=decrypt(key, encrypted_share]) or report(i)
if p_commitments[i][peerid] != g^s * h^r
report(i)
msg_13 = send_msg(13, 0, 0xff, stp_sign_sk, session_id, msgs)
state = update_ts(state, msg_13)
broadcast(msg_13)
```
------<=[ 17. Peers receive and check all defenses ]=>-------
Peers receive the encrypted shares, and their encryption keys, and
then run essentially the same step as the STP in the previous step,
then they directly skip to the final phase in the next step.
------<=[ 20. Peers VSPS check, calculate shares and finish ]=>-------
Players verify the VSPS property of the sum of the shared secrets by
running VSPS-Check on π_i,..,π_n for p where
π_j = Ξ π_i,j
i
If this check fails the players run VSPS-Check on each individual
sharing. Any player that fails this check is disqualified. The number
of all qualified peers (from this step, and the complaint analysis) is
checked that is greater than 1 and then number of disqualified peers
is less than t. If this fails the protocol aborts.
The shares dealt by the qualified peers are summed, creating the final
share. The commitment for this final share is calculated.
To finalize the 2nd phase before concluding the DKG of p, we compare
the transcript hashes of all peers. Thus each peer broadcasts their
own together with the final commitments to all parties.
```
p_C = []
for i in 1..n
p_C[i] = sum(p_commitments[j][i] for j in 1..n if peer[i] is qualified)
if vsps(p_C) fails:
for i in 1..n
if vsps(p_commitments[i]) fails disqualify(p,i)
p_s = 0, p_r = 0
for i in 1..n
if i is not disqualfied(p):
p_s += p_shares[i]_s
p_r += p_shares[i]_r
p_C = g^p_s * h^p_r
transcript = final_ts(state)
msg_20 = send_msg(20, peerid, 0, peer_sign_sk, session_id, {transcript, p_C})
send(msg_20)
```
------<=[ 21. STP receives and verifies all transcripts ]=>-------
STP receives all transcripts, and asserts that they all match its own
transcript, it reports if any transcript mismatch is detected. It also
does a final VSPS check on the commitments seen.
```
transcript = final_ts(state)
msgs = []
p_commitments = []
for i in 1..N
msg = recv(i)
data = recv_msg(20, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg)
ts, p_commitments[i] = data
if ts != transcript
report transcript mismatch
msgs = msgs | msg
if vsps(p_commitments) fails:
report failure
msg_14 = send_msg(14, 0, 0xff, stp_sign_sk, session_id, msgs)
broadcast(msg_14)
------<=[ 22. DKGs END, start FT_Mult ]=>-------
All peers receive the broadcasts transcripts and commitments, they run
the same check as the STP in the previous step and abort if any of
these fails. Otherwise the protocol continues with the FT-Mult phase
of calculating kc'=kc*p.
------<=[ Phase 4 - FT-Mult ]=>-----------
In this phase we calculate the product (kc') of the original key kc
with p.
All the peers pre-compute the inverted Vandermonde matrix based on the
indices of the dealers for the multiplications, and cache its first
row in their internal state.
Peers start their FT-MULT computation of kc*p. In the following
section we describe the steps for one FT-MULT calculation, we use the
following notation:
- a share of Ξ±: Ξ±_i = π_Ξ±(i), one of the values to multiply
- a share of Ξ²: Ξ²_i = π_Ξ²(i), the other value to multiply
- a share of π: Ο_i = π(i), a blinding factor for the homomorphic
commitment of Ξ±
- a share of π : Ο_i = π (i), blinding factor for the homomorphic
commitment of Ξ²
public inputs, for i:=1..n
- π_i = π(Ξ±_i,Ο_i) = g^(Ξ±_i)*h^(Ο_i) - the commitments to the
shares of the value to multiply
- π_i = π(Ξ²_i,Ο_i) = g^(Ξ²_i)*h^(Ο_i) - the commitments to the
shares of the other value to multiply
We denote as a dealer all peers, whose index is smaller or equal to
2(t-1)+1. (we use t to denote the threshold, but we need to decrease
it by one to get the degree of the polynomial)
Each dealer P_i shares Ξ»_iΞ±_iΞ²_i, using VSS:
c_ij = π_Ξ±Ξ²,i(j),
Ο_ij = u_i(j),
where π_Ξ±Ξ²,i and u_i, are random polynomials of degree t, such that
π_Ξ±Ξ²,i(0) = Ξ»_iΞ±_iΞ²_i
(c_ij is a t-sharing of Ξ»_iΞ±_iΞ²_i, and Ο_ij a t-sharing of a random value.)
Ξ»_i is the first row of the inverted Vandermonde matrix, as defined by
GRR98 section 3.1. and pre-computed and cached in this step.
secret information of P_i: share c_ji, Ο_ji of Ξ»_jΞ±_jΞ²_j
public information:
π_ij = g^(c_ij) * h^(Ο_ij) for i,j := 1..n
π_i0 = g^(c_i0) * h^(Ο_i0) for i := 1..n
Dealers broadcast their a hash of their π_i0, π_ij commitments for
each of their sharings for kc*p, and the HMACs for each of the shares
to all peers.
------<=[ 23. STP broadcasts commitment hashes and HMACs ]=>-----------
STP does the regular broadcasting procedure. The STP keeps a copy of
all commitment hashes and of all share HMACs for later.
------<=[ 24. Peers receive commitments hashes and HMACs]=>-----------
Peers receive and store the commitment hashes and the share HMACs in
an internal state for later.
Dealers broadcast their commitments.
------<=[ 25. STP broadcasts π_i0, π_ij for kc*p ]=>-----------
STP does the regular broadcasting procedure. STP verifies the
commitment hashes received in the previous step against commitments,
aborts if any of these fails. STP may also run VSPS checks on all of
the commitments, reports any failures, but otherwise doesn't react to
them and just continues.
------<=[ 26. Peers receive commitments, send shares ]=>-----------
Peers receive all commitments and verify them against the commitment
hashes. If this fails, they abort.
Dealers send the shares c_ij, Ο_ij for both kc*p to the corresponding
peers via noise protected channel.
------<=[ 27. STP standard p2p routing shares ]=>-----------
In this step the STP simply distributes the incoming Noise protected
shares to their proper recipients.
------<=[ 28. Peers receive shares ]=>-----------
Peers receive shares, verify them against the previously broadcast
commitments. Peers broadcast complaints against any dealers failing
their commitments.
------<=[ 29. STP broadcasts complaints ]=>-----------
STP does the regular broadcasting procedure. If there are no
complaints registered the STP will continue with the regular protocol,
otherwise the next step will be conflict resolution.
------<=[ 30. Peers receive complaints ]=>-----------
In this step the peers decide whether to continue with the regular
protocol or they engage in conflict resolution.
If there is complaints, the accused dealer broadcasts the contested
encrypted shares together with their encryption keys.
If there is no complaints, the regular protocol continues with the
verification of the ZK proofs on the correctness of π_i0.
------<=[ Phase 5 - Recover from commitment failures ]=>-----------
------<=[ 31. STP broadcasts dealer defenses ]=>-----------
In case there was complaints, the STP checks for each disclosure if
the previously stored share HMACs equal the hmac(key, encrypted
share), and if the commitment matches the decrypted share. The STP
keeps track of these results to anticipate which dealer will send how
much data in the next step.
------<=[ 32. Peers receive dealer defenses ]=>-----------
Each peer checks for each disclosure if the previously stored share
HMACs equal the hmac(key, encrypted share), and if the commitment
matches the decrypted share. If both of these checks succeed, then the
accused dealer is cleared and the accuser is reported, and the
complaint is cleared.
If there is no valid complaints, the regular protocol continues with
the next phase: verification of the ZK proofs on the correctness of π_i0.
For any valid remaining complaints, the peers reconstruct the accused
dealers secret by broadcasting their share to everyone.
------<=[ 33. STP broadcasts shares for reconstruction ]=>-----------
STP does the regular broadcasting procedure. Based on the received
shares STP also reconstructs any shares and corrects any invalid
commitments it has stored.
Reconstruction is done as following:
0. we set the candidate threshold to the originally configured
threshold.
1. all shares are checked against their commitments and any correct
ones are selected for reconstructing the shared secret. If there is
not enough correct shares to satisfy the threshold the
reconstruction attempt fails.
2. the reconstructed secret is verified against the π_i0 commitment,
if this succeeds the reconstruction attempt is successful and we
continue with the next step. If the attempt fails we increment the
threshold by one and abort if we have less shares than this new
threshold, otherwise we try again with step 2.
3. If step 2 succeeded in reconstructing the secret with a candidate
threshold, we reconstruct any contested shares and their
commitments.
------<=[ 34. Peers receive shares for reconstruction ]=>-----------
Peers reconstruct any shares that have been complained about - the
accuser replaces their previously possibly faulty share with the
reconstructed share - and calculate the correct commitment for this
share, if this is different than previously received, this is replaced
by the corrected one. See the previous STP step on how the
reconstruction process works.
This step has no output. Peers immediately continue with the next
step.
------<=[ Phase 6 - ZK proof of correctness of product ]=>-----------
------<=[ 35. Dealers prove in ZK correctness of π_i0 ]=>-----------
This step has no input, we arrive here either because there was no
complaints, all complaints were invalid, or the valid complaints
were neutralized by reconstruction of the contested shares.
We now start the ZK proofs, in which P_i proves in zk that π_i0 is a
commitment of the product Ξ»_iΞ±_iΞ²_i. As per Appendix F: ZK Proof for
multiplication of committed values from GRR98
Note the paper GRR98 describes a ZK proof for C = g^Ξ±Ξ² * h^Ο
however the multiplication algorithms Mult and FT-Mult require a proof for
C = g^Ξ±Ξ²Ξ» * h^Ο
Note the extra Ξ» in the exponent of g. to make this work a few changes
are necessary, these are:
z = (x+eΞ±Ξ»)
w_1 = (s_1 + Ξ»eΟ)
w_2 = (s_2 + e(Ο - ΟΞ±Ξ»))
and in the 2nd equation of the last step instead of A^e we need A^eΞ»:
g^z * h^w_1 == M_1 * A^e'_iΞ»
we have applied these changes in the following steps of the ZK proof
specification
We apply the optimization presented at the end of this Appendix F of
GRR98:
Each per chooses a challenge e_j,r_j β Z_q, broadcasts a commitment
π_ej = g^e_j * h^r_j
------<=[ 36. STP broadcasts π_ej commitments ]=>-----------
STP does the regular broadcasting procedure.
------<=[ 37. Peers receive π_ej commitments ]=>-----------
The peers store the π_ej commitments for later usage.
Dealer P_i chooses d, s, x, s_1, s_2 β Z_q.
Broadcasts the following messages:
M = g^d * h^s,
M_1 = g^x * h^s_1,
M_2 = B^x * h^s_2
------<=[ 38. STP broadcasts M,M_1,M_2 messages ]=>-----------
STP does the regular broadcasting procedure. STP keeps a copy of all
the ZK commitments received.
------<=[ 39. Peers receive M,M_1,M_2 messages ]=>-----------
Peers receive and store the M, M_1 and M_2 messages. Peers now
broadcast their previously chosen e_j,r_j values.
------<=[ 40. STP broadcasts e_j,r_j values ]=>-----------
STP does the regular broadcasting procedure. STP computes e'_i for all
dealers P_i:
e'_i = Ξ£ e_j
j!=i
and stores these for later.
------<=[ 41. Peers receive e_j,r_j values ]=>-----------
Peers receive e_j,r_j values, verify then against the previously
received, π_ej commitments:
π_ej == g^e_j * h^r_j
abort if this fails.
each peer computes e'_i for all dealers P_i:
e'_i = Ξ£ e_j
j!=i
Each dealer P_i broadcasts the following values:
y = d + e'_iΞ²,
w = s + e'_iΟ
z = x + e'_iΞ±Ξ»
w_1 = s_1 + e'_iΟΞ»
w_2 = s_2 + e'_i(Ο - ΟΞ±Ξ»)
------<=[ 42. STP broadcasts proofs ]=>-----------
STP does the regular broadcasting procedure. Verifies all ZK proofs
and uses this information to decide with step to continue with in the
protocol.
------<=[ 43. Peers receive proofs checks them. ]=>-----------
Each peer checks for each proof the following equations.
g^y * h^w == M * B^e'_i
g^z * h^w_1 == M_1 * A^e'_iΞ»
B^z * h^w_2 == M_2 * C^e'_i
If all the proofs are correct the protocol continues with completing
the FT-Mult results. This step has no output, the output is generated
by the next step which is immediately executed by the peers after this
one.
------<=[ Phase 7 - Recover from ZK proof failures ]=>-----------
In this phase peers disclose the shares from the dealer that failed to
prove the correctness of C_i0 so that this C_i0 can be corrected.
------<=[ 44. Peers receive proofs checks them. ]=>-----------
If any of the ZK proofs fail, the peers expose the shares of the
dealer P_i who failed the proof and engage in a reconstruction
phase. Peers broadcast the plaintext shares of the dealers who failed.
------<=[ 45. STP broadcasts shares for reconstruction ]=>-----------
STP does the regular broadcasting procedure. STP also reconstructs the
secret of the sharing and verifies that it satisfies π_i0, if that
fails, the STP aborts the protocol.
------<=[ 46. Peers receive shares for reconstruction ]=>-----------
Peers reconstruct the secret of any dealers failing their ZK proof,
the reconstructed secret is checked if it matches the corresponding
π_i0 commitment, if this fails the peers abort.
This step has no output, peers immediately continue with the next
phase/step.
------<=[ Phase 8 - Finish FT-Mult ]=>-----------
In this phase the FT-Mult protocol concludes, one last check and if
needed reconstruction phase provide robustness.
------<=[ 47. Peers calculate FT-MULT results ]=>-----------
Note this step has no input it follows directly if all ZK proofs were
correct or if the failing dealers secrets have been successfully
reconstructed.
Each peer P_i computes:
2t+1
Ξ³_i = Ξ£ c_ji
j=1
which is a share of Ξ³ = Ξ±Ξ², via random polynomial of degree t and
2t+1
Ο_i = Ξ£ Ο_ji
j=1
Each peer also computes
π_i = π(Ξ³_i, Ο_i)
= g^(Ξ³_i)*h^(Ο_i)
and also:
2t+1
π'_i = Ξ π_ji
j=1
Then compares π_i == π'_i, and aborts if this fails.
Otherwise the peers broadcast their final π_i commitment.
------<=[ 48. STP broadcasts π_i commitment ]=>-----------
STP does the regular broadcasting procedure. However STP keeps a copy
of each of the final commitments, for later verification of the final
reconstruction of p. STP also runs a fast-track VSPS check on all the
commitments, depending on the result of this deciding on the next step
of the protocol regular or reconstruction.
------<=[ 49. Peers receive π_i, run VSPS on them ]=>-----------
players run a VSPS Check on π_i for both kc*p, if it fails peers run a
VSPS Check on every dealers sharing, identifying the one that fails
the VSPS check.
If the fast-track VSPS checks succeed on the final commitments the
protocol continues with the regular final phase, otherwise the
protocol for a last time engages in a reconstruction of secrets of
dealers who failed the VSPS check of their commitments.
This step has no output message, either of the two possible follow-up
steps will generate an output message.
------<=[ Phase 9 - Recover from failing VSPS-checks ]=>-----------
In this phase any failing VSPS-checks are countered by reconstruction
of the secret of the dealer who failed the check, the secret is then
re-shared using a "deterministic" sharing.
------<=[ 51. Peers disclose shares of failed dealers ]=>-----------
In case the VSPS checks failed in the previous step, all peers
disclose the shares of the dealer(s) who has failed the VSPS check.
------<=[ 52. STP broadcasts shares and re-shares ]=>-----------
The STP broadcasts all the disclosed shares.
The STP also reconstructs the secrets associated with these shares,
and then creates a "deterministic" re-sharing of this secret and
updates the commitments it holds to these new shares and re-calculates
the final commitments based on these.
The "deterministic" re-sharing of the reconstructed shares is based on
a polynomial with the constant term being the secret and the other
coefficients being derived by using HKDF-expand(info, prk), where the
prk is the current sessionid and the info parameter is either
"k0p lambda * a * b re-sharing"
or
"k0p blind re-sharing"
for the secret and the blinding factor respectively.
This deterministic re-sharing is used so that all parties in the
protocol can create the same re-sharing without any party having
control over this and without needing to distribute extra information
to all parties.
------<=[ 53. Peers receive shares and re-share ]=>-----------
Peers receive disclosed shares of all dealers who failed the VSPS
check, and re-share the reconstructed secrets in the same way as the
STP does using the same "deterministic" re-sharing procedure.
This step has no output message, it immediately followed by the next
step sending the results of both multiplications to the STP.
------<=[ Phase 10 - Finalize TOPRF Update ]=>-----------
This phase concludes the update of the TOPRF secret.
------<=[ 54. Peers send their final shares to STP ]=>-----------
The peers send their shares of p to the STP.
------<=[ 55. STP receives results and calculates delta ]=>-----------
The STP receives all shares from peers, runs a VSPS check on their
commitments, aborts if these fail. It also verifies the commitments of
these shares, again, aborts if these fail. Otherwise reconstructs from
them the value:
β = Ο
Finally the STP sends out a message of one byte of value 0 to all
peers indicating the success of the protocol.
The protocol finishes for the STP.
------<=[ 44. Peers complete the protocol ]=>-----------
If the received message from STP is 0, they replace the share (and
commitment) for the old kc with the new share (and commitment) for
kc'=(ΟΒ·kc).
Otherwise they keep the old kc.
The protocol finishes here for the peers.
------<=[ TODO verify transcript ]=>-----------
All parties broadcast their transcript, and verify that they all
match. If there is non-matching, abort. Maybe make the transcript a
rolling check, by for example using it as a continuously updated
sessionid.
------<=[ References ]=>-----------
[GRR98] R. Gennaro, M. O. Rabin, and T. Rabin. "Simplified VSS and
fact-track multiparty computations with applications to threshold
cryptography" In B. A. Coan and Y. Afek, editors, 17th ACM PODC, pages
101β111. ACM, June / July 1998
[JKR19] "Updatable Oblivious Key Management for Storage Systems", by
Stanislaw Jarecki, Hugo Krawczyk, and Jason Resch.
[RFC9497] Oblivious Pseudorandom Functions (OPRFs) Using Prime-Order
Groups https://www.rfc-editor.org/rfc/rfc9497.html
liboprf-0.9.4/docs/tp-dkg.txt 0000664 0000000 0000000 00000070231 15146734002 0016045 0 ustar 00root root 0000000 0000000 Trusted-party (TP) Distributed Key Generation (DKG) v1.0
This document specifies a proposal for a non-robust DKG that can work
for small deployments with a small number of parties and infrequent
DKG executions. Non-robust means that the protocol succeeds only if no
party aborts. If someone aborts then the protocol needs to run again,
possibly after kicking out misbehaving parties. This protocol does
support maximum 127 peers. This is probably already too much for a
non-robust protocol, but it might work in very special circumstances.
Broadcast is implemented by the trusted party (TP) opening a channel
to each peer secured by the peers long-term encryption key. Every
message is routed through the TP.
Peer long-term encryption keys can be either TLS-based, or
Noise_XK-based (https://noiseexplorer.com/patterns/XK/). In the latter
case the long-term public keys must be known and validated in advance
by the TP.
The basis for this protocol is JF-DKG (fig 1.) a variant on Pedersens
DKG from the 2006 paper "Secure Distributed Key Generation for
Discrete-Log Based Cryptosystems" by R. Gennaro, S. Jarecki,
H. Krawczyk, and T. Rabin [GJKR06]. The algorithm JF-DKG is presented
in the paper as a reduced example how an adversary can influence the
bits in the generated secret by manipulating the complaints and thus
the final composition of the QUAL set, gaining a 3/4 chance to
influence a bit. Since in our TP variant is non-robust, we do not
allow individual disqualifications of peers, - either all peers
qualify or the protocol fails - this mitigates the case where an
adversary can adaptively disqualify a peer. Thus the JF-DKG is a
simple and sufficient algorithm for our purposes.
------<=[ Rationale ]=>-----------
Traditionally DKGs are used in setting where all parties are equal and
are using the distributed key together, without having any one party
having a different role in the protocol utilizing the shared key. This
does not translate entirely to threshold OPRFs (tOPRF) and protocols
based on these.
In an OPRF there is normally two parties, one holding the key, and
another one holding the input and learning the output. In a tOPRF the
party holding the key is a group of peers that hold shares of the key
in a threshold setting.
The whole point of OPRFs is to be able to learn the output for a
certain input, without being able to do so without the contribution of
the party/parties holding (parts of) the key. Hence the party with the
input is in a kind of trusted role, and in many protocols based on
OPRFs it is in the best interest of the input-holding party to not
learn the key (or its parts) - otherwise the input-holding party could
just deploy a PRF instead.
And if the input holding party is in such a trusted role, there is two
options to generate a threshold shared key:
1. the trusted input-holding party just generates a secret and shares
it with the key-holding parties using Shamir's Secret Sharing.
This is a very simple approach, with one drawback, the secret
itself is however briefly know at the input-holding TP.
2. The input-holding TP can run the simple non-robust DKG specified
below. This has the benefit that as long as the protocol is
followed precisely the secret is never "assembled" and thus cannot
leak, and is never exposed to the TP. Drawback of this is, that
the protocol below consists of many rounds of communication.
The protocol in this document allows for a variant, were each
keyshare-holder generates a completely new set of ephemeral
(encryption and signature) keys, and thus allows complete anonymity
between the keyshare-holders from each other. While only the TP is
aware of the identities of each of the keyshare-holders (by knowing
their long-term signature and encryption keys). This increases the
security of the whole scheme, as an attacker compromising one
keyshare-holder will not be able to learn the identity of the other
parties - and more importantly the location of the other keyshares. If
this keyshare-holder anonymity is not necessary, steps 3, 4 and the
first half of step 5 in the following protocol can be skipped.
------<=[ Prototocol Phases ]=>-----------
The protocol has the following phases:
1. Initialization and introduction (step 1 - 5)
2. Setup secure P2P channels (step 5 - 10)
3. core DKG (step 11 - 17)
4. Finish with failure: complaint resolution (only if there are
complaints) (step 17 - 19)
5. Finish with success: verification of transcript and completion of
protocol (step 20 - 22)
------<=[ Simplified API ]=>-----------
Since the protocol consists of many steps, it is recommended to
abstract the API to the following schema:
0. Initialize
While not done and not error:
1. Allocate input buffers
2. input = receive()
3. allocate output buffer
4. run next step of protocol
5. if there is output: send(output)
6. Post-processing
This simple schema simplifies the load of an implementer using this
protocol, reducing opportunities for errors and provides strict
security.
The reference implementation in tp-dkg.c follows this schema for both
the TP and the peers.
------<=[ Protocol transcript ]=>-----------
Transcript - all broadcast messages are accumulated into a transcript
by each peer and the trusted party, at the end of the protocol all
parties publish their signed transcripts and only if all signatures
are correct and the transcripts match, is the protocol successful.
The transcript is a hash, that is initialized with the string:
"tp dkg session transcript"
in pseudo-code:
transcript_state = hash_init("tp dkg session transcript")
Updating the transcript first updates the hash with the canonical
32bit size of the message to be added to the transcript, then the
message itself is added to the hash.
transcript_state = hash_update(transcript_state, I2OSP(len(msg))
transcript_state = hash_update(transcript_state, msg)
The signature of each message is similarly added to the transcript.
A function `update_ts` can be used as a high-level interface to
updating the transcript with messages and their signatures:
```
update_ts(state,msg,sig)
state = hash_update(state, I2OSP(len(msg))
state = hash_update(state, msg)
state = hash_update(state, I2OSP(len(sig))
state = hash_update(state, sig)
return state
```
------<=[ Session id ]=>-----------
Every execution of the protocol starts by the TP sending out a message
with a unique and fresh session id, this is to ensure that no messages
can be replayed. The session id is a 256 bit (32B) random value of
cryptographic quality.
------<=[ Message header ]=>-----------
All messages have a message header:
uint8 type
uint8 version = 0
uint8 messageno
uint32 len
uint8 from
uint8 to
uint64 timestamp
uint8 sessionid[32]
The first field is the protocol type identifier. TP-DKG has an
identifier value of zero (0).
The second field in the header is really a state identifier. A
recipient MUST verify that the messageno is matching with the expected
number related to the state of the protocol.
The len field MUST be equal to the size of the packet received on the
network including the packet header.
The `from` field is simply the index of the peer, since peers are
indexed starting from 1, the value 0 is used for the trusted
party. Any value greater than 128 is invalid. The state defines from
whom to receive messages, and thus the from field MUST be validated
against these expectations.
The `to` field is similar to the `from` field, with the difference
that the value 0xff is reserved for broadcast messages. The peer (or
TP) MUST validate that it is indeed the recipient of a given message.
The timestamp field is just a 64bit timestamp as seconds elapsed since
1970/01/01, for peers that have no accurate clock themselves but do
have an RTC, the first initiating message from the TP SHOULD be used
as a reference for synchronizing during the protocol.
------<=[ Message signatures ]=>-----------
Every message MUST be signed using the sender peers ephemeral signing
key. The signature is made over the message and the appended session
id. The session id is announced by the TP in the first message.
------<=[ Verifying messages ]=>-----------
Whenever a message is received by any participant, they first MUST
check the correctness of the signature:
```
msg, sig = recv()
sign_pk = sign_keys[expected_sender_id]
assert(verify(sign_pk, msg, sig))
```
The recipient MUST also assert the correctness of all the other header
fields:
```
assert(msg.type == 0)
assert(msg.version == 0)
assert(msg.messageno == expected_messageno)
assert(msg.from == expected_sender_id)
assert(msg.to == (own_peer_id or 0xff))
assert(ref_ts <= msg.ts < ref_ts + timeout))
ref_ts = msg.ts
```
The value `timeout` should be configurable and be set to the smallest
value that doesn't cause protocol aborts due to slow responses.
If at any step of the protocol the TP receives one or more messages
that fail these checks, the TP MUST abort the protocol and report all
violating peers to the user.
------<=[ Message transmission ]=>-----------
A higher level message transmission interface can be provided, for
sending:
```
msg, sig = send_msg(msgno, from, to, sign_sk, session_id, data)
ts = timestamp()
msg = messageno: msgno, len: len(header) + len(data) + len(sig), from: from, to: to, ts: ts, data
sig = sign(sign_sk, msg)
return msg, sig
```
And for validating incoming messages:
```
data = recv_msg(msgno, from, to, ref_ts, sign_pk, session_id, msg, sig)
assert(verify(sign_pk, msg, sig)
assert(msg.type == 0)
assert(msg.version == 0)
assert(msg.messageno == msgno)
assert(msg.len == len(msg|sig))
assert(msg.from == from)
assert(msg.to == to)
assert(ref_ts < msg.ts < ref_ts + timeout))
if msg.to == 0xff:
update_ts(state,msg,sig)
```
The parameters `msgno`, `from`, `to`, `session_id` should be the
values expected according to the current protocol state.
------<=[ Cheater detection ]=>-----------
The TP MUST report to the user all errors that can identify cheating
peers in a given step. For each detected cheating peer the TP MUST
record the following information:
- the current protocol step,
- the violating peer,
- the other peer involved, and
- the type of violation
In order to detect other misbehaving peers in the current step,
processing for the rest of the SHOULD peers continue until the end of
the current step. Any further violations should be recorded as above.
Before the next message to the peers is sent, the TP must
check if there are no noted violations, if so the TP aborts and
reports all violators with their parameters to the user.
Abort conditions include any errors detected by recv_msg(), or when
the number of complaints is more than t for one peer, or more than t^2
in total, as well any of the checks of the JF-DKG algorithm from
GJKR06.
------<=[ The protocol ]=>-----------
------<=[ 0. Precondition ]=>-----------
Peers use TLS or TP knows long-term encryption keys for all peers.
Client knows long-term signing keys of all peers.
------<=[ 1. DKG Announcement - TP(peers, t, proto_name) ]=>----------
The protocol starts by asking the trusted party (TP) to initiate a new
run of the DKG protocol by providing it with:
- a list of the peers,
- a threshold value, and
- protocol instance name used as a domain separation token.
The TP then sanity checks these parameters:
```
n = len(peers)
assert(2<=t0)
```
The TP then generates a fresh session id, and a hash of the DST.
The TP then creates a broadcast message containing the session id, a
hash (so that the message is always of fixed size) of the DST,
the values N and T and its own public signing key:
```
dst_str = "TP DKG for protocol %s" % proto_name
dst = hash(I2OSP(len(dst_str)) | dst_str)
sessionid = random_bytes(32)
data = {dst, n, t, tp_sign_pk}
msg_0, sig_0 = send_msg(0, 0, 0xff, tp_sign_sk, session_id, data)
broadcast(msg_0 | sig_0)
```
The TPs copy of the transcript is initialized by the TP, and updated
with the value of the 1st broadcast message:
```
state = hash_init("tp dkg session transcript")
state = update_ts(state, msg, sig)
```
Since the order of the peers is random, and important for the protocol
a custom message is created for each peer by the TP and sent
individually notifying each peer of their index in this protocol
run. This is essentially an empty message consisting only of a
header. The msg.to field conveys the index of the peer.
```
# sending each peer its index
for i in 1..n:
msg_1, sig_1 = send_msg(1, 0, i, tp_sign_sk, session_id, {})
send(i, msg_1 | sig_1)
```
------<=[ 2. each peer(msg_0, sig_0) ]=>------------
In this step each peer receives the initial parameter broadcast,
verifies it, initializes the transcript and adds the initial
message. Then receives the message assigning its index.
```
msg_0, sig_0 = recv()
assert(recv_msg(0, 0, 0xff, ref_ts, msg.data.tp_sign_pk, session_id, msg_0, sig_0))
```
If the peer has no accurate internal clock but has at least an RTC, it
SHOULD set the ref_ts to the message timestamp:
```
ref_ts = msg_0.ts
```
Furthermore the peer MUST also verify that the N&T parameters are
sane, and if possible the peer SHOULD also check if the session id is
fresh (if it is not possible, isfresh() MAY always return true.
```
assert(2 <= msg_0.t < n)
assert(isfresh(msg_0,sessionid))
```
The transcript MUST be initialized by the peer, and updated with the
value of the 1st broadcast message:
```
state = hash_init("tp dkg session transcript")
state = update_ts(state, msg, sig)
```
After processing the broadcast message from the TP, the peers also
have to process the second message from the TP in which they are
assigned their index.
```
sig1, msg1 = recv()
assert(recv_msg(1, 0, msg1.to, ref_ts, tp_sign_pk, session_id, msg_1, sig_1))
assert(msg1.to <= 128 and msg1.to > 0)
peerid = msg.to
```
------<=[ 3. peers broadcast their keys via TP ]=>-------------
If this protocol requires anonymity from each peer all peers broadcast
fresh signing and noise keys to all peers via the TP. If no
peer-anonymity is required it is ok to either send long-term keys keys
here, or skip to the 2nd half or step 5 below.
In order to assure the TP that the peer is authentic, this message is
additionally signed by the peers long-term signing key - which must be
known in advance by the TP. This ensures that the fresh ephemeral keys
belong to the peer and not some adversary.
```
peer_sign_sk, peer_sign_pk = sign_genkey()
peer_noise_sk, peer_noise_pk = noise_genkey()
msg_2, sig_2 = send_msg(2, peerid, 0xff, peer_sign_sk, session_id, {peer_sign_pk, peer_noise_pk})
ltsig = sign(peer_long_term_sig_sk, msg_2|sig_2)
broadcast(ltsig | msg_2 | sig_2 )
```
------<=[ 4. TP collects and broadcasts all peer keys ]=>-------------
The TP first checks if each of the received messages is signed by the
expected long-term signing key, if this fails the TP aborts. If all
long-term signatures are correct the TP MUST strip those signatures
from all the messages. This is to ensure their anonymity from each
other.
Then the TP acts as a broadcast medium on the long-term
signature-stripped messages.
This is a recurring pattern where the TP acts in its broadcasting
intermediary role:
1. receives the messages from each peer
2. validates the message using recv_msg()
3. extracts all signing pubkeys (or other information depending on
the current step) for usage by the TP in the rest of the protocol
4. concatenates all received messages into a new message
5. signs the message of messages
6. adds this the message of messages and its signature to the transcript
7. sends it to all peers
```
peer_sig_pks = []
msgs = []
for i in 1..N
ltsig, msg_2, sig_2 = recv()
assert(verify(lt_sign_pk[i], msg_2 | sig_2, ltsig))
sig_pk, noise_pk = recv_msg(2, i, 0xff, ref_ts, msg_2.data.peer_sign_pk, session_id, msg_2, sig_2)
peer_sig_pks[i] = sig_pk
msgs = msgs | { msg_2 , sig_2 }
msg_3, sig_3 = send_msg(3, 0, 0xff, tp_sign_sk, session_id, msgs)
state = update_ts(state, msg_3, sig_3)
broadcast(msg_3|sig_3)
```
------<=[ 5. each peer get all keys and initiate noise channels with all peers ]=>-------
In this phase all peers process the broadcast signing and noise keys
received from all peers, and initiate a noise_xk handshake with each
of them (including themselves for simplicity and thus security).
Note: For performance it MAY be, that each peer only initiates
handshakes with peers having a higher index than themselves. But this
would create a packet-size and timing side-channel revealing the index
of the peer.
```
msg_3, sig_3 = recv()
msgs = recv_msg(3, 0, 0xff, ref_ts, tp_sign_pk, session_id, msg_3, sig_3)
state = update_ts(state, msg_3, sig_3)
peers_sign_pks = []
peers_noise_pks = []
send_session = []
for i in 1..N
msg, sig = msgs[i]
peers_sign_pks[i], peers_noise_pks[i] = recv_msg(2, i, 0xff, ref_ts, msg.peer_sign_pk, session_id, msg, sig)
send_session[i], handshake1 = noisexk_initiator_session(peer_noise_sk, peers_noise_pks[i])
msg, sig = send_msg(4,peerid,i,peer_sign_sk, session_id, handshake1)
send(msg | sig)
```
------<=[ 6. TP routes handshakes from each peer to each peer ]=>-------
The TP receives all 1st handshake messages from all peers and routes
them correctly to their destination. These messages are not broadcast,
each of them is a P2P message. The benefit of the TP forming a star
topology here is, that the peers can be on very different physical
networks (wifi, lora, uart, nfc, bluetooth, etc) and only the TP needs
to be able to connect to all of them.
```
for i in 1..N
handshakes = recv(i)
for j in 1..N
send(j, handshakes[j])
```
------<=[ 7. each peer responds to each handshake each peer ]=>-------
Peer receives noise handshake1 from each peer and responds with
handshake2 answer to each peer.
```
for i in 1..N
msg, sig = recv()
handshake1 = recv_msg(4, i, peerid, ref_ts, peers_sign_pks[i], session_id, msg, sig)
receive_session[i], handshake2 = noisexk_responder_session(peer_noise_sk, handshake1)
msg, sig = send_msg(5, peerid, i, peer_sign_sk, session_id, handshake2)
send(msg | sig)
```
------<=[ 8. TP routes handshakes from each peer to each peer ]=>-------
TP just routes all P2P messages from all peers to the correct
recipients of the messages.
```
for i in 1..N
handshakes = recv(i)
for j in 1..N
send(j, handshakes[j])
```
------<=[ 9. each peer completes each handshake with each peer ]=>-------
Peers complete the noise handshake.
```
for i in 1..N
msg, sig = recv()
handshake3 = recv_msg(5, i, peerid, ref_ts, peers_sign_pks[i], session_id, msg, sig)
send_session[i] = noisexk_initiator_session_complete(send_session[i], handshake3)
```
------<=[ 10. Setup complete ]=>-------
Each peer has a confidential connection with every peer (including self, for simplicity)
The one time this channel is used, when distributing the shares from
step 13. The sender uses the initiator interface of the noise session,
and the receiver uses the responder interface.
------<=[ 11. each peer executes DKG Round 1 ]=>-------
This step is as described by GJKR06 (fig 1. JF-DKG) step 1: Each party
P_i (as a dealer) chooses a random polynomial f_i(z) over Z_q of degree t:
f_i(z) = a_(i0) + a_(i1)z + Β·Β·Β· + a_(it)z^t
P_i broadcasts A_ik = g^(a_ik) mod p for k = 0,... ,t.
Each P_i computes the shares s_ij = f_i(j) mod q for j = 1, ... ,n.
```
a = []
A = []
for i in 0..t
a[i]=randombytes(32)
A[i]=g*a[i]
s = []
for i in 1..N
for j in 0..t
s[i]+=a[j]*i^j
msg_6, sig_6 = send_msg(6, peerid, 0xff, peer_sign_sk, session_id, A)
send(msg_6 | sig_6)
```
------<=[ 12. TP collects and broadcasts all A vectors ]=>-------
This is another broadcast pattern instance:
receive-verify-collect-sign-transcript-broadcast. The TP keeps a copy
of all commitments being broadcast.
```
A = [][]
msgs = []
for i in 1..N
msg_6, sig_6 = recv(i)
A[i] = recv_msg(6, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_6, sig_6)
msgs = msgs | { msg_6 , sig_6 }
msg_7, sig_7 = send_msg(7, 0, 0xff, tp_sign_sk, session_id, msgs)
state = update_ts(state, msg_7, sig_7)
broadcast(msg_7|sig_7)
```
------<=[ 13. each peer collects all A vectors and distributes their generated shares ]=>-------
All peers receive the bundled A commitment messages which have been
sent by all peers and re-broadcast by the TP. First the bundle is
verified, then each message containing the j-th A commitment vector is
also verified. A copy of all A commitment vectors is retained for
later usage. Then the share for the j-th peer is sent using the
previously established noise channel to the j-th peer. These shares
have been already computed in step 11, as per the step 1 of the JF-DKG
algorithm from the GJKR06 paper.
```
msg_7, sig_7 = recv()
msgs = recv_msg(7, 0, 0xff, ref_ts, tp_sign_pk, session_id, msg_7, sig_7)
state = update_ts(state, msg_7, sig_7)
A=[][]
for i in 1..N
msg, sig = msgs[i]
A[i] = recv_msg(6, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg, sig)
pkt = noise_send(send_session[i], s[i])
msg, sig = send_msg(8,peerid,i,peer_sign_sk, session_id, pkt)
send(msg | sig)
```
------<=[ 14. TP routes noise protected messages between peers ]=>-------
Since all these messages are confidential P2P messages protected by
noise, all the TP is doing in this step is routing each packet to its
correct destination. For the resolution of complaints and cheater
identification, TP keeps a copy of all messages.
```
encrypted_shares = [][]
for i in 1..N
for j in 1..N
msg = recv(i)
send(j, msg)
encrypted_shares[i][j] = msg
```
------<=[ 15. each peer executes DKG Round 2 ]=>-------
Each peer having received all their shares from all the peers,
verifies the messages, and then verifies the shares against the
previously broadcast A commitment vectors. For each s_ij, A_i pair
that fails, a complaint against the peer producing the conflicting
commitment and share is logged in an array, which is broadcast to
everyone. This is essentially step 2 from the JF-DKG algorithm
described in GJKR06.
```
s=[]
for i in 1..N
msg, sig = recv()
pkt = recv_msg(8, i, peerid, ref_ts, peer_sign_pks[i], session_id, msg, sig)
s[i] = noise_recv(receive_session[i], pkt)
complaints = []
for i in 1..N
v = 0
for k in 0..t
v += A[i][k]*peerid*k
if (g*s[i] != v)
complaints = complaints | i
msg, sig = send_msg(9, peerid, 0xff, peer_sign_sk, session_id, len(complaints) | complaints)
send(msg | sig)
```
------<=[ 16. TP collects complaints ]=>-------
Another receive-verify-collect-sign-transcribe-broadcast
instantiation. The TP keeps a copy of all complaints for the 18th
step.
If any peer complaints about more than t peers, that complaining peer
is a cheater, and must be disqualified. Furthermore if there are in
total more than t^2 complaints there are multiple cheaters and the
protocol must be aborted and new peers must be chosen in case a rerun
is initiated.
```
complaints = []
msgs = []
for i in 1..N
msg_9, sig_9 = recv(i)
complaints_i = recv_msg(9, i, 0xff, ref_ts, peer_sign_pks[i], session_id, msg_9, sig_9)
assert(len(complaints_i) < t)
complaints = complaints | complaints_i
msgs = msgs | { msg_9 , sig_9 }
assert(len(complaints) < t^2)
msg_10, sig_10 = send_msg(10, 0, 0xff, tp_sign_sk, session_id, msgs)
state = update_ts(state, msg_10, sig_10)
broadcast(msg_10|sig_10)
```
The next step of the protocol depends on the number of complaints
received, if none then the next step is 21. otherwise 18.
If the next TP step is 18 (there are complaints) the next input buffer
size depends on the number of complaints against each peer.
Each complaint is answered by the symmetric encryption key used to
encrypt the share of the accused belonging to the complainer. Each
accused packs all answers into one message.
------<=[ 17. Each peer receives all complaints ]=>-------
All complaint messages broadcast are received by each peer. If peer_i
is being complained about by peer_j, peer_i sends the symmetric
encryption key that was used to encrypt s_ij to the TP. This is the
first part of step 3. in JF-DKG of GJKR06. There is a slight
variation, instead of broadcasting the share, the accused peer reveals
the symmetric encryption key that was used to encrypt the share. The
TP has a copy of this encrypted message, and with the symmetric
encryption key, it can decrypt the originally sent share. This is some
kind of poor mans provable encryption.
If any complaints have been lodged by any peer the protocol ends here
for all the peers.
```
msg_10, sig_10 = recv()
msgs = recv_msg(10, 0, 0xff, ref_ts, tp_sign_pk, session_id, msg_10, sig_10)
state = update_ts(state, msg_10, sig_10)
keys = []
for i in 1..N
msg, sig = msgs[i]
complaints_len, complaints = recv_msg(9, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg, sig)
for k in 0..complaints_len
if complaints[k] == peerid
# complaint about current peer, publish key used to encrypt s_ij
keys = keys | send_session[i].key
if len(keys) > 0
msg_11, sig_11 = send_msg(11, peer, 0, peer_sign_sk, session_id, keys)
send(msg_11, sig_11)
```
------<=[ 18. TP collects all s_ij, broadcasts and verifies them ]=>-------
In this step TP checks equation 3 from step 2 in JF-DKG of GJKR06.
TP also checks if all complaints lodged earlier are answered by the
correct s_ij shares. The shares to be verified are decrypted from the
previously encrypted messages, using the revealed encryption keys by
the accused peers.
The protocol ends here, as either the complainer or the accused tried
to cheat.
```
for i in 1..N
if len(complaints[i]) < 1
continue
msg, sig = recv(i)
keys = recv_msg(11, i, 0, ref_ts, peers_sign_pks[i], session_id, msg, sig)
assert(len(keys) == len(complaints[i]))
sij=[][]
for j, key in keys
sij[i][j]=decrypt(key, encrypted_shares[i][j])
for complaint in complaints[i]
v = 0
for k in 0..t
v += A[i][k]*peerid*k
if(g*sij[complaint.from][complaint.data] != v)
suspicious = suspicious | identity(i)
else
suspicious = suspicious | identity(j)
```
------<=[ 19. Compare all transcripts ]=>-------
Each peer calculates the final transcripts and sends it to TP.
```
transcript = final_ts(state)
msg_20, sig_20 = send_msg(20, peerid, 0, peer_sign_sk, session_id, transcript)
send(msg_20, sig_20)
```
------<=[ 20. TP receives all and verifies transcripts ]=>-------
TP receives all transcripts, and asserts that they all match its own
transcript, it aborts if any transcript mismatch is detected. If
everything matches it broadcasts the result either as OK.
```
transcript = final_ts(state)
for i in 1..N
msg, sig = recv(i)
ts = recv_msg(20, i, 0xff, ref_ts, peers_sign_pks[i], session_id, msg, sig)
assert( ts == transcript)
msg_21, sig_21 = send_msg(21, 0, 0xff, tp_sign_sk, session_id, { "OK" })
------<=[ 21. SUCCESS, peers set their share and confirm ]=>-------
All peers receive the OK acknowledgment from the TP and calculate the
final share, this is equivalent with the calculation of x_j in the
4. step in JF-DKG of GJKR06. Finally all peers acknowledge this step
with another "OK" message sent to the TP. This is the final step for
the peers, each needs to persist the calculated x_j share for usage in
later threshold protocol runs (such as tOPRF).
```
msg_21, sig_21 = recv()
recv_msg(21, 0, 0xff, ref_ts, tp_sign_pk, session_id, msg_21, sig_21)
share = 0
for i in 1..N
share += s[i]
msg_22, sig_22 = send_msg(22, peerid, 0, peers_sign_sk, session_id, "OK")
persist(own_peer_id, share)
```
------<=[ 22. TP asserts all peers respond with "OK" ]=>-------
The TP collects all "OK" messages from all peers.
```
for i in 1..N
msg, sig = recv(i)
ok = recv_msg(22, i, 0, ref_ts, peers_sign_pks[i], session_id, msg, sig)
assert( ok == "OK")
```
This successfully concludes the protocol.
liboprf-0.9.4/liboprf.pc 0000664 0000000 0000000 00000000451 15146734002 0015144 0 ustar 00root root 0000000 0000000 includedir=${prefix}/include
Name: liboprf
Description: implementation of OPRF (RFC9497) including (updatable) threshold variant
Version: 0.7.1
Cflags: -I${includedir}/oprf -I${includedir}/oprf/noiseXK/ -I${includedir}/oprf/noiseXK/karmel -I${includedir}/oprf/noiseXK/karmel/minimal
Libs: -loprf
liboprf-0.9.4/misc/ 0000775 0000000 0000000 00000000000 15146734002 0014116 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/misc/attack.c 0000664 0000000 0000000 00000007472 15146734002 0015543 0 ustar 00root root 0000000 0000000 // # SPDX-FileCopyrightText: 2024, Marsiske Stefan
// # SPDX-License-Identifier: GPL-3.0-or-later
// build with
// $ gcc -Wall -O3 attack.c -o attack -loprf -lsodium
// then run:
// $ ./attack test
#include // memcmp
#include // f?printf
#include // uint8_t
#include // va_list, va_start, va_end
#include
#include
static const uint8_t k[crypto_core_ristretto255_SCALARBYTES] = {1};
void dump(const uint8_t *p, const size_t len, const char* msg, ...) {
va_list args;
va_start(args, msg);
vfprintf(stderr,msg, args);
va_end(args);
fprintf(stderr,"\t");
for(size_t i=0;ibeta\n", exec);
printf("usage: cat rwd | %s guess password\n", exec);
return ret;
}
static int tamper(const uint8_t alpha[crypto_core_ristretto255_BYTES],
uint8_t beta[crypto_core_ristretto255_BYTES]) {
puts("tampering");
dump(k, sizeof k, "k");
if(0!=crypto_scalarmult_ristretto255(beta, k, alpha)) {
fputs("failed to tamper with k\nabort.\n", stderr);
return 1;
}
return 0;
}
static int guess(uint8_t rwd[OPRF_BYTES], const uint8_t *pwd, const size_t pwd_len) {
//fputs("[1] hashing to group...", stdout);
uint8_t h0pwd[crypto_core_ristretto255_BYTES]={0};
if(0!=voprf_hash_to_group(pwd, pwd_len, h0pwd)) {
fputs("failed to hash to group\nabort\n", stderr);
return 1;
}
// tamper(h0pwd, h0pwd)
uint8_t rwd_[OPRF_BYTES];
if(0!=oprf_Finalize(pwd, pwd_len, h0pwd, rwd_)) {
fputs("failed to finalize OPRF\nabort\n", stderr);
return 1;
}
if(memcmp(rwd,rwd_, OPRF_BYTES)!=0) return -1;
return 0;
}
static int test(void) {
// regular OPRF flow on the client
const uint8_t password[] = "Exploitability of this is low, OPRFs are still cool";
uint8_t alpha[crypto_core_ristretto255_BYTES]={0};
uint8_t r[crypto_core_ristretto255_SCALARBYTES]={0};
if(0!=oprf_Blind(password, sizeof password, r, alpha)) {
fputs("failed to blind password\nabort\n", stderr);
return 1;
}
//dump(r, sizeof r, "r");
// we tamper with beta
uint8_t beta[crypto_core_ristretto255_BYTES]={0};
dump(alpha, sizeof alpha, "alpha");
tamper(alpha, beta);
dump(beta, sizeof beta, "beta");
// regular OPRF flow on the client
uint8_t N[crypto_core_ristretto255_BYTES]={0};
int x = oprf_Unblind(r, beta, N);
if(0!=x) {
fputs("failed to unblind beta\nabort\n", stderr);
return 1;
}
uint8_t rwd[OPRF_BYTES];
if(0!=oprf_Finalize(password, sizeof password, N, rwd)) {
fputs("failed to finalize OPRF\nabort\n", stderr);
return 1;
}
// we "intercept" the oprf output and guess candidate inputs
fprintf(stderr, "guess(\"%s\") = %d\n", password, guess(rwd, password, sizeof password-1));
fprintf(stderr, "guess(\"%s\") = %d\n", password, guess(rwd, password, sizeof password));
return 0;
}
int main(const int argc, const char** argv) {
if(argc<2) {
return usage(argv[0], 0);
}
if(memcmp(argv[1],"tamper",7)==0) {
uint8_t alpha[crypto_core_ristretto255_BYTES];
if(fread(alpha, 1, 32, stdin) != 32) {
fputs("failed to read point\nabort.\n", stderr);
return 1;
}
uint8_t beta[crypto_core_ristretto255_BYTES];
if(0!=tamper(alpha, beta)) {
return 1;
};
fwrite(beta, 1, sizeof beta, stdout);
return 0;
}
if(memcmp(argv[1],"guess",6)==0) {
if(argc<3) {
return usage(argv[0], 1);
}
uint8_t rwd[OPRF_BYTES];
if(fread(rwd, 1, OPRF_BYTES, stdin) != OPRF_BYTES) {
fputs("failed to read rwd\nabort.\n", stderr);
return 1;
}
return guess(rwd, (uint8_t*) argv[2], strlen(argv[2]));
}
if(memcmp(argv[1],"test",5)==0) {
return test();
}
return usage(argv[0], 1);
}
liboprf-0.9.4/python/ 0000775 0000000 0000000 00000000000 15146734002 0014504 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/python/.gitignore 0000664 0000000 0000000 00000000056 15146734002 0016475 0 ustar 00root root 0000000 0000000 pyoprf.egg-info
pyoprf/__pycache__
build
dist
liboprf-0.9.4/python/MANIFEST.in 0000664 0000000 0000000 00000000037 15146734002 0016242 0 ustar 00root root 0000000 0000000 include README.md
include *.py
liboprf-0.9.4/python/README.md 0000664 0000000 0000000 00000013016 15146734002 0015764 0 ustar 00root root 0000000 0000000 # pyoprf
pyoprf offers Python bindings for the liboprf library, allowing integration of Oblivious Pseudorandom Functions (OPRFs) into Python applications. It provides access to the [features](../README.md#features) of the [liboprf](https://github.com/stef/liboprf) library.
## Installation
### Prerequisites
- [liboprf](https://github.com/stef/liboprf): The core library
- [libsodium](https://github.com/jedisct1/libsodium): Required dependency for liboprf
- OpenSSL: For TLS connections between the participants
### Installing from PyPI
```bash
pip install pyoprf
```
### Installing from source
```bash
git clone https://github.com/stef/liboprf.git
cd liboprf/python
pip install .
```
## Usage
For detailed usage examples, refer to the [`test.py`](./tests/test.py) file and the [`examples`](/examples) folder.
### Basic Example
Imagine a scenario where a client wants to retrieve data from a server using a password, but doesn't want to reveal the actual password to the server:
```python
import pyoprf
# Basic OPRF evaluation process
# Step 1: Client blinds the input value
input_value = b"password123"
blind_factor, blinded_input = pyoprf.blind(input_value)
# Step 2: Server generates a key and evaluates the blinded input
server_key = pyoprf.keygen()
server_evaluation = pyoprf.evaluate(server_key, blinded_input)
# Step 3: Client unblinds the server's response
unblinded_result = pyoprf.unblind(blind_factor, server_evaluation)
# Step 4: Client finalizes the OPRF computation
final_result = pyoprf.finalize(input_value, unblinded_result)
print(f"OPRF result: {final_result.hex()}")
# Verify that repeated evaluations with the same key and input produce the same result
blind_factor2, blinded_input2 = pyoprf.blind(input_value)
server_evaluation2 = pyoprf.evaluate(server_key, blinded_input2)
unblinded_result2 = pyoprf.unblind(blind_factor2, server_evaluation2)
final_result2 = pyoprf.finalize(input_value, unblinded_result2)
print(f"Verification result: {final_result2.hex()}")
assert final_result == final_result2, "OPRF evaluations should be deterministic for the same input and key"
# The `final_result` can be used as a key for encryption, authentication token, and more.
# Only client can derive this value without the server learning the password or the final result.
```
### Threshold Example
Suppose you want to build a password authentication system that distributes trust across multiple servers, so no single server can learn a user's password. The library also supports threshold OPRFs, where multiple servers hold shares of a key:
```python
import pyoprf
# Setting up a threshold OPRF with 3 servers, threshold of 2
# Server setup, which would happen on each server
n = 3 # Total number of servers
t = 2 # The minimum servers needed, also called the threshold
# Generate a key
key = pyoprf.keygen()
# Create shares of the key for distributed evaluation
shares = pyoprf.create_shares(key, n, t)
# On client
input_value = b"password123"
blind_factor, blinded_input = pyoprf.blind(input_value)
# Each server evaluates the input with its share
evaluations = []
for i in range(n):
# This evaluation happens on server i
server_evaluation = pyoprf.evaluate(shares[i][1:], blinded_input)
evaluations.append(shares[i][:1] + server_evaluation)
# Client combines evaluations (need at least t of them)
collected_evaluations = evaluations[:t] # Just use the first t evaluations
combined = pyoprf.thresholdmult(collected_evaluations)
# Client unblinds the combined result
unblinded = pyoprf.unblind(blind_factor, combined)
# Finalize to get the OPRF output
final_result = pyoprf.finalize(input_value, unblinded)
print(f"Threshold OPRF result: {final_result.hex()}")
# Verify that it matches a direct evaluation with the key
server_evaluation = pyoprf.evaluate(key, blinded_input)
unblinded_direct = pyoprf.unblind(blind_factor, server_evaluation)
direct_result = pyoprf.finalize(input_value, unblinded_direct)
print(f"Direct OPRF result: {direct_result.hex()}")
assert final_result == direct_result, "Threshold evaluation should match direct evaluation"
```
## Troubleshooting
If you encounter issues, first ensure that libsodium, liboprf and OpenSSL are properly installed.
### OpenSSL Header Issues
If after installing OpenSSL, you get the error `'openssl/crypto.h' file not found`, you might need to provide OpenSSL headers to the compiler. For example, if OpenSSL was installed on Mac using Homebrew:
```
export CFLAGS="-I/opt/homebrew/opt/openssl@3/include"
export LDFLAGS="-L/opt/homebrew/opt/openssl@3/lib"
```
### Library Loading Issues
When running Python code, you might encounter errors like:
```
OSError: liboprf.so.0: cannot open shared object file: No such file or directory
OSError: liboprf-noiseXK.so.0: cannot open shared object file: No such file or directory
```
To fix this, you can try to install liboprf globally on your system.
Either by using your distributions package manager:
```sh
% sudo apt install liboprf0t64
```
Or install liboprf from source:
```sh
cd /path/to/liboprf/src
sudo PREFIX=/usr make install
sudo ldconfig
```
Or by using environment variables, first create symbolic links:
```bash
cd /path/to/liboprf/src
ln -s liboprf.so liboprf.so.0
cd noise_xk
ln -s liboprf-noiseXK.so liboprf-noiseXK.so.0
```
Then when running your Python code, use the LD_LIBRARY_PATH environment variable:
```bash
LD_LIBRARY_PATH=/path/to/liboprf/src:/path/to/liboprf/src/noise_xk python your_script.py
```
## Documentation
For more information on the underlying liboprf functionality, visit the [liboprf documentation](../README.md).
## License
LGPLv3.0+
liboprf-0.9.4/python/examples/ 0000775 0000000 0000000 00000000000 15146734002 0016322 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/python/examples/3hashtdh.py 0000775 0000000 0000000 00000001476 15146734002 0020415 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
from pyoprf import keygen, create_shares, blind, evaluate, unblind, thresholdmult
from pysodium import randombytes, crypto_core_ristretto255_from_hash, crypto_generichash, crypto_core_ristretto255_add
k = keygen()
shares = create_shares(k, 5, 3)
zero_shares = create_shares(bytes([0]*32), 5, 3)
r, alpha = blind(b"test")
ssid_S = randombytes(32)
betas = []
for ki, zi in zip(shares,zero_shares):
h2 = evaluate(
zi[1:],
crypto_core_ristretto255_from_hash(crypto_generichash(ssid_S + alpha, outlen=64)),
)
beta = evaluate(ki[1:], alpha)
betas.append(ki[:1]+crypto_core_ristretto255_add(beta, h2))
# normal 2hashdh(k,"test")
beta = evaluate(k, alpha)
Nt0 = unblind(r, beta)
print(Nt0)
beta = thresholdmult(betas[:3])
Nt1 = unblind(r, beta)
print(Nt1)
assert Nt0 == Nt1
liboprf-0.9.4/python/examples/toprf-update.py 0000775 0000000 0000000 00000020633 15146734002 0021315 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Test for TP DKG wrapper of pyoprf/liboprf
SPDX-FileCopyrightText: 2024, Marsiske Stefan
SPDX-License-Identifier: LGPL-3.0-or-later
Copyright (c) 2024, Marsiske Stefan.
All rights reserved.
This file is part of liboprf.
liboprf is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
liboprf is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with liboprf. If not, see .
"""
import pyoprf, pysodium, ctypes as c
from itertools import combinations
n = 9
t = 4
ts_epsilon = 5
# enable verbose logging for tp-dkg
libc = c.cdll.LoadLibrary('libc.so.6')
cstderr = c.c_void_p.in_dll(libc, 'stderr')
log_file = c.c_void_p.in_dll(pyoprf.liboprf,'log_file')
log_file.value = cstderr.value
# create some long-term keypairs
sig_pks = []
sig_sks = []
for _ in range(n+1):
pk, sk = pysodium.crypto_sign_keypair()
sig_pks.append(pk)
sig_sks.append(sk)
noise_pks = []
noise_sks = []
for _ in range(n):
sk = pysodium.randombytes(pysodium.crypto_scalarmult_SCALARBYTES)
pk = pysodium.crypto_scalarmult_base(sk)
noise_sks.append(sk)
noise_pks.append(pk)
# initialize the TP and get the first message
stp, msg0 = pyoprf.stp_dkg_start_stp(n, t, ts_epsilon, "pyoprf stp_dkg test", sig_pks, sig_sks[0])
print(f"n: {pyoprf.stp_dkg_stpstate_n(stp)}, t: {pyoprf.stp_dkg_stpstate_t(stp)}, sid: {bytes(c for c in pyoprf.stp_dkg_stpstate_sessionid(stp)).hex()}")
# initialize all peers with the 1st message from TP
keystore = { pysodium.crypto_generichash(s): (s, n) for s,n in zip(sig_pks[1:], noise_pks)}
#typedef int (*Keyloader_CB)(const uint8_t id[crypto_generichash_BYTES],
# void *arg,
# uint8_t sigpk[crypto_sign_PUBLICKEYBYTES],
# uint8_t noise_pk[crypto_scalarmult_BYTES]);
@c.CFUNCTYPE(c.c_int, c.POINTER(c.c_ubyte), c.POINTER(c.c_ubyte), c.POINTER(c.c_ubyte), c.POINTER(c.c_ubyte))
def load_key(keyid, arg, sig_pk, noise_pk):
rec = keystore.get(bytes(keyid[:pysodium.crypto_generichash_BYTES]))
if rec is None: return 1
c.memmove(sig_pk, rec[0], len(rec[0]))
c.memmove(noise_pk, rec[1], len(rec[1]))
return 0
peers=[]
for i in range(n):
peer = pyoprf.stp_dkg_peer_start(ts_epsilon, sig_sks[i+1], noise_sks[i], sig_pks[0], msg0, keyloader=load_key)
peers.append(peer)
for i in range(n):
assert(pyoprf.stp_dkg_peerstate_sessionid(peers[i]) == pyoprf.stp_dkg_stpstate_sessionid(stp))
assert(sig_sks[i+1] == pyoprf.stp_dkg_peerstate_lt_sk(peers[i]))
peer_msgs = []
while pyoprf.stp_dkg_stp_not_done(stp):
ret, sizes = pyoprf.stp_dkg_stp_input_sizes(stp)
# peer_msgs = (recv(size) for size in sizes)
msgs = b''.join(peer_msgs)
cur_step = pyoprf.stp_dkg_stpstate_step(stp)
try:
stp_out = pyoprf.stp_dkg_stp_next(stp, msgs)
#print(f"tp: msg[{tp[0].step}]: {tp_out.raw.hex()}")
except Exception as e:
#cheaters, cheats = pyoprf.stp_dkg_get_cheaters(stp)
#print(f"Warning during the distributed key generation the peers misbehaved: {sorted(cheaters)}")
#for k, v in cheats:
# print(f"\tmisbehaving peer: {k} was caught: {v}")
raise ValueError(f"{e} | tp step {cur_step}")
peer_msgs = []
while(len(b''.join(peer_msgs))==0 and pyoprf.stp_dkg_peer_not_done(peers[0])):
for i in range(n):
if(len(stp_out)>0):
msg = pyoprf.stp_dkg_stp_peer_msg(stp, stp_out, i)
#print(f"tp -> peer[{i+1}] {msg.hex()}")
else:
msg = ''
out = pyoprf.stp_dkg_peer_next(peers[i], msg)
if(len(out)>0):
peer_msgs.append(out)
#print(f"peer[{i+1}] -> tp {peer_msgs[-1].hex()}")
stp_out = ''
# we are done, let's check the shares
k0shares = [pyoprf.stp_dkg_peerstate_share(peers[i]) for i in range(n)]
k0commitments = pyoprf.stp_dkg_stpstate_commitments(stp)
print("commitments", k0commitments)
for i, share in enumerate(k0shares):
print(f"share[{i+1}] {share.hex()} {k0commitments[i].hex()}")
ci = pyoprf.stp_dkg_peerstate_commitments(peers[i])
assert ci == k0commitments
kc0, blind = pyoprf.dkg_vss_reconstruct(n, t, 0, k0shares, k0commitments)
print("kc0 is", kc0.hex())
for s_sub in combinations(k0shares, t):
v, _ = pyoprf.dkg_vss_reconstruct(n, t, 0, s_sub)
assert kc0 == v
keyid = pyoprf.stp_dkg_stpstate_sessionid(stp)
# clean up allocated buffers
for i in range(n):
pyoprf.stp_dkg_peer_free(peers[i])
# calculate some OPRF
r, alpha = pyoprf.blind(b"test")
betas = tuple(s[:1]+pyoprf.evaluate(s[1:33], alpha) for s in k0shares)
beta = pyoprf.thresholdmult(betas)
oprfed_test = pyoprf.unblind(r, beta)
print('oprf("test")', oprfed_test.hex())
# tOPRF update
stp, msg0 = pyoprf.tupdate_start_stp(n, t, ts_epsilon, "tOPRF update test", sig_pks, keyid, sig_sks[0], k0commitments)
for s,p in zip(sig_sks, sig_pks):
print("sp", s.hex(), p.hex())
for s,p in zip(noise_sks, noise_pks):
print("nsp", s.hex(), p.hex())
peers=[]
for i in range(n):
ctx, keyid, stp_pub = pyoprf.tupdate_peer_start(ts_epsilon, sig_sks[i+1], noise_sks[i], msg0)
#print(keyid.hex(), stp_pub.hex())
# based on keyid load the relevant parameters: n, t, share, commitment.
ctx = pyoprf.tupdate_peer_set_bufs(ctx, n, t, i+1, sig_pks, noise_pks, k0shares[i], k0commitments)
peers.append(ctx)
#print(ctx)
for i in range(n):
assert(pyoprf.tupdate_peerstate_sessionid(peers[i]) == pyoprf.tupdate_stpstate_sessionid(stp))
peer_msgs = []
while pyoprf.tupdate_peer_not_done(peers[0]):
peer_msgs = []
while(len(b''.join(peer_msgs))==0 and pyoprf.tupdate_peer_not_done(peers[0])):
for i in range(n):
if(len(stp_out)>0):
msg = pyoprf.tupdate_stp_peer_msg(stp, stp_out, i)
#print(f"tp -> peer[{i+1}] {msg.hex()}")
else:
msg = ''
out = pyoprf.tupdate_peer_next(peers[i], msg)
if(len(out)>0):
peer_msgs.append(out)
#print(f"peer[{i+1}] -> tp {peer_msgs[-1].hex()}")
stp_out = ''
if pyoprf.tupdate_stp_not_done(stp):
ret, sizes = pyoprf.tupdate_stp_input_sizes(stp)
# peer_msgs = (recv(size) for size in sizes)
msgs = b''.join(peer_msgs)
cur_step = pyoprf.tupdate_stpstate_step(stp)
try:
stp_out = pyoprf.tupdate_stp_next(stp, msgs)
#print(f"tp: msg[{tp[0].step}]: {tp_out.raw.hex()}")
except Exception as e:
#cheaters, cheats = pyoprf.stp_dkg_get_cheaters(stp)
#print(f"Warning during the distributed key generation the peers misbehaved: {sorted(cheaters)}")
#for k, v in cheats:
# print(f"\tmisbehaving peer: {k} was caught: {v}")
raise ValueError(f"{e} | tp step {cur_step}")
delta = pyoprf.tupdate_stpstate_delta(stp)
print("delta", delta.hex())
k1shares = [pyoprf.tupdate_peerstate_share(peers[i]) for i in range(n)]
k1commitments = tuple(pyoprf.tupdate_peerstate_commitment(peers[i]) for i in range(n))
assert k1commitments == pyoprf.tupdate_stpstate_commitments(stp)
for i, share in enumerate(k1shares):
print(f"share[{i+1}] {share.hex()} {k1commitments[i].hex()}")
assert k1commitments == pyoprf.tupdate_peerstate_commitments(peers[i])
kc1, blind = pyoprf.dkg_vss_reconstruct(n, t, 0, k1shares, k1commitments)
print("kc1 is", kc1.hex())
for s_sub in combinations(k1shares, t):
v, _ = pyoprf.dkg_vss_reconstruct(n, t, 0, s_sub)
assert kc1 == v
kc0inv = pysodium.crypto_core_ristretto255_scalar_invert(kc0)
deltakc = pysodium.crypto_core_ristretto255_scalar_mul(kc1, kc0inv)
print("delta", deltakc.hex())
assert delta == deltakc
updated_test = pysodium.crypto_scalarmult_ristretto255(deltakc, oprfed_test)
r, alpha = pyoprf.blind(b"test")
betas = tuple(s[:1]+pyoprf.evaluate(s[1:33], alpha) for s in k1shares)
beta = pyoprf.thresholdmult(betas)
updated_oprfed_test = pyoprf.unblind(r, beta)
print('updated oprf\'("test")', updated_test.hex())
print('oprf\'("test") ', updated_oprfed_test.hex())
assert updated_test == updated_oprfed_test
liboprf-0.9.4/python/examples/tpdkg_test.py 0000775 0000000 0000000 00000007634 15146734002 0021061 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Test for TP DKG wrapper of pyoprf/liboprf
SPDX-FileCopyrightText: 2024, Marsiske Stefan
SPDX-License-Identifier: LGPL-3.0-or-later
Copyright (c) 2024, Marsiske Stefan.
All rights reserved.
This file is part of liboprf.
liboprf is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
liboprf is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with liboprf. If not, see .
"""
import pyoprf, pysodium, ctypes
n = 5
t = 3
ts_epsilon = 5
# enable verbose logging for tp-dkg
libc = ctypes.cdll.LoadLibrary('libc.so.6')
cstderr = ctypes.c_void_p.in_dll(libc, 'stderr')
log_file = ctypes.c_void_p.in_dll(pyoprf.liboprf,'log_file')
log_file.value = cstderr.value
# create some long-term keypairs
peer_lt_pks = []
peer_lt_sks = []
for _ in range(n):
pk, sk = pysodium.crypto_sign_keypair()
peer_lt_pks.append(pk)
peer_lt_sks.append(sk)
# initialize the TP and get the first message
tp, msg0 = pyoprf.tpdkg_start_tp(n, t, ts_epsilon, "pyoprf tpdkg test", peer_lt_pks)
print(f"n: {pyoprf.tpdkg_tpstate_n(tp)}, t: {pyoprf.tpdkg_tpstate_t(tp)}, sid: {bytes(c for c in pyoprf.tpdkg_tpstate_sessionid(tp)).hex()}")
# initialize all peers with the 1st message from TP
peers=[]
for i in range(n):
peer = pyoprf.tpdkg_peer_start(ts_epsilon, peer_lt_sks[i], msg0)
peers.append(peer)
for i in range(n):
assert(pyoprf.tpdkg_peerstate_sessionid(peers[i]) == pyoprf.tpdkg_tpstate_sessionid(tp))
assert(peer_lt_sks[i] == pyoprf.tpdkg_peerstate_lt_sk(peers[i]))
peer_msgs = []
while pyoprf.tpdkg_tp_not_done(tp):
ret, sizes = pyoprf.tpdkg_tp_input_sizes(tp)
# peer_msgs = (recv(size) for size in sizes)
msgs = b''.join(peer_msgs)
cur_step = pyoprf.tpdkg_tpstate_step(tp)
try:
tp_out = pyoprf.tpdkg_tp_next(tp, msgs)
#print(f"tp: msg[{tp[0].step}]: {tp_out.raw.hex()}")
except Exception as e:
cheaters, cheats = pyoprf.tpdkg_get_cheaters(tp)
print(f"Warning during the distributed key generation the peers misbehaved: {sorted(cheaters)}")
for k, v in cheats:
print(f"\tmisbehaving peer: {k} was caught: {v}")
raise ValueError(f"{e} | tp step {cur_step}")
peer_msgs = []
while(len(b''.join(peer_msgs))==0 and pyoprf.tpdkg_peer_not_done(peers[0])):
for i in range(n):
if(len(tp_out)>0):
msg = pyoprf.tpdkg_tp_peer_msg(tp, tp_out, i)
#print(f"tp -> peer[{i+1}] {msg.hex()}")
else:
msg = ''
out = pyoprf.tpdkg_peer_next(peers[i], msg)
if(len(out)>0):
peer_msgs.append(out)
#print(f"peer[{i+1}] -> tp {peer_msgs[-1].hex()}")
tp_out = ''
# we are done, let's check the shares
shares = [pyoprf.tpdkg_peerstate_share(peers[i]) for i in range(n)]
for i, share in enumerate(shares):
print(f"share[{i+1}] {share.hex()}")
v0 = pyoprf.thresholdmult([bytes([i+1])+pysodium.crypto_scalarmult_ristretto255_base(shares[i][1:]) for i in (0,1,2)])
v1 = pyoprf.thresholdmult([bytes([i+1])+pysodium.crypto_scalarmult_ristretto255_base(shares[i][1:]) for i in (2,0,3)])
assert v0 == v1
v2 = pyoprf.thresholdmult([bytes([i+1])+pysodium.crypto_scalarmult_ristretto255_base(shares[i][1:]) for i in (2,1,4)])
assert v0 == v2
secret = pyoprf.dkg_reconstruct(shares[:t])
#print("secret", secret.hex())
assert v0 == pysodium.crypto_scalarmult_ristretto255_base(secret)
# clean up allocated buffers
for i in range(n):
pyoprf.tpdkg_peer_free(peers[i])
liboprf-0.9.4/python/pyoprf/ 0000775 0000000 0000000 00000000000 15146734002 0016023 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/python/pyoprf/__init__.py 0000775 0000000 0000000 00000213126 15146734002 0020144 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Wrapper for liboprf library
SPDX-FileCopyrightText: 2023, Marsiske Stefan
SPDX-License-Identifier: LGPL-3.0-or-later
Copyright (c) 2023, Marsiske Stefan.
All rights reserved.
This file is part of liboprf.
liboprf is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
liboprf is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with liboprf. If not, see .
"""
import ctypes
import ctypes.util
import pysodium, os
import platform, sys
from typing import List, Tuple
from itertools import zip_longest
if "BYZANTINE_DKG" in os.environ:
liboprf = ctypes.cdll.LoadLibrary(os.environ['BYZANTINE_DKG'])
print("\x1b[1m\x1b[31mwarning: loading intentionally corrupting version of liboprf, only use this for testing!\x1b[0m", file=sys.stderr)
else:
liboprf = ctypes.cdll.LoadLibrary(ctypes.util.find_library('oprf') or ctypes.util.find_library('liboprf'))
if not liboprf._name:
raise ValueError('Unable to find liboprf')
def split_by_n(iterable, n):
return list(zip_longest(*[iter(iterable)]*n, fillvalue=''))
def __check(code):
if code != 0:
raise ValueError(f"error: {code}")
# (CFRG/IRTF) OPRF section
OPRF_BYTES=64
# This function generates an OPRF private key.
#
# This is almost the KeyGen OPRF function defined in the RFC: since
# this lib does not implement V oprf, we don't need a pubkey and so
# we don't bother with all that is related.
#
# @param [out] k - the per-user OPRF private key
# void oprf_KeyGen(uint8_t kU[crypto_core_ristretto255_SCALARBYTES]);
def keygen() -> bytes:
k = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_SCALARBYTES)
liboprf.oprf_KeyGen(k)
return k.raw
# This function converts input x into an element of the OPRF group, randomizes it
# by some scalar r, producing blinded, and outputs (r, blinded).
#
# This is the Blind OPRF function defined in the RFC.
#
# @param [in] x - the input value to blind
# @param [out] r - an OPRF scalar value used for randomization
# @param [out] blinded - a serialized OPRF group element, a byte array of fixed length,
# the blinded version of x, an input to oprf_Evaluate
# @return The function raises a ValueError if there is something wrong with the inputs.
#
#int oprf_Blind(const uint8_t *x, const uint16_t x_len,
# uint8_t r[crypto_core_ristretto255_SCALARBYTES],
# uint8_t blinded[crypto_core_ristretto255_BYTES]);
def blind(x: bytes) -> (bytes, bytes):
r = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_SCALARBYTES)
blinded = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_BYTES)
__check(liboprf.oprf_Blind(x, ctypes.c_size_t(len(x)), r, blinded))
return r.raw, blinded.raw
# This function evaluates input element blinded using private key k, yielding output
# element Z.
#
# This is the Evaluate OPRF function defined in the RFC.
#
# @param [in] key - a private key - the output of keygen()
# @param [in] blinded - a serialized OPRF group element, a byte array
# of fixed length, an output of blind()
# @param [out] Z - a serialized OPRF group element, a byte array of fixed
# length, an input to oprf_Unblind
# @return The function raises a ValueError if there is something wrong with the inputs.
#int oprf_Evaluate(const uint8_t k[crypto_core_ristretto255_SCALARBYTES],
# const uint8_t blinded[crypto_core_ristretto255_BYTES],
# uint8_t Z[crypto_core_ristretto255_BYTES]);
def evaluate(key: bytes, blinded: bytes) -> bytes:
if len(key) != pysodium.crypto_core_ristretto255_SCALARBYTES:
raise ValueError("key has incorrect length")
if not isinstance(key, bytes):
raise ValueError("key is not of type bytes")
if len(blinded) != pysodium.crypto_core_ristretto255_BYTES:
raise ValueError("blinded param has incorrect length")
if not isinstance(blinded, bytes):
raise ValueError("blinded is not of type bytes")
Z = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_BYTES)
__check(liboprf.oprf_Evaluate(key, blinded, Z))
return Z.raw
# This function removes random scalar r from Z, yielding output N.
#
# This is the Unblind OPRF function defined in the RFC.
#
# If you do not call finalize() on the result the output is equivalent
# to the OPRF protcol we refer to as HashDH - this protocol retains
# the algebraic structure of the value, and has weaker security
# guarantees, than the full 2HashDH which is equivalent to running
# finalize on the output of blind(). The hashDH variant is not
# explicitly specified by the CFRG/IRTF specification. This hashDH
# variant has one property that makes it interesting: it is an
# updateable OPRF - that is if the server updates their key, they can
# calculate a public delta value, that can be applied by the client to
# the output of blind() and the result will be as if the client and
# the server run the OPRF protocol with the original input and the new
# key. It is important to note that the delta value is not sensitive,
# and can be public.
#
# @param [in] r - an OPRF scalar value used for randomization in oprf_Blind
# @param [in] Z - a serialized OPRF group element, a byte array of fixed length,
# an output of oprf_Evaluate
# @param [out] N - a serialized OPRF group element with random scalar r removed,
# a byte array of fixed length, an input to oprf_Finalize
# @return The function raises a ValueError if there is something wrong with the inputs.
#int oprf_Unblind(const uint8_t r[crypto_core_ristretto255_SCALARBYTES],
# const uint8_t Z[crypto_core_ristretto255_BYTES],
# uint8_t N[crypto_core_ristretto255_BYTES]);
def unblind(r: bytes, Z: bytes) -> bytes:
if len(r) != pysodium.crypto_core_ristretto255_SCALARBYTES:
raise ValueError("param r has incorrect length")
if not isinstance(r, bytes):
raise ValueError("param r is not of type bytes")
if len(Z) != pysodium.crypto_core_ristretto255_BYTES:
raise ValueError("param Z has incorrect length")
if not isinstance(Z, bytes):
raise ValueError("param Z is not of type bytes")
N = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_BYTES)
__check(liboprf.oprf_Unblind(r, Z, N))
return N.raw
# This function computes the OPRF output using input x, N, and domain
# separation tag info.
#
# This is the Finalize OPRF function defined in the RFC.
#
# @param [in] x - a value used to compute OPRF (the same value that
# was used as input to be blinded)
# @param [in] N - a serialized OPRF group element, a byte array of fixed length,
# an output of oprf_Unblind
# @param [out] y - an OPRF output
# @return The function raises a ValueError if there is something wrong with the inputs.
#int oprf_Finalize(const uint8_t *x, const uint16_t x_len,
# const uint8_t N[crypto_core_ristretto255_BYTES],
# uint8_t rwdU[OPRF_BYTES]);
def finalize(x: bytes, N: bytes) -> bytes:
if len(N) != pysodium.crypto_core_ristretto255_BYTES:
raise ValueError("param N has incorrect length")
if not isinstance(N, bytes):
raise ValueError("param N is not of type bytes")
y = ctypes.create_string_buffer(OPRF_BYTES)
__check(liboprf.oprf_Finalize(x, ctypes.c_size_t(len(x)), N, y))
return y.raw
# This function combines unblind() and finalize() as a convenience
def unblind_finalize(r: bytes, Z: bytes, x: bytes) -> bytes:
return finalize(x, unblind(r,Z))
# TOPRF section
TOPRF_Share_BYTES=pysodium.crypto_core_ristretto255_SCALARBYTES+1
TOPRF_Part_BYTES=pysodium.crypto_core_ristretto255_BYTES+1
# This function calculates a lagrange coefficient based on the index
# and the indexes of the other contributing shareholders.
#
# @param [in] index - the index of the shareholder whose lagrange
# coefficient we're calculating, must be greater than 0
#
# @param [in] peers - list of the shares that contribute to the reconstruction
#
# @param [out] result - the lagrange coefficient
#void coeff(const int index, const int peers_len, const uint8_t peers[peers_len], uint8_t result[crypto_scalarmult_ristretto255_SCALARBYTES]);
def coeff(index: int, peers: list) -> bytes:
if index < 1: raise ValueError("index must be positive integer")
if len(peers) < 2: raise ValueError("peers must be a list of at least 2 integers")
peers_len=ctypes.c_size_t(len(peers))
c = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_SCALARBYTES)
liboprf.coeff(index, peers_len, peers, c)
return c.raw
# This function creates shares of secret in a (threshold, n) scheme
# over the curve ristretto255
#
# @param [in] secret - the scalar value to be secretly shared
#
# @param [in] n - the number of shares created
#
# @param [in] threshold - the threshold needed to reconstruct the secret
#
# @param [out] shares - n shares
#
# @return The function raises a ValueError if there is something wrong with the inputs.
#void toprf_create_shares(const uint8_t secret[crypto_core_ristretto255_SCALARBYTES],
# const uint8_t n,
# const uint8_t threshold,
# uint8_t shares[n][TOPRF_Share_BYTES]);
bytes_list_t = List[bytes]
def create_shares(secret: bytes, n: int, t: int) -> bytes_list_t:
if len(secret) != pysodium.crypto_core_ristretto255_SCALARBYTES:
raise ValueError("secret has incorrect length")
if not isinstance(secret, bytes):
raise ValueError("secret is not of type bytes")
if n < t:
raise ValueError("t cannot be bigger than n")
if t < 2:
raise ValueError("t must be bigger than 1")
shares = ctypes.create_string_buffer(n*TOPRF_Share_BYTES)
__check(liboprf.toprf_create_shares(secret, n, t, shares))
return tuple([bytes(s) for s in split_by_n(shares.raw, TOPRF_Share_BYTES)])
# This function recovers the secret in the exponent using lagrange interpolation
# over the curve ristretto255
#
# The shareholders are not aware if they are contributing to a
# threshold or non-threshold oprf evaluation, from their perspective
# nothing changes in this approach.
#
# @param [in] responses - is an array of shares (k_i) multiplied by a
# point (P) on the r255 curve
#
# @param [in] responses_len - the number of elements in the response array
#
# @param [out] result - the reconstructed value of P multipled by k
#
# @return The function raises a ValueError if there is something wrong with the inputs.
#int toprf_thresholdmult(const size_t response_len,
# const uint8_t responses[response_len][TOPRF_Part_BYTES],
# uint8_t result[crypto_scalarmult_ristretto255_BYTES]);
def thresholdmult(responses: bytes_list_t) -> bytes:
if len(responses) < 2: raise ValueError("responses must be a list of at least 2 integers")
if not all(isinstance(r,bytes) for r in responses):
raise ValueError("at least one of the responses is not of type bytes")
if not all(len(r)==TOPRF_Part_BYTES for r in responses):
raise ValueError("at least one of the responses is not of correct size")
responses_len=ctypes.c_size_t(len(responses))
responses_buf = ctypes.create_string_buffer(b''.join(responses))
result = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_BYTES)
__check(liboprf.toprf_thresholdmult(responses_len, responses_buf, result))
return result.raw
# This function is the efficient threshold version of oprf_Evaluate.
#
# This function needs to know in advance the indexes of all the
# shares that will be combined later in the toprf_thresholdcombine() function.
# by doing so this reduces the total costs and distributes them to the shareholders.
#
# @param [in] k - a private key (for OPAQUE, this is kU, the user's
# OPRF private key)
#
# @param [in] blinded - a serialized OPRF group element, a byte array
# of fixed length, an output of oprf_Blind (for OPAQUE, this
# is the blinded pwdU, the user's password)
#
# @param [in] self - the index of the current shareholder
#
# @param [in] indexes - the indexes of the all the shareholders
# contributing to this oprf evaluation,
#
# @param [in] index_len - the length of the indexes array,
#
# @param [out] Z - a serialized OPRF group element, a byte array of fixed length,
# an input to oprf_Unblind
#
# @return The function raises a ValueError if there is something wrong with the inputs.
#int toprf_Evaluate(const uint8_t k[TOPRF_Share_BYTES],
# const uint8_t blinded[crypto_core_ristretto255_BYTES],
# const uint8_t self, const uint8_t *indexes, const uint16_t index_len,
# uint8_t Z[TOPRF_Part_BYTES]);
def threshold_evaluate(k: bytes, blinded: bytes, self: int, indexes: list) -> bytes:
if len(k) != TOPRF_Share_BYTES:
raise ValueError("param k has incorrect length")
if not isinstance(k, bytes):
raise ValueError("param k is not of type bytes")
if len(blinded) != pysodium.crypto_core_ristretto255_BYTES:
raise ValueError("blinded param has incorrect length")
if not isinstance(blinded, bytes):
raise ValueError("blinded is not of type bytes")
if(self>255 or self<1):
raise ValueError("self outside valid range")
if(not all(i>0 and i<256 for i in indexes)):
raise ValueError("index(es) outside valid range")
index_len=ctypes.c_uint16(len(indexes))
indexes_buf=ctypes.create_string_buffer(bytes(indexes))
Z = ctypes.create_string_buffer(TOPRF_Part_BYTES)
__check(liboprf.toprf_Evaluate(k, blinded, self, indexes_buf, index_len, Z))
return Z.raw
# This function is combines the results of the toprf_Evaluate()
# function to recover the shared secret in the exponent.
#
# @param [in] responses - is an array of shares (k_i) multiplied by a point (P) on the r255 curve
#
# @param [in] responses_len - the number of elements in the response array
#
# @param [out] result - the reconstructed value of P multipled by k
#
# @return The function raises a ValueError if there is something wrong with the inputs.
#void toprf_thresholdcombine(const size_t response_len,
# const uint8_t _responses[response_len][TOPRF_Part_BYTES],
# uint8_t result[crypto_scalarmult_ristretto255_BYTES]);
def threshold_combine(responses: bytes_list_t) -> bytes:
if len(responses) < 2: raise ValueError("responses must be a list of at least 2 integers")
if not all(isinstance(r,bytes) for r in responses):
raise ValueError("at least one of the responses is not of type bytes")
if not all(len(r)==TOPRF_Part_BYTES for r in responses):
raise ValueError("at least one of the responses is not of correct size")
responses_len=ctypes.c_size_t(len(responses))
responses_buf = ctypes.create_string_buffer(b''.join(responses))
result = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_BYTES)
__check(liboprf.toprf_thresholdcombine(responses_len, responses_buf, result))
return result.raw
#int toprf_3hashtdh(const uint8_t k[TOPRF_Share_BYTES],
# const uint8_t z[TOPRF_Share_BYTES],
# const uint8_t alpha[crypto_core_ristretto255_BYTES],
# const uint8_t *ssid_S, const uint16_t ssid_S_len,
# uint8_t beta[TOPRF_Part_BYTES]);
def _3hashtdh(k: bytes, z: bytes, alpha: bytes, ssid_S: bytes) -> bytes:
if len(k) != TOPRF_Share_BYTES:
raise ValueError("param k has incorrect length")
if not isinstance(k, bytes):
raise ValueError("param k is not of type bytes")
if len(z) != TOPRF_Share_BYTES:
raise ValueError("param z has incorrect length")
if not isinstance(z, bytes):
raise ValueError("param z is not of type bytes")
if len(alpha) != pysodium.crypto_core_ristretto255_BYTES:
raise ValueError("alpha param has incorrect length")
if not isinstance(alpha, bytes):
raise ValueError("alpha is not of type bytes")
if not isinstance(ssid_S, bytes):
raise ValueError("ssid_S is not of type bytes")
if len(ssid_S) > (1<<16)-1:
raise ValueError("ssid_S is too long")
ssid_S_len=ctypes.c_uint16(len(ssid_S))
beta = ctypes.create_string_buffer(TOPRF_Part_BYTES)
__check(liboprf.toprf_3hashtdh(k, z, alpha, ssid_S, ssid_S_len, beta))
return beta.raw
# todo documentation!
#int dkg_start(const uint8_t n,
# const uint8_t threshold,
# uint8_t commitment_hash[dkg_hash_BYTES],
# uint8_t commitments[dkg_commitment_BYTES(threshold)],
# TOPRF_Share shares[n]);
def dkg_start(n : int, t : int) -> (bytes, bytes, bytes_list_t):
if n < t:
raise ValueError("t cannot be bigger than n")
if t < 2:
raise ValueError("t must be bigger than 1")
shares = ctypes.create_string_buffer(n*TOPRF_Share_BYTES)
commitments = ctypes.create_string_buffer(t*pysodium.crypto_core_ristretto255_BYTES)
__check(liboprf.dkg_start(n, t, commitments, shares))
shares = tuple([bytes(s) for s in split_by_n(shares.raw, TOPRF_Share_BYTES)])
return commitments.raw, shares
#int dkg_verify_commitments(const uint8_t n,
# const uint8_t threshold,
# const uint8_t self,
# const uint8_t commitments[n][threshold*crypto_core_ristretto255_BYTES],
# const TOPRF_Share shares[n],
# uint8_t fails[n],
# uint8_t *fails_len);
def dkg_verify_commitments(n: int, t: int, self: int,
commitments : bytes_list_t,
shares: bytes_list_t) -> bytes:
if n < t:
raise ValueError("t cannot be bigger than n")
if t < 2:
raise ValueError("t must be bigger than 1")
if self < 1 or self > n:
raise ValueError("self must 1 <= self <= n")
if len(commitments) != n*t*pysodium.crypto_core_ristretto255_BYTES:
raise ValueError(f"signed_commitments must be {n*t*pysodium.crypto_core_ristretto255_BYTES} bytes is instead: {len(commitments)}")
shares = b''.join(shares)
if len(shares) != n*TOPRF_Share_BYTES:
raise ValueError(f"shares must be {TOPRF_Share_BYTES*n} bytes is instead {len(shares)}")
shares = ctypes.create_string_buffer(shares)
fails = ctypes.create_string_buffer(n)
fails_len = ctypes.c_uint8()
__check(liboprf.dkg_verify_commitments(n, t, self,
commitments, shares,
fails, ctypes.byref(fails_len)))
return fails[:fails_len.value]
#void dkg_finish(const uint8_t n,
# const TOPRF_Share shares[n],
# const uint8_t self,
# TOPRF_Share *xi);
def dkg_finish(n: int, shares: List[bytes], self: int, ) -> bytes:
if self < 1 or self > n:
raise ValueError("self must 1 <= self <= n")
shares = b''.join(shares)
if len(shares) != n*TOPRF_Share_BYTES:
raise ValueError(f"shares must be {TOPRF_Share_BYTES*n} bytes is instead {len(shares)}")
shares = ctypes.create_string_buffer(shares)
xi = ctypes.create_string_buffer(TOPRF_Share_BYTES)
xi[0]=self
liboprf.dkg_finish(n, shares, self, xi)
return xi.raw
#void dkg_reconstruct(const size_t response_len,
# const TOPRF_Share responses[response_len][2],
# uint8_t result[crypto_scalarmult_ristretto255_BYTES]);
def dkg_reconstruct(responses) -> bytes_list_t:
rlen = len(responses)
responses = ctypes.create_string_buffer(b''.join(responses))
result = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_BYTES)
liboprf.dkg_reconstruct(rlen, responses, result)
return result.raw
tpdkg_sessionid_SIZE=32
tpdkg_msg0_SIZE = 179 # ( sizeof(TP_DKG_Message) \
# + crypto_generichash_BYTES/*dst*/ \
# + 2 /*n,t*/ \
# + crypto_sign_PUBLICKEYBYTES /* tp_sign_pk */)
tpdkg_msg8_SIZE = 258 # (sizeof(TP_DKG_Message) /* header */ \
# + noise_xk_handshake3_SIZE /* 4th&final noise handshake */ \
# + sizeof(TOPRF_Share) /* msg: the noise_xk wrapped share */ \
# + crypto_secretbox_xchacha20poly1305_MACBYTES /* mac of msg */ \
# + crypto_auth_hmacsha256_BYTES /* key-committing mac over msg*/ )
tpdkg_max_err_SIZE = 128
class TP_DKG_Cheater(ctypes.Structure):
_fields_ = [('step', ctypes.c_int),
('error', ctypes.c_int),
('peer', ctypes.c_uint8),
('other_peer', ctypes.c_uint8),
('invalid_index', ctypes.c_int),
]
#int tpdkg_start_tp(TP_DKG_TPState *ctx, const uint64_t ts_epsilon,
# const uint8_t n, const uint8_t t,
# const char *proto_name, const size_t proto_name_len,
# const size_t msg0_len, TP_DKG_Message *msg0);
#
# also wraps conveniently:
#
# void tpdkg_tp_set_bufs(TP_DKG_TPState *ctx,
# uint8_t (*commitments)[][crypto_core_ristretto255_BYTES],
# uint16_t (*complaints)[],
# uint8_t (*suspicious)[],
# uint8_t (*tp_peers_sig_pks)[][crypto_sign_PUBLICKEYBYTES],
# uint8_t (*peer_lt_pks)[][crypto_sign_PUBLICKEYBYTES],
# uint64_t (*last_ts)[]);
def tpdkg_start_tp(n, t, ts_epsilon, proto_name, peer_lt_pks):
b = ctypes.create_string_buffer(liboprf.tpdkg_tpstate_size()+32)
b_addr = ctypes.addressof(b)
s_addr = b_addr + (b_addr % 32)
state = ctypes.c_void_p(s_addr)
if state.value % 32 != 0:
raise ValueError("cannot align at 32bytes the TP_DKG_TPState struct")
msg = ctypes.create_string_buffer(tpdkg_msg0_SIZE)
__check(liboprf.tpdkg_start_tp(state, ctypes.c_uint64(ts_epsilon), ctypes.c_uint8(n), ctypes.c_uint8(t), proto_name, ctypes.c_size_t(len(proto_name)), ctypes.c_size_t(len(msg.raw)), msg))
peers_sig_pks = ctypes.create_string_buffer(n*pysodium.crypto_sign_PUBLICKEYBYTES)
commitments = ctypes.create_string_buffer(n*t*pysodium.crypto_core_ristretto255_BYTES)
complaints = ctypes.create_string_buffer(n*n*2)
noisy_shares = ctypes.create_string_buffer(n*n*tpdkg_msg8_SIZE)
cheaters = (TP_DKG_Cheater * (t*t - 1))()
peer_lt_pks = b''.join(peer_lt_pks)
last_ts = (ctypes.c_uint64 * n)()
liboprf.tpdkg_tp_set_bufs(state,
ctypes.byref(commitments),
ctypes.byref(complaints),
ctypes.byref(noisy_shares),
ctypes.byref(cheaters),
len(cheaters),
ctypes.byref(peers_sig_pks),
peer_lt_pks,
ctypes.byref(last_ts))
# we need to keep these arrays around, otherwise the gc eats them up.
ctx = (state, cheaters, peers_sig_pks, commitments, complaints, noisy_shares, peer_lt_pks, last_ts, b)
return ctx, msg.raw
#size_t tpdkg_tp_input_size(const TP_DKG_TPState *ctx);
def tpdkg_tp_input_size(ctx):
return liboprf.tpdkg_tp_input_size(ctx[0])
#int tpdkg_tp_input_sizes(const TP_DKG_TPState *ctx, size_t *sizes);
def tpdkg_tp_input_sizes(ctx):
sizes = (ctypes.c_size_t * tpdkg_tpstate_n(ctx))()
ret = liboprf.tpdkg_tp_input_sizes(ctx[0], ctypes.byref(sizes))
return ret, [x for x in sizes]
#size_t tpdkg_tp_output_size(const TP_DKG_TPState *ctx);
def tpdkg_tp_output_size(ctx):
return liboprf.tpdkg_tp_output_size(ctx[0])
#int tpdkg_tp_next(TP_DKG_TPState *ctx, const uint8_t *input, const size_t input_len, uint8_t *output, const size_t output_len);
def tpdkg_tp_next(ctx, msg):
input_len = tpdkg_tp_input_size(ctx)
if len(msg) != input_len: raise ValueError(f"input msg is invalid size: {len(msg)}B must be: {input_len}B")
output_len = tpdkg_tp_output_size(ctx)
output = ctypes.create_string_buffer(output_len)
__check(liboprf.tpdkg_tp_next(ctx[0], msg, ctypes.c_size_t(input_len), output, ctypes.c_size_t(output_len)))
return output
#int tpdkg_tp_peer_msg(const TP_DKG_TPState *ctx, const uint8_t *base, const size_t base_size, const uint8_t peer, const uint8_t **msg, size_t *len);
def tpdkg_tp_peer_msg(ctx, base, peer):
msg = ctypes.POINTER(ctypes.c_char)()
size = ctypes.c_size_t()
__check(liboprf.tpdkg_tp_peer_msg(ctx[0], base, len(base.raw), peer, ctypes.byref(msg), ctypes.byref(size)))
msg = b''.join([msg[i] for i in range(size.value)])
return msg
#int tpdkg_tp_not_done(const TP_DKG_TPState *tp);
def tpdkg_tp_not_done(ctx):
return liboprf.tpdkg_tp_not_done(ctx[0]) == 1
def tpdkg_get_cheaters(ctx):
cheats = []
cheaters = set()
for i in range(tpdkg_tpstate_cheater_len(ctx)):
err = ctypes.create_string_buffer(tpdkg_max_err_SIZE)
p = liboprf.tpdkg_cheater_msg(ctypes.byref(ctx[1][i]), err, tpdkg_max_err_SIZE)
if 0 >= p > tpdkg_tpstate_n(ctx):
print(f"invalid cheater index: {p}, skipping this entry")
continue
cheaters.add(p)
cheats.append((p, err.raw[:err.raw.find(b'\x00')].decode('utf8')))
return cheaters, cheats
liboprf.tpdkg_peerstate_n.restype = ctypes.c_uint8
def tpdkg_peerstate_n(ctx):
return liboprf.tpdkg_peerstate_n(ctx[0])
liboprf.tpdkg_peerstate_t.restype = ctypes.c_uint8
def tpdkg_peerstate_t(ctx):
return liboprf.tpdkg_peerstate_t(ctx[0])
liboprf.tpdkg_peerstate_sessionid.restype = ctypes.POINTER(ctypes.c_uint8)
def tpdkg_peerstate_sessionid(ctx):
ptr = liboprf.tpdkg_peerstate_sessionid(ctx[0])
return bytes(ptr[i] for i in range(tpdkg_sessionid_SIZE))
liboprf.tpdkg_peerstate_lt_sk.restype = ctypes.POINTER(ctypes.c_uint8)
def tpdkg_peerstate_lt_sk(ctx):
ptr = liboprf.tpdkg_peerstate_lt_sk(ctx[0])
return bytes(ptr[i] for i in range(pysodium.crypto_sign_SECRETKEYBYTES))
liboprf.tpdkg_peerstate_share.restype = ctypes.POINTER(ctypes.c_uint8)
def tpdkg_peerstate_share(ctx):
ptr = liboprf.tpdkg_peerstate_share(ctx[0])
return bytes(ptr[i] for i in range(TOPRF_Share_BYTES))
def tpdkg_peerstate_step(ctx):
return liboprf.tpdkg_peerstate_step(ctx[0])
liboprf.tpdkg_tpstate_n.restype = ctypes.c_uint8
def tpdkg_tpstate_n(ctx):
return liboprf.tpdkg_tpstate_n(ctx[0])
liboprf.tpdkg_tpstate_t.restype = ctypes.c_uint8
def tpdkg_tpstate_t(ctx):
return liboprf.tpdkg_tpstate_t(ctx[0])
liboprf.tpdkg_tpstate_cheater_len.restype = ctypes.c_size_t
def tpdkg_tpstate_cheater_len(ctx):
return liboprf.tpdkg_tpstate_cheater_len(ctx[0])
liboprf.tpdkg_tpstate_sessionid.restype = ctypes.POINTER(ctypes.c_uint8)
def tpdkg_tpstate_sessionid(ctx):
ptr = liboprf.tpdkg_tpstate_sessionid(ctx[0])
return bytes(ptr[i] for i in range(tpdkg_sessionid_SIZE))
def tpdkg_tpstate_step(ctx):
return liboprf.tpdkg_tpstate_step(ctx[0])
#int tpdkg_start_peer(TP_DKG_PeerState *ctx, const uint64_t ts_epsilon,
# const uint8_t peer_lt_sk[crypto_sign_SECRETKEYBYTES],
# const TP_DKG_Message *msg0);
#
# also wraps conveniently
#
#void tpdkg_peer_set_bufs(TP_DKG_PeerState *ctx,
# uint8_t (*peers_sig_pks)[][crypto_sign_PUBLICKEYBYTES],
# uint8_t (*peers_noise_pks)[][crypto_scalarmult_BYTES],
# Noise_XK_session_t *(*noise_outs)[],
# Noise_XK_session_t *(*noise_ins)[],
# TOPRF_Share (*shares)[],
# TOPRF_Share (*xshares)[],
# uint8_t (*commitments)[][crypto_core_ristretto255_BYTES],
# uint16_t (*complaints)[],
# uint8_t (*my_complaints)[]);
def tpdkg_peer_start(ts_epsilon, peer_lt_sk, msg0):
b = ctypes.create_string_buffer(liboprf.tpdkg_peerstate_size()+32)
b_addr = ctypes.addressof(b)
s_addr = b_addr + (b_addr % 32)
state = ctypes.c_void_p(s_addr)
if state.value % 32 != 0:
raise ValueError("cannot align at 32bytes the TP_DKG_PeerState struct")
__check(liboprf.tpdkg_start_peer(state, ctypes.c_uint64(ts_epsilon), peer_lt_sk, msg0))
n = tpdkg_peerstate_n([state])
t = tpdkg_peerstate_t([state])
peers_sig_pks = ctypes.create_string_buffer(b"peer_sig_pks", n * pysodium.crypto_sign_PUBLICKEYBYTES)
peers_noise_pks = ctypes.create_string_buffer(b"peer_noise_pks", n * pysodium.crypto_scalarmult_BYTES)
noise_outs = (ctypes.c_void_p * n)()
noise_ins = (ctypes.c_void_p * n)()
shares = ctypes.create_string_buffer(n * TOPRF_Share_BYTES)
xshares = ctypes.create_string_buffer(n * TOPRF_Share_BYTES)
commitments = ctypes.create_string_buffer(n * t * pysodium.crypto_core_ristretto255_BYTES)
complaints = ctypes.create_string_buffer(n * n * 2)
my_complaints = ctypes.create_string_buffer(n)
last_ts = (ctypes.c_uint64 * n)()
liboprf.tpdkg_peer_set_bufs(state,
ctypes.byref(peers_sig_pks),
ctypes.byref(peers_noise_pks),
noise_outs,
noise_ins,
ctypes.byref(shares),
ctypes.byref(xshares),
ctypes.byref(commitments),
ctypes.byref(complaints),
ctypes.byref(my_complaints),
ctypes.byref(last_ts))
# we need to keep these arrays around, otherwise the gc eats them up.
ctx = (state, peers_sig_pks, peers_noise_pks, noise_outs, noise_ins, shares, xshares, commitments, complaints, my_complaints, b, last_ts)
return ctx
#size_t tpdkg_peer_input_size(const TP_DKG_PeerState *ctx);
def tpdkg_peer_input_size(ctx):
return liboprf.tpdkg_peer_input_size(ctx[0])
#size_t tpdkg_peer_output_size(const TP_DKG_PeerState *ctx);
def tpdkg_peer_output_size(ctx):
return liboprf.tpdkg_peer_output_size(ctx[0])
#int tpdkg_peer_next(TP_DKG_PeerState *ctx, const uint8_t *input, const size_t input_len, uint8_t *output, const size_t output_len);
def tpdkg_peer_next(ctx, msg):
input_len = tpdkg_peer_input_size(ctx)
if len(msg) != input_len: raise ValueError(f"input msg is invalid size: {len(msg)}B must be: {input_len}B")
output_len = tpdkg_peer_output_size(ctx)
output = ctypes.create_string_buffer(output_len)
__check(liboprf.tpdkg_peer_next(ctx[0], msg, ctypes.c_size_t(input_len), output, ctypes.c_size_t(output_len)))
return output.raw
#int tpdkg_peer_not_done(const TP_DKG_PeerState *peer);
def tpdkg_peer_not_done(ctx):
return liboprf.tpdkg_peer_not_done(ctx[0]) == 1
#void tpdkg_peer_free(TP_DKG_PeerState *ctx);
def tpdkg_peer_free(ctx):
liboprf.tpdkg_peer_free(ctx[0])
#int dkg_vss_reconstruct(const uint8_t t,
# const uint8_t x,
# const size_t shares_len,
# const TOPRF_Share shares[shares_len][2],
# const uint8_t commitments[shares_len][crypto_scalarmult_ristretto255_BYTES]
# uint8_t result[crypto_scalarmult_ristretto255_SCALARBYTES],
# uint8_t blind[crypto_scalarmult_ristretto255_SCALARBYTES]) {
def dkg_vss_reconstruct(n, t, x, shares, commitments = None):
if len(shares) < t:
raise ValueError(f"shares must be at least {TOPRF_Share_BYTES*2*n} bytes is instead {len(shares)}")
for i, s in enumerate(shares):
if len(s)!=TOPRF_Share_BYTES*2:
raise ValueError(f"share {i+1} has incorrect length: {len(s)}, must be {TOPRF_Share_BYTES*2}")
if commitments is not None:
if len(commitments) < t:
raise ValueError(f"commitments must be at least {pysodium.crypto_core_ristretto255_BYTES*t} bytes is instead {len(commitments)}")
for i, c in enumerate(commitments):
if len(c)!=pysodium.crypto_core_ristretto255_BYTES:
raise ValueError(f"commitment {i+1} has incorrect length: {len(c)}, must be {pysodium.crypto_core_ristretto255_BYTES}")
commitments = b''.join(commitments)
shares_len = ctypes.c_size_t(len(shares))
shares = b''.join(shares)
result = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_SCALARBYTES)
blind = ctypes.create_string_buffer(pysodium.crypto_core_ristretto255_SCALARBYTES)
__check(liboprf.dkg_vss_reconstruct(ctypes.c_uint8(t),
ctypes.c_uint8(x),
shares_len,
shares,
commitments,
result, blind))
return result.raw, blind.raw
sessionid_SIZE=32
tupdate_msg0_SIZE = 0xd1 # ( sizeof(TP_DKG_Message) \
# + crypto_generichash_BYTES/*dst*/ \
# + 2 /*n,t*/ \
# + crypto_sign_PUBLICKEYBYTES /* tp_sign_pk */)
tupdate_max_err_SIZE = 128
tupdate_keyid_SIZE = 32
tupdate_commitment_HASHBYTES = 32
noise_xk_handshake3_SIZE = 64
class Cheater(ctypes.Structure):
_fields_ = [('step', ctypes.c_int),
('error', ctypes.c_int),
('peer', ctypes.c_uint8),
('other_peer', ctypes.c_uint8),
('invalid_index', ctypes.c_int),
]
# int toprf_update_start_stp(TOPRF_Update_STPState *ctx, const uint64_t ts_epsilon,
# const uint8_t n, const uint8_t t,
# const char *proto_name, const size_t proto_name_len,
# const uint8_t keyid[toprf_keyid_SIZE],
# const uint8_t (*sig_pks)[][crypto_sign_PUBLICKEYBYTES],
# const uint8_t ltssk[crypto_sign_SECRETKEYBYTES],
# const size_t msg0_len,
# TOPRF_Update_Message *msg0);
#
# also wraps conveniently:
#
# void toprf_update_stp_set_bufs(TOPRF_Update_STPState *ctx,
# uint16_t p_complaints[],
# uint16_t y2_complaints[],
# TOPRF_Update_Cheater (*cheaters)[], const size_t cheater_max,
# uint8_t (*p_commitments_hashes)[][toprf_update_commitment_HASHBYTES],
# uint8_t (*p_share_macs)[][crypto_auth_hmacsha256_BYTES],
# uint8_t (*p_commitments)[][crypto_core_ristretto255_BYTES],
# uint8_t (*kc0_commitments)[][crypto_core_ristretto255_BYTES],
# uint8_t (*k0p_commitments)[][crypto_core_ristretto255_BYTES],
# uint8_t (*zk_challenge_commitments)[][3][crypto_scalarmult_ristretto255_SCALARBYTES],
# uint8_t (*zk_challenge_e_i)[][crypto_scalarmult_ristretto255_SCALARBYTES],
# uint8_t (*k0p_final_commitments)[][crypto_scalarmult_ristretto255_BYTES],
# uint64_t *last_ts);
def tupdate_start_stp(n, t, ts_epsilon, proto_name, sig_pks, keyid, ltssk):
dealers = (t-1)*2 + 1
if(len(keyid)!=tupdate_keyid_SIZE): raise ValueError(f"keyid has incorrect size, must be {tupdate_keyid_SIZE}")
if(len(sig_pks)!=n+1): raise ValueError(f"invalid number of long-term signature pubkeys ({len(sig_pks)}, must be equal n ({n+1})")
for i, k in enumerate(sig_pks):
if len(k) != pysodium.crypto_sign_PUBLICKEYBYTES:
raise ValueError(f"long-term signature pubkey #{i} has invalid length ({len(k)}) must be {pysodium.crypto_sign_PUBLICKEYBYTES}")
if len(ltssk) != pysodium.crypto_sign_SECRETKEYBYTES:
raise ValueError(f"long-term signature secret key of STP has invalid length ({len(ltssk)}) must be {pysodium.crypto_sign_SECRETKEYBYTES}")
b = ctypes.create_string_buffer(liboprf.toprf_update_stpstate_size()+32)
b_addr = ctypes.addressof(b)
s_addr = b_addr + (b_addr % 32)
state = ctypes.c_void_p(s_addr)
if state.value % 32 != 0:
raise ValueError("cannot align at 32bytes the TOPRF_Update_STPState struct")
sig_pks = ctypes.create_string_buffer(b''.join(sig_pks))
msg = ctypes.create_string_buffer(tupdate_msg0_SIZE)
__check(liboprf.toprf_update_start_stp(state, ctypes.c_uint64(ts_epsilon),
ctypes.c_uint8(n), ctypes.c_uint8(t),
proto_name, ctypes.c_size_t(len(proto_name)),
keyid, ctypes.byref(sig_pks), ltssk,
ctypes.c_size_t(len(msg.raw)), msg))
k0_commitments = ctypes.create_string_buffer(n*pysodium.crypto_core_ristretto255_BYTES)
p_complaints = (ctypes.c_uint16 * n*n)()
y2_complaints = (ctypes.c_uint16 * n*n)()
cheaters = (Cheater * (t*t - 1))()
p_commitments_hashes = ctypes.create_string_buffer(n*tupdate_commitment_HASHBYTES)
p_share_macs = ctypes.create_string_buffer(n*n*pysodium.crypto_auth_hmacsha256_BYTES)
p_commitments = ctypes.create_string_buffer(n*n*pysodium.crypto_core_ristretto255_BYTES)
k0p_commitments = ctypes.create_string_buffer(dealers*(n+1)*pysodium.crypto_core_ristretto255_BYTES)
zk_challenge_commitments = ctypes.create_string_buffer(dealers*2*3*pysodium.crypto_core_ristretto255_SCALARBYTES)
zk_challenge_e_i = ctypes.create_string_buffer(2*dealers*pysodium.crypto_core_ristretto255_SCALARBYTES)
k0p_final_commitments = ctypes.create_string_buffer(n*pysodium.crypto_core_ristretto255_BYTES)
last_ts = (ctypes.c_uint64 * n)()
liboprf.toprf_update_stp_set_bufs(state
# uint16_t p_complaints[],
,p_complaints
# uint16_t x2_complaints[], uint16_t y2_complaints[],
,y2_complaints
# TOPRF_Update_Cheater (*cheaters)[], const size_t cheater_max,
,ctypes.byref(cheaters), ctypes.c_size_t(len(cheaters))
# uint8_t (*p_commitments_hashes)[][toprf_update_commitment_HASHBYTES],
,ctypes.byref(p_commitments_hashes)
# uint8_t (*p_share_macs)[][crypto_auth_hmacsha256_BYTES],
,ctypes.byref(p_share_macs)
# uint8_t (*p_commitments)[][crypto_core_ristretto255_BYTES],
,ctypes.byref(p_commitments)
# uint8_t (*kc0_commitments)[][crypto_core_ristretto255_BYTES],
,ctypes.byref(k0_commitments)
# uint8_t (*k0p_commitments)[][crypto_core_ristretto255_BYTES],
,ctypes.byref(k0p_commitments)
# uint8_t (*zk_challenge_commitments)[][3][crypto_scalarmult_ristretto255_SCALARBYTES],
,ctypes.byref(zk_challenge_commitments)
# uint8_t (*zk_challenge_e_i)[][crypto_scalarmult_ristretto255_SCALARBYTES],
,ctypes.byref(zk_challenge_e_i)
# uint8_t (*k0p_final_commitments)[][crypto_scalarmult_ristretto255_BYTES],
,ctypes.byref(k0p_final_commitments)
# uint64_t *last_ts);
,ctypes.byref(last_ts))
# we need to keep these arrays around, otherwise the gc eats them up.
ctx = (state, cheaters, p_complaints, y2_complaints,
p_commitments_hashes, p_share_macs,
p_commitments,
k0_commitments,
k0p_commitments,
zk_challenge_commitments, zk_challenge_e_i,
k0p_final_commitments,
last_ts, sig_pks, b)
return ctx, msg.raw
#size_t tpdkg_tp_input_size(const TP_DKG_TPState *ctx);
def tupdate_stp_input_size(ctx):
return liboprf.toprf_update_stp_input_size(ctx[0])
#int tpdkg_tp_input_sizes(const TP_DKG_TPState *ctx, size_t *sizes);
def tupdate_stp_input_sizes(ctx):
sizes = (ctypes.c_size_t * tpdkg_tpstate_n(ctx))()
ret = liboprf.toprf_update_stp_input_sizes(ctx[0], ctypes.byref(sizes))
return ret, [x for x in sizes]
#size_t tpdkg_tp_output_size(const TP_DKG_TPState *ctx);
def tupdate_stp_output_size(ctx):
return liboprf.toprf_update_stp_output_size(ctx[0])
#int tpdkg_tp_next(TP_DKG_TPState *ctx, const uint8_t *input, const size_t input_len, uint8_t *output, const size_t output_len);
def tupdate_stp_next(ctx, msg):
input_len = tupdate_stp_input_size(ctx)
if len(msg) != input_len: raise ValueError(f"input msg is invalid size: {len(msg)}B must be: {input_len}B")
output_len = tupdate_stp_output_size(ctx)
output = ctypes.create_string_buffer(output_len)
__check(liboprf.toprf_update_stp_next(ctx[0], msg, ctypes.c_size_t(input_len), output, ctypes.c_size_t(output_len)))
return output
#int tpdkg_tp_peer_msg(const TP_DKG_TPState *ctx, const uint8_t *base, const size_t base_size, const uint8_t peer, const uint8_t **msg, size_t *len);
def tupdate_stp_peer_msg(ctx, base, peer):
msg = ctypes.POINTER(ctypes.c_char)()
size = ctypes.c_size_t()
__check(liboprf.toprf_update_stp_peer_msg(ctx[0], base, len(base.raw), peer, ctypes.byref(msg), ctypes.byref(size)))
msg = b''.join([msg[i] for i in range(size.value)])
return msg
#int tpdkg_tp_not_done(const TP_DKG_TPState *tp);
def tupdate_stp_not_done(ctx):
return liboprf.toprf_update_stp_not_done(ctx[0]) == 1
#todo
#def tupdate_get_cheaters(ctx):
# cheats = []
# cheaters = set()
# for i in range(tupdate_stpstate_cheater_len(ctx)):
# err = ctypes.create_string_buffer(tpdkg_max_err_SIZE)
# p = liboprf.toprf_update_cheater_msg(ctypes.byref(ctx[1][i]), err, tpdkg_max_err_SIZE)
# if 0 >= p > tpdkg_tpstate_n(ctx):
# print(f"invalid cheater index: {p}, skipping this entry")
# continue
# cheaters.add(p)
# cheats.append((p, err.raw[:err.raw.find(b'\x00')].decode('utf8')))
# return cheaters, cheats
liboprf.toprf_update_peerstate_n.restype = ctypes.c_uint8
def tupdate_peerstate_n(ctx):
return liboprf.toprf_update_peerstate_n(ctx[0])
liboprf.toprf_update_peerstate_t.restype = ctypes.c_uint8
def tupdate_peerstate_t(ctx):
return liboprf.toprf_update_peerstate_t(ctx[0])
liboprf.toprf_update_peerstate_sessionid.restype = ctypes.POINTER(ctypes.c_uint8)
def tupdate_peerstate_sessionid(ctx):
ptr = liboprf.toprf_update_peerstate_sessionid(ctx[0])
return bytes(ptr[i] for i in range(sessionid_SIZE))
liboprf.toprf_update_peerstate_share.restype = ctypes.POINTER(ctypes.c_uint8)
def tupdate_peerstate_share(ctx):
ptr = liboprf.toprf_update_peerstate_share(ctx[0])
return bytes(ptr[i] for i in range(TOPRF_Share_BYTES*2))
liboprf.toprf_update_peerstate_commitment.restype = ctypes.POINTER(ctypes.c_uint8)
def tupdate_peerstate_commitment(ctx):
ptr = liboprf.toprf_update_peerstate_commitment(ctx[0])
return bytes(ptr[i] for i in range(pysodium.crypto_core_ristretto255_BYTES))
liboprf.toprf_update_peerstate_commitments.restype = ctypes.POINTER(ctypes.c_uint8)
def tupdate_peerstate_commitments(ctx):
ptr = liboprf.toprf_update_peerstate_commitments(ctx[0])
return tuple(bytes(ptr[p*pysodium.crypto_core_ristretto255_BYTES:(p+1)*pysodium.crypto_core_ristretto255_BYTES]) for p in range(tupdate_peerstate_n(ctx)))
def tupdate_peerstate_step(ctx):
return liboprf.toprf_update_peerstate_step(ctx[0])
liboprf.toprf_update_stpstate_n.restype = ctypes.c_uint8
def tupdate_stpstate_n(ctx):
return liboprf.toprf_update_stpstate_n(ctx[0])
liboprf.toprf_update_stpstate_t.restype = ctypes.c_uint8
def tupdate_stpstate_t(ctx):
return liboprf.toprf_update_stpstate_t(ctx[0])
liboprf.toprf_update_stpstate_cheater_len.restype = ctypes.c_size_t
def tupdate_stpstate_cheater_len(ctx):
return liboprf.toprf_update_stpstate_cheater_len(ctx[0])
liboprf.toprf_update_stpstate_sessionid.restype = ctypes.POINTER(ctypes.c_uint8)
def tupdate_stpstate_sessionid(ctx):
ptr = liboprf.toprf_update_stpstate_sessionid(ctx[0])
return bytes(ptr[i] for i in range(sessionid_SIZE))
liboprf.toprf_update_stpstate_delta.restype = ctypes.POINTER(ctypes.c_uint8)
def tupdate_stpstate_delta(ctx):
ptr = liboprf.toprf_update_stpstate_delta(ctx[0])
return bytes(ptr[i] for i in range(pysodium.crypto_core_ristretto255_BYTES))
liboprf.toprf_update_stpstate_commitments.restype = ctypes.POINTER(ctypes.c_uint8)
def tupdate_stpstate_commitments(ctx):
ptr = liboprf.toprf_update_stpstate_commitments(ctx[0])
return tuple(bytes(ptr[p*pysodium.crypto_core_ristretto255_BYTES:(p+1)*pysodium.crypto_core_ristretto255_BYTES]) for p in range(tupdate_stpstate_n(ctx)))
def tupdate_stpstate_step(ctx):
return liboprf.toprf_update_stpstate_step(ctx[0])
# TOPRF_Update_Err toprf_update_start_peer(TOPRF_Update_PeerState *ctx,
# const uint64_t ts_epsilon,
# const uint8_t lt_sk[crypto_sign_SECRETKEYBYTES],
# const TOPRF_Update_Message *msg0,
# uint8_t keyid[toprf_keyid_SIZE],
# uint8_t stp_ltpk[crypto_sign_PUBLICKEYBYTES]);
def tupdate_peer_start(ts_epsilon, peer_lt_sk, noise_sk, msg0):
if len(peer_lt_sk) != pysodium.crypto_sign_SECRETKEYBYTES:
raise ValueError(f"peer long-term secret key has invalid size, must be {pysodium.crypto_sign_SECRETKEYBYTES}")
if len(noise_sk) != pysodium.crypto_scalarmult_SCALARBYTES:
raise ValueError(f"peer long-term secret noise key has invalid size, must be {pysodium.crypto_scalarmult_SCALARBYTES}")
b = ctypes.create_string_buffer(liboprf.toprf_update_peerstate_size()+32)
b_addr = ctypes.addressof(b)
s_addr = b_addr + (b_addr % 32)
state = ctypes.c_void_p(s_addr)
if state.value % 32 != 0:
raise ValueError("cannot align at 32bytes the TP_DKG_PeerState struct")
keyid = ctypes.create_string_buffer(tupdate_keyid_SIZE)
stp_ltpk = ctypes.create_string_buffer(pysodium.crypto_sign_PUBLICKEYBYTES)
__check(liboprf.toprf_update_start_peer(state, ctypes.c_uint64(ts_epsilon), peer_lt_sk,
noise_sk,
msg0, keyid, stp_ltpk))
return (state, b), keyid.raw, stp_ltpk.raw
def tupdate_peer_set_bufs(ctx, n, t, index, sig_pks, noise_pks, k0 = None, k0_commitments = None):
dealers = (t-1)*2 + 1
if k0 is not None:
if len(k0) != TOPRF_Share_BYTES * 2:
raise ValueError(f"k0 has invalid size {len(k0)} must be {TOPRF_Share_BYTES * 2}")
if(k0[0]!=index or k0[TOPRF_Share_BYTES]!=index):
raise ValueError(f"k0 has a different index ({k0[0]} & {k0[TOPRF_Share_BYTES]} than provided: {index}")
if k0_commitments is None:
raise ValueError(f"must provide also commitments for k0")
if len(k0_commitments) < dealers:
raise ValueError(f"not enough dealers holding kc0 shares, need at least {dealers}")
for i, c in enumerate(k0_commitments):
if len(c) == pysodium.crypto_core_ristretto255_BYTES: continue
raise ValueError(f"k0 commitment #{i} has invalid length ({len(c)}) must be {pysodium.crypto_core_ristretto255_BYTES}")
k0_commitments = ctypes.create_string_buffer(b''.join(k0_commitments))
if(len(sig_pks)!=n+1): raise ValueError(f"invalid number of long-term signature pubkeys ({len(sig_pks)}, must be equal n ({n+1})")
for i, k in enumerate(sig_pks):
if len(k) != pysodium.crypto_sign_PUBLICKEYBYTES:
raise ValueError(f"long-term signature pubkey #{i} has invalid length ({len(k)}) must be {pysodium.crypto_sign_PUBLICKEYBYTES}")
sig_pks = ctypes.create_string_buffer(b''.join(sig_pks))
if(len(noise_pks)!=n): raise ValueError(f"invalid number of long-term noise pubkeys ({len(noise_pks)}, must be equal n ({n})")
for i, k in enumerate(noise_pks):
if len(k) != pysodium.crypto_scalarmult_BYTES:
raise ValueError(f"noise pubkey #{i} has invalid length ({len(k)}) must be {pysodium.crypto_scalarmult_BYTES}")
noise_pks = ctypes.create_string_buffer(b''.join(noise_pks))
noise_outs = (ctypes.c_void_p * n)()
noise_ins = (ctypes.c_void_p * n)()
p_shares = ctypes.create_string_buffer(n * TOPRF_Share_BYTES * 2)
p_commitments = ctypes.create_string_buffer(n * n * pysodium.crypto_core_ristretto255_BYTES)
p_commitment_hashes = ctypes.create_string_buffer(n * tupdate_commitment_HASHBYTES)
p_share_macs = ctypes.create_string_buffer(n * n * pysodium.crypto_auth_hmacsha256_BYTES)
encrypted_shares = ctypes.create_string_buffer(n * (noise_xk_handshake3_SIZE + TOPRF_Share_BYTES * 2))
cheaters = (Cheater * (t*t - 1))()
lambdas = ctypes.create_string_buffer(dealers * pysodium.crypto_core_ristretto255_SCALARBYTES)
k0p_shares = ctypes.create_string_buffer(dealers * TOPRF_Share_BYTES * 2)
k0p_commitments = ctypes.create_string_buffer(dealers * (n+1) * pysodium.crypto_core_ristretto255_BYTES)
zk_challenge_nonce_commitments = ctypes.create_string_buffer(n * pysodium.crypto_core_ristretto255_BYTES)
zk_challenge_nonces = ctypes.create_string_buffer(n * 2 * pysodium.crypto_core_ristretto255_SCALARBYTES)
zk_challenge_commitments = ctypes.create_string_buffer(dealers * 3 * pysodium.crypto_core_ristretto255_SCALARBYTES)
zk_challenge_e_i = ctypes.create_string_buffer(dealers * pysodium.crypto_core_ristretto255_SCALARBYTES)
p_complaints = (ctypes.c_uint16 * n*n)()
p_my_complaints = ctypes.create_string_buffer(n)
last_ts = (ctypes.c_uint64 * n)()
# int toprf_update_peer_set_bufs(TOPRF_Update_PeerState *ctx,
liboprf.toprf_update_peer_set_bufs(ctx[0]
# const uint8_t self,
,ctypes.c_uint8(index)
# const uint8_t n, const uint8_t t,
,ctypes.c_uint8(n), ctypes.c_uint8(t)
# const TOPRF_Share k0[2],
,k0
# uint8_t (*kc0_commitments)[][crypto_core_ristretto255_BYTES],
,ctypes.byref(k0_commitments)
# const uint8_t (*sig_pks)[][],
,ctypes.byref(sig_pks)
# uint8_t (*peers_noise_pks)[][crypto_scalarmult_BYTES],
,ctypes.byref(noise_pks)
# Noise_XK_session_t *(*noise_outs)[],
,noise_outs
# Noise_XK_session_t *(*noise_ins)[],
,noise_ins
# TOPRF_Share (*p_shares)[][2],
,ctypes.byref(p_shares)
# uint8_t (*p_commitments)[][crypto_core_ristretto255_BYTES],
,ctypes.byref(p_commitments)
# uint8_t (*p_commitments_hashes)[][toprf_update_commitment_HASHBYTES],
,ctypes.byref(p_commitment_hashes)
# uint8_t (*p_share_macs)[][crypto_auth_hmacsha256_BYTES],
,ctypes.byref(p_share_macs)
# uint8_t (*encrypted_shares)[][noise_xk_handshake3_SIZE + toprf_update_encrypted_shares_SIZE*2],
,ctypes.byref(encrypted_shares)
# TOPRF_Update_Cheater (*cheaters)[], const size_t cheater_max,
,ctypes.byref(cheaters), ctypes.c_size_t(len(cheaters))
# uint8_t (*lambdas)[][crypto_core_ristretto255_SCALARBYTES],
,ctypes.byref(lambdas)
# TOPRF_Share (*k0p_shares)[][2],
,ctypes.byref(k0p_shares)
# uint8_t (*k0p_commitments)[][crypto_core_ristretto255_BYTES],
,ctypes.byref(k0p_commitments)
# uint8_t (*zk_challenge_nonce_commitments)[][crypto_scalarmult_ristretto255_BYTES],
,ctypes.byref(zk_challenge_nonce_commitments)
# uint8_t (*zk_challenge_nonces)[][2][crypto_scalarmult_ristretto255_SCALARBYTES],
,ctypes.byref(zk_challenge_nonces)
# uint8_t (*zk_challenge_commitments)[][3][crypto_scalarmult_ristretto255_SCALARBYTES],
,ctypes.byref(zk_challenge_commitments)
# uint8_t (*zk_challenge_e_i)[][crypto_scalarmult_ristretto255_SCALARBYTES],
,ctypes.byref(zk_challenge_e_i)
# uint16_t *p_complaints,
,p_complaints
#uint8_t *my_p_complaints,
,p_my_complaints
# uint64_t *last_ts);
,ctypes.byref(last_ts))
# we need to keep these arrays around, otherwise the gc eats them up.
ctx = (ctx[0], noise_pks, noise_outs, noise_ins,
k0_commitments, sig_pks,
p_shares,
p_commitments,
p_commitment_hashes,
p_share_macs,
encrypted_shares,
cheaters,
lambdas,
k0p_shares, k0p_commitments,
zk_challenge_nonce_commitments, zk_challenge_nonces, zk_challenge_commitments, zk_challenge_e_i,
p_complaints, p_my_complaints,
last_ts, ctx[1])
return ctx
#size_t toprf_update_peer_input_size(const TOPRF_Update_PeerState *ctx);
def tupdate_peer_input_size(ctx):
return liboprf.toprf_update_peer_input_size(ctx[0])
#size_t toprf_update_peer_output_size(const TOPRF_Update_PeerState *ctx);
def tupdate_peer_output_size(ctx):
return liboprf.toprf_update_peer_output_size(ctx[0])
#int toprf_update_peer_next(TOPRF_Update_PeerState *ctx, const uint8_t *input, const size_t input_len, uint8_t *output, const size_t output_len);
def tupdate_peer_next(ctx, msg):
input_len = tupdate_peer_input_size(ctx)
if len(msg) != input_len: raise ValueError(f"input msg is invalid size: {len(msg)}B must be: {input_len}B")
output_len = tupdate_peer_output_size(ctx)
output = ctypes.create_string_buffer(output_len)
__check(liboprf.toprf_update_peer_next(ctx[0], msg, ctypes.c_size_t(input_len), output, ctypes.c_size_t(output_len)))
return output.raw
#int toprf_update_peer_not_done(const TOPRF_Update_PeerState *peer);
def tupdate_peer_not_done(ctx):
return liboprf.toprf_update_peer_not_done(ctx[0]) == 1
#void toprf_update_peer_free(TOPRF_Update_PeerState *ctx);
def tupdate_peer_free(ctx):
liboprf.toprf_update_peer_free(ctx[0])
stpdkg_msg0_SIZE = 0xb3
stp_dkg_commitment_HASHBYTES = 32
stp_dkg_max_err_SIZE = 128
stp_dkg_sessionid_SIZE = 32
stp_dkg_encrypted_share_SIZE = TOPRF_Share_BYTES * 2 + 16 #pysodium.crypto_secretbox_xchacha20poly1305_MACBYTES
def stp_dkg_start_stp(n, t, ts_epsilon, proto_name, sig_pks, ltssk):
b = ctypes.create_string_buffer(liboprf.stp_dkg_stpstate_size()+32)
b_addr = ctypes.addressof(b)
s_addr = b_addr + (b_addr % 32)
state = ctypes.c_void_p(s_addr)
if state.value % 32 != 0:
raise ValueError("cannot align at 32bytes the STP_DKG_STPState struct")
if(len(sig_pks)!=n+1): raise ValueError(f"invalid number of long-term signature pubkeys ({len(sig_pks)}, must be equal n ({n+1})")
for i, k in enumerate(sig_pks):
if len(k) != pysodium.crypto_sign_PUBLICKEYBYTES:
raise ValueError(f"long-term signature pubkey #{i} has invalid length ({len(k)}) must be {pysodium.crypto_sign_PUBLICKEYBYTES}")
if len(ltssk) != pysodium.crypto_sign_SECRETKEYBYTES:
raise ValueError(f"long-term signature secret key of STP has invalid length ({len(ltssk)}) must be {pysodium.crypto_sign_SECRETKEYBYTES}")
msg = ctypes.create_string_buffer(stpdkg_msg0_SIZE)
sig_pks = ctypes.create_string_buffer(b''.join(sig_pks))
__check(liboprf.stp_dkg_start_stp(state,
ctypes.c_uint64(ts_epsilon),
ctypes.c_uint8(n), ctypes.c_uint8(t),
proto_name, ctypes.c_size_t(len(proto_name)),
ctypes.byref(sig_pks),
ltssk,
ctypes.c_size_t(len(msg.raw)), msg))
commitment_hashes = ctypes.create_string_buffer(n*stp_dkg_commitment_HASHBYTES)
share_macs = ctypes.create_string_buffer(n * n * pysodium.crypto_auth_hmacsha256_BYTES)
commitments = ctypes.create_string_buffer(n*n*pysodium.crypto_core_ristretto255_BYTES)
share_complaints = (ctypes.c_uint16 * n*n)()
cheaters = (TP_DKG_Cheater * (t*t - 1))()
last_ts = (ctypes.c_uint64 * n)()
liboprf.stp_dkg_stp_set_bufs(state,
ctypes.byref(commitment_hashes),
ctypes.byref(share_macs),
ctypes.byref(commitments),
ctypes.byref(share_complaints),
ctypes.byref(cheaters),
ctypes.c_size_t(len(cheaters)),
ctypes.byref(last_ts))
# we need to keep these arrays around, otherwise the gc eats them up.
ctx = (state, cheaters, sig_pks, commitments, commitment_hashes, share_macs, share_complaints, last_ts, b)
return ctx, msg.raw
#size_t stp_dkg_stp_input_size(const STP_DKG_STPState *ctx);
def stp_dkg_stp_input_size(ctx):
return liboprf.stp_dkg_stp_input_size(ctx[0])
#int stp_dkg_stp_input_sizes(const STP_DKG_STPState *ctx, size_t *sizes);
def stp_dkg_stp_input_sizes(ctx):
sizes = (ctypes.c_size_t * stp_dkg_stpstate_n(ctx))()
ret = liboprf.stp_dkg_stp_input_sizes(ctx[0], ctypes.byref(sizes))
return ret, [x for x in sizes]
#size_t stp_dkg_stp_output_size(const STP_DKG_STPState *ctx);
def stp_dkg_stp_output_size(ctx):
return liboprf.stp_dkg_stp_output_size(ctx[0])
#int stp_dkg_stp_next(TP_DKG_STPState *ctx, const uint8_t *input, const size_t input_len, uint8_t *output, const size_t output_len);
def stp_dkg_stp_next(ctx, msg):
input_len = stp_dkg_stp_input_size(ctx)
if len(msg) != input_len: raise ValueError(f"input msg is invalid size: {len(msg)}B must be: {input_len}B")
output_len = stp_dkg_stp_output_size(ctx)
output = ctypes.create_string_buffer(output_len)
__check(liboprf.stp_dkg_stp_next(ctx[0], msg, ctypes.c_size_t(input_len), output, ctypes.c_size_t(output_len)))
return output
#int stp_dkg_stp_peer_msg(const STP_DKG_STPState *ctx, const uint8_t *base, const size_t base_size, const uint8_t peer, const uint8_t **msg, size_t *len);
def stp_dkg_stp_peer_msg(ctx, base, peer):
msg = ctypes.POINTER(ctypes.c_char)()
size = ctypes.c_size_t()
__check(liboprf.stp_dkg_stp_peer_msg(ctx[0], base, ctypes.c_size_t(len(base.raw)), peer, ctypes.byref(msg), ctypes.byref(size)))
return msg[:size.value]
#int stp_dkg_stp_not_done(const STP_DKG_STPState *tp);
def stp_dkg_stp_not_done(ctx):
return liboprf.stp_dkg_stp_not_done(ctx[0]) == 1
def stp_dkg_get_cheaters(ctx):
cheats = []
cheaters = set()
for i in range(stp_dkg_stpstate_cheater_len(ctx)):
err = ctypes.create_string_buffer(stp_dkg_max_err_SIZE)
p = liboprf.stp_dkg_stp_cheater_msg(ctypes.byref(ctx[1][i]), err, stp_dkg_max_err_SIZE)
if 0 >= p > stp_dkg_stpstate_n(ctx):
print(f"invalid cheater index: {p}, skipping this entry")
continue
cheaters.add(p)
cheats.append((p, err.raw[:err.raw.find(b'\x00')].decode('utf8')))
return cheaters, cheats
liboprf.stp_dkg_peerstate_n.restype = ctypes.c_uint8
def stp_dkg_peerstate_n(ctx):
return liboprf.stp_dkg_peerstate_n(ctx[0])
liboprf.stp_dkg_peerstate_t.restype = ctypes.c_uint8
def stp_dkg_peerstate_t(ctx):
return liboprf.stp_dkg_peerstate_t(ctx[0])
liboprf.stp_dkg_peerstate_sessionid.restype = ctypes.POINTER(ctypes.c_uint8)
def stp_dkg_peerstate_sessionid(ctx):
ptr = liboprf.stp_dkg_peerstate_sessionid(ctx[0])
return bytes(ptr[i] for i in range(stp_dkg_sessionid_SIZE))
liboprf.stp_dkg_peerstate_lt_sk.restype = ctypes.POINTER(ctypes.c_uint8)
def stp_dkg_peerstate_lt_sk(ctx):
ptr = liboprf.stp_dkg_peerstate_lt_sk(ctx[0])
return bytes(ptr[i] for i in range(pysodium.crypto_sign_SECRETKEYBYTES))
liboprf.stp_dkg_peerstate_share.restype = ctypes.POINTER(ctypes.c_uint8)
def stp_dkg_peerstate_share(ctx):
ptr = liboprf.stp_dkg_peerstate_share(ctx[0])
return bytes(ptr[i] for i in range(TOPRF_Share_BYTES*2))
liboprf.stp_dkg_peerstate_commitments.restype = ctypes.POINTER(ctypes.c_uint8)
def stp_dkg_peerstate_commitments(ctx):
ptr = liboprf.stp_dkg_peerstate_commitments(ctx[0])
return tuple(bytes(ptr[c*pysodium.crypto_core_ristretto255_BYTES+i]
for i in range(pysodium.crypto_core_ristretto255_BYTES))
for c in range(stp_dkg_peerstate_n(ctx)))
def stp_dkg_peerstate_step(ctx):
return liboprf.stp_dkg_peerstate_step(ctx[0])
liboprf.stp_dkg_stpstate_n.restype = ctypes.c_uint8
def stp_dkg_stpstate_n(ctx):
return liboprf.stp_dkg_stpstate_n(ctx[0])
liboprf.stp_dkg_stpstate_t.restype = ctypes.c_uint8
def stp_dkg_stpstate_t(ctx):
return liboprf.stp_dkg_stpstate_t(ctx[0])
liboprf.stp_dkg_stpstate_cheater_len.restype = ctypes.c_size_t
def stp_dkg_stpstate_cheater_len(ctx):
return liboprf.stp_dkg_stpstate_cheater_len(ctx[0])
liboprf.stp_dkg_stpstate_sessionid.restype = ctypes.POINTER(ctypes.c_uint8)
def stp_dkg_stpstate_sessionid(ctx):
ptr = liboprf.stp_dkg_stpstate_sessionid(ctx[0])
return bytes(ptr[i] for i in range(stp_dkg_sessionid_SIZE))
liboprf.stp_dkg_stpstate_commitments.restype = ctypes.POINTER(ctypes.c_uint8)
def stp_dkg_stpstate_commitments(ctx):
ptr = liboprf.stp_dkg_stpstate_commitments(ctx[0])
return tuple(bytes(ptr[c*pysodium.crypto_core_ristretto255_BYTES+i]
for i in range(pysodium.crypto_core_ristretto255_BYTES))
for c in range(stp_dkg_stpstate_n(ctx)))
def stp_dkg_stpstate_step(ctx):
return liboprf.stp_dkg_stpstate_step(ctx[0])
#typedef int (*Keyloader_CB)(const uint8_t id[crypto_generichash_BYTES],
# void *arg,
# uint8_t sigpk[crypto_sign_PUBLICKEYBYTES],
# uint8_t noise_pk[crypto_scalarmult_BYTES]);
#@ctypes.CFUNCTYPE(c.c_int, c.POINTER(c.c_ubyte), c.POINTER(c.c_ubyte), c.POINTER(c.c_ubyte))
#def load_key(keyid, alpha, beta):
# c.memmove(beta, beta_, len(beta_))
# STP_DKG_Err stp_dkg_start_peer(STP_DKG_PeerState *ctx,
# const uint64_t ts_epsilon,
# const uint8_t lt_sk[crypto_sign_SECRETKEYBYTES],
# const STP_DKG_Message *msg0,
# uint8_t stp_ltpk[crypto_sign_PUBLICKEYBYTES]);
# also conveniently wraps
# int stp_dkg_peer_set_bufs(STP_DKG_PeerState *ctx,
# uint8_t (*peerids)[][crypto_generichash_BYTES],
# Keyloader_CB keyloader_cb,
# void *keyloader_cb_arg,
# uint8_t (*peers_sig_pks)[][crypto_sign_PUBLICKEYBYTES],
# uint8_t (*peers_noise_pks)[][crypto_scalarmult_BYTES],
# Noise_XK_session_t *(*noise_outs)[],
# Noise_XK_session_t *(*noise_ins)[],
# TOPRF_Share (*k_shares)[][2],
# uint8_t (*encrypted_shares)[][noise_xk_handshake3_SIZE + stp_dkg_encrypted_share_SIZE],
# uint8_t (*share_macs)[][crypto_auth_hmacsha256_BYTES],
# uint8_t (*ki_commitments)[][crypto_core_ristretto255_BYTES],
# uint8_t (*k_commitments)[][crypto_core_ristretto255_BYTES],
# uint8_t (*commitments_hashes)[][stp_dkg_commitment_HASHBYTES],
# STP_DKG_Cheater (*cheaters)[], const size_t cheater_max,
# uint16_t *share_complaints,
# uint8_t *my_share_complaints,
# uint64_t *last_ts);
def stp_dkg_peer_start(ts_epsilon, lt_sk, noise_sk, stp_ltpk, msg0, keyloader=None, keyloader_arg=None):
b = ctypes.create_string_buffer(liboprf.stp_dkg_peerstate_size()+32)
b_addr = ctypes.addressof(b)
s_addr = b_addr + (b_addr % 32)
state = ctypes.c_void_p(s_addr)
if state.value % 32 != 0:
raise ValueError("cannot align at 32bytes the STP_DKG_PeerState struct")
if len(lt_sk) != pysodium.crypto_sign_SECRETKEYBYTES:
raise ValueError(f"long-term signature secret key of peer has invalid length ({len(lt_sk)}) must be {pysodium.crypto_sign_SECRETKEYBYTES}")
if len(noise_sk) != pysodium.crypto_scalarmult_SCALARBYTES:
raise ValueError(f"long-term noise secret key of peer has invalid length ({len(noise_sk)}) must be {pysodium.crypto_scalarmult_SCALARBYTES}")
if len(stp_ltpk) != pysodium.crypto_sign_PUBLICKEYBYTES:
raise ValueError(f"long-term signature public key of STP has invalid length ({len(stp_ltpk)}) must be {pysodium.crypto_sign_PUBLICKEYBYTES}")
__check(liboprf.stp_dkg_start_peer(state, ctypes.c_uint64(ts_epsilon), lt_sk, noise_sk, msg0, stp_ltpk))
n = stp_dkg_peerstate_n([state])
t = stp_dkg_peerstate_t([state])
peer_ids = ctypes.create_string_buffer(n * pysodium.crypto_generichash_BYTES)
peers_sig_pks = ctypes.create_string_buffer((n+1) * pysodium.crypto_sign_PUBLICKEYBYTES)
peers_noise_pks = ctypes.create_string_buffer(n * pysodium.crypto_scalarmult_BYTES)
noise_outs = (ctypes.c_void_p * n)()
noise_ins = (ctypes.c_void_p * n)()
shares = ctypes.create_string_buffer(n * TOPRF_Share_BYTES*2)
encrypted_shares = ctypes.create_string_buffer(n * (noise_xk_handshake3_SIZE + stp_dkg_encrypted_share_SIZE))
share_macs = ctypes.create_string_buffer(n * n * pysodium.crypto_auth_hmacsha256_BYTES)
commitments = ctypes.create_string_buffer(n * n * pysodium.crypto_core_ristretto255_BYTES)
k_commitments = ctypes.create_string_buffer(n * pysodium.crypto_core_ristretto255_BYTES)
commitment_hashes = ctypes.create_string_buffer(n * tupdate_commitment_HASHBYTES)
cheaters = (TP_DKG_Cheater * (t*t - 1))()
complaints = (ctypes.c_uint16 * n*n)()
my_complaints = ctypes.create_string_buffer(n)
last_ts = (ctypes.c_uint64 * n)()
liboprf.stp_dkg_peer_set_bufs(state,
ctypes.byref(peer_ids),
#ctypes.byref(keyloader),
keyloader,
#ctypes.byref(keyloader_arg),
keyloader_arg,
ctypes.byref(peers_sig_pks),
ctypes.byref(peers_noise_pks),
noise_outs,
noise_ins,
ctypes.byref(shares),
ctypes.byref(encrypted_shares),
ctypes.byref(share_macs),
ctypes.byref(commitments),
ctypes.byref(k_commitments),
ctypes.byref(commitment_hashes),
ctypes.byref(cheaters), ctypes.c_size_t(len(cheaters)),
ctypes.byref(complaints),
ctypes.byref(my_complaints),
ctypes.byref(last_ts))
# we need to keep these arrays around, otherwise the gc eats them up.
ctx = (state, peer_ids, peers_sig_pks, peers_noise_pks, noise_outs, noise_ins, shares, encrypted_shares,
share_macs, commitments, k_commitments, commitment_hashes, cheaters, complaints, my_complaints, b, last_ts)
return ctx
#size_t stp_dkg_peer_input_size(const STP_DKG_PeerState *ctx);
def stp_dkg_peer_input_size(ctx):
return liboprf.stp_dkg_peer_input_size(ctx[0])
#size_t stp_dkg_peer_output_size(const STP_DKG_PeerState *ctx);
def stp_dkg_peer_output_size(ctx):
return liboprf.stp_dkg_peer_output_size(ctx[0])
#int stp_dkg_peer_next(STP_DKG_PeerState *ctx, const uint8_t *input, const size_t input_len, uint8_t *output, const size_t output_len);
def stp_dkg_peer_next(ctx, msg):
input_len = stp_dkg_peer_input_size(ctx)
if len(msg) != input_len: raise ValueError(f"input msg is invalid size: {len(msg)}B must be: {input_len}B")
output_len = stp_dkg_peer_output_size(ctx)
output = ctypes.create_string_buffer(output_len)
__check(liboprf.stp_dkg_peer_next(ctx[0], msg, ctypes.c_size_t(input_len), output, ctypes.c_size_t(output_len)))
return output.raw
#int stp_dkg_peer_not_done(const STP_DKG_PeerState *peer);
def stp_dkg_peer_not_done(ctx):
return liboprf.stp_dkg_peer_not_done(ctx[0]) == 1
#void stp_dkg_peer_free(STP_DKG_PeerState *ctx);
def stp_dkg_peer_free(ctx):
liboprf.stp_dkg_peer_free(ctx[0])
liboprf-0.9.4/python/pyoprf/multiplexer.py 0000775 0000000 0000000 00000040325 15146734002 0020756 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import ssl, socket, select, struct, asyncio, serial, sys, time
from pyoprf import noisexk
from itertools import zip_longest
from serial_asyncio import create_serial_connection
try:
from ble_serial.bluetooth.ble_client import BLE_client
except ImportError:
BLE_client = None
try:
import pyudev
except ImportError:
pyudev = None
def split_by_n(iterable, n):
return list(zip_longest(*[iter(iterable)]*n))
def get_event_loop():
try:
return asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
class Peer:
def __init__(self, name, addr, type = "SSL", ssl_cert=None, timeout=5, alpn_proto=None):
self.name = name
self.type = type # currently only TCP or SSL over TCP, but
# could be others like dedicated NOISE_XK,
# or hybrid mceliece+x25519 over USB or
# even UART
self.address = addr # Currently only TCP host:port as a tuple
self.ssl_cert = ssl_cert
self.timeout = timeout
self.alpn_proto = alpn_proto or ["oprf/1"]
self.state = "new"
self.fd = None
def connect(self):
if self.state == "connected":
raise ValueError(f"{self.name} is already connected")
if self.type not in {"SSL", "TCP"}:
raise ValueError(f"Unsupported peer type: {self.type}")
if self.type == "SSL":
ctx = ssl.create_default_context()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.set_alpn_protocols(self.alpn_proto)
if(self.ssl_cert):
ctx.load_verify_locations(self.ssl_cert) # only for dev, production system should use proper certs!
ctx.check_hostname=False # only for dev, production system should use proper certs!
ctx.verify_mode=ssl.CERT_NONE # only for dev, production system should use proper certs!
else:
ctx.load_default_certs()
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.check_hostname = True
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(self.timeout)
if self.type == "SSL":
self.fd = ctx.wrap_socket(s, server_hostname=self.address[0])
try: self.fd.connect(self.address)
except: return
self.state="connected"
def connected(self):
return self.state == "connected"
async def read_async(self,size):
if not self.connected():
return None
#raise ValueError(f"{self.name} cannot read, is not connected")
res = []
read = 0
while read 0 and self.rx_len >= self.rx_pected:
self.rx_available.set()
async def read_raw(self,size):
while(self.rx_available.is_set()):
await asyncio.sleep(0.001)
self.rx_pected = size
if(self.rx_len < self.rx_pected):
#print(f"{self.rx_len} < {self.rx_pected}", file=sys.stderr)
await self.rx_available.wait()
rsize = 0;
ret = []
while(rsizeH",len(ct))
get_event_loop().run_until_complete(self._send(header+ct))
def close(self):
if self.state == "closed": return
if not self.connected():
return
#raise ValueError(f"{self.name} cannot close, is not connected")
get_event_loop().run_until_complete(self._disconnect())
class Serial(asyncio.Protocol):
def __init__(self, *args, **kwargs):
self.rx_buffer = []
self.rx_len = 0
self.rx_pexted = 0
self.rx_available = asyncio.Event()
super().__init__(*args, **kwargs)
def connection_made(self, transport):
#print('port opened', transport, file=sys.stderr)
self.transport = transport
transport.serial.dtr = True
#transport.serial.rts = False
#transport.write(b'hello world\n')
def data_received(self, data):
#print('data received', len(data), data.hex(), file=sys.stderr)
#print('data received', len(data), file=sys.stderr)
self.rx_buffer.append(data)
self.rx_len += len(data)
if self.rx_pected > 0 and self.rx_len >= self.rx_pected:
self.rx_available.set()
def connection_lost(self, exc):
print('port closed', file=sys.stderr)
self.rx_available.set()
#get_event_loop().stop()
async def read_raw(self,size):
#print(f"read_raw({size})",file=sys.stderr)
#while(self.rx_available.is_set()): pass
self.rx_pected = size
while(self.rx_len < self.rx_pected
and not self.rx_available.is_set()):
await asyncio.sleep(0.001)
#if(self.rx_len < self.rx_pected):
#while(self.rx_available.is_set()): pass
#print(f"{self.rx_len} < {self.rx_pected}", file=sys.stderr)
# await self.rx_available.wait()
rsize = 0;
ret = []
while(rsizeH",len(ct))
self.transport.serial.write(header+ct)
def close(self):
if self.state == "closed": return
if not self.connected():
return
#raise ValueError(f"{self.name} cannot close, is not connected")
if self.transport.serial is not None:
self.transport.serial.dtr = False
self.transport.close()
self.state = "closed"
class Multiplexer:
def __init__(self, peers, alpn_proto=None):
if asyncio.get_event_loop_policy()._local._loop is None:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.peers = []
for name, p in peers.items():
if 'port' in p:
p = Peer(name
,(p['host'],p['port'])
,type=p.get("type", "SSL")
,ssl_cert = p.get('ssl_cert')
,timeout = p.get('timeout')
,alpn_proto=alpn_proto)
elif 'bleaddr' in p:
p = BLEPeer(name
,p['bleaddr']
,p['device_pk']
,p['client_sk']
,timeout=p.get('timeout'))
elif 'usb_serial' in p:
p = USBPeer(name
,p['usb_serial']
,p['device_pk']
,p['client_sk']
,timeout=p.get('timeout'))
else:
raise ValueError(f"cannot decide type of peer: {name}")
self.peers.append(p)
def __getitem__(self, idx):
return self.peers[idx]
def __iter__(self):
for p in self.peers:
yield p
def __len__(self):
return len(self.peers)
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, exception_traceback):
if exception_type is not None:
print("exception caught", exception_type, exception_value, exception_traceback)
self.close()
def connect(self):
for p in self.peers:
p.connect()
def send(self, idx, msg):
self.peers[idx].send(msg)
def broadcast(self, msg):
for p in self.peers:
p.send(msg)
async def gather_async(self, expected_msg_len, n=None, proc=None):
results = await asyncio.gather(
*[peer.read_async(expected_msg_len) for peer in self.peers], return_exceptions=True
)
for i in range(len(results)):
if isinstance(results[i], Exception):
print(f"client {self.peers[i].name} returned exception: {results[i]}", file=sys.stderr)
results[i]=None
continue
if results[i] == b'\x00\x04fail':
results[i]=None
continue
tmp = results[i] if not proc else proc(results[i])
if tmp is None: continue
results[i]=tmp
if n is None:
n=len(self.peers)
if len([1 for e in results if e is not None]) < n:
raise ValueError(f"not enough responses gathered: {results}")
return results
def gather(self, *args, **kwargs):
return get_event_loop().run_until_complete(self.gather_async(*args, **kwargs))
def close(self):
for p in self.peers:
p.close()
liboprf-0.9.4/python/pyoprf/noisexk.py 0000775 0000000 0000000 00000023475 15146734002 0020073 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Wrapper for hacl-star XK_Noise
SPDX-FileCopyrightText: 2024, Marsiske Stefan
SPDX-License-Identifier: LGPL-3.0-or-later
Copyright (c) 2024, Marsiske Stefan.
All rights reserved.
This file is part of liboprf.
liboprf is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
liboprf is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with liboprf. If not, see .
"""
import ctypes
import ctypes.util
from ctypes import c_void_p, c_ubyte, c_uint32, c_char, c_size_t, POINTER, byref
lib = ctypes.cdll.LoadLibrary(ctypes.util.find_library('oprf-noiseXK')
or ctypes.util.find_library('liboprf-noiseXK'))
if not lib._name:
raise ValueError('Unable to find liboprf-noiseXK')
libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c') or ctypes.util.find_library('libc'))
if not libc._name:
raise ValueError('Unable to find libc')
KEYSIZE = 32
NOISE_XK_CONF_ZERO = 0
NOISE_XK_AUTH_KNOWN_SENDER_NO_KCI = 2
NOISE_XK_CONF_STRONG_FORWARD_SECRECY = 5
def __check(code):
if code != 0:
raise ValueError
lib.Noise_XK_device_add_peer.restype = c_void_p
lib.Noise_XK_device_add_peer.argtypes = [c_void_p, c_void_p, ctypes.c_char_p]
def add_peer(device, name, key):
return lib.Noise_XK_device_add_peer(device, name, key)
def pubkey(privkey):
pubkey = ctypes.create_string_buffer(KEYSIZE)
lib.Noise_XK_dh_secret_to_public(pubkey, privkey)
return pubkey.raw
lib.Noise_XK_device_create.restype = c_void_p
def create_device(prologue, name, privkey):
srlz_key = b'\x00'*KEYSIZE
return lib.Noise_XK_device_create(len(prologue), prologue, name, srlz_key, privkey)
lib.Noise_XK_peer_get_id.restype = c_void_p
lib.Noise_XK_peer_get_id.argtypes = [c_void_p]
def get_peerid(peer):
return lib.Noise_XK_peer_get_id(peer)
lib.Noise_XK_session_create_initiator.restype = c_void_p
lib.Noise_XK_session_create_initiator.argtypes = [c_void_p, c_void_p]
def create_session_initiator(device, peerid):
return lib.Noise_XK_session_create_initiator(device, peerid)
lib.Noise_XK_session_create_initiator.restype = c_void_p
lib.Noise_XK_session_create_initiator.argtypes = [c_void_p, c_void_p]
def create_session_initiator(device, peerid):
res = lib.Noise_XK_session_create_initiator(device, peerid)
if res == 0: raise ValueError
return res
lib.Noise_XK_session_create_responder.restype = c_void_p
lib.Noise_XK_session_create_responder.argtypes = [c_void_p]
def create_session_responder(device):
res = lib.Noise_XK_session_create_responder(device)
if res == 0: raise ValueError
return res
lib.Noise_XK_pack_message_with_conf_level.restype = c_void_p
lib.Noise_XK_session_write.argtypes = [c_void_p, c_void_p, POINTER(c_uint32), POINTER(POINTER(c_ubyte))]
lib.Noise_XK_encap_message_p_free.argtypes = [c_void_p]
def initiator_1st_msg(session):
encap_msg = lib.Noise_XK_pack_message_with_conf_level(0, 0, 0);
msg_len = c_uint32()
msg = POINTER(c_ubyte)()
if 0!=lib.Noise_XK_session_write(encap_msg, session, byref(msg_len), byref(msg)):
raise ValueError
lib.Noise_XK_encap_message_p_free(encap_msg)
res = bytes(msg[i] for i in range(msg_len.value))
if msg_len.value > 0:
libc.free(msg)
return res
# Noise_XK_session_read(&encap_msg, bob_session, cipher_msg_len, cipher_msg);
lib.Noise_XK_session_read.argtypes = [POINTER(c_void_p), c_void_p, c_uint32, POINTER(c_ubyte)]
# Noise_XK_unpack_message_with_auth_level(&plain_msg_len, &plain_msg, NOISE_XK_AUTH_ZERO, encap_msg),
def responder_1st_msg(session, msg):
encap_msg = c_void_p()
msg = (c_ubyte * len(msg)).from_buffer(bytearray(msg))
msg_len = c_uint32(len(msg))
if 0 != lib.Noise_XK_session_read(byref(encap_msg), session, msg_len, msg):
raise ValueError
plain_msg_len = c_uint32()
plain_msg = POINTER(c_ubyte)()
if not lib.Noise_XK_unpack_message_with_auth_level(byref(plain_msg_len), byref(plain_msg), 0, encap_msg):
raise ValueError
lib.Noise_XK_encap_message_p_free(encap_msg)
if plain_msg_len.value > 0:
libc.free(plain_msg)
return initiator_1st_msg(session)
def initiator_handshake_finish(session, msg):
encap_msg = c_void_p()
msg = (c_ubyte * len(msg)).from_buffer(bytearray(msg))
msg_len = c_uint32(len(msg))
if 0 != lib.Noise_XK_session_read(byref(encap_msg), session, msg_len, msg):
raise ValueError
plain_msg_len = c_uint32()
plain_msg = POINTER(c_ubyte)()
if not lib.Noise_XK_unpack_message_with_auth_level(byref(plain_msg_len), byref(plain_msg), 0, encap_msg):
raise ValueError
lib.Noise_XK_encap_message_p_free(encap_msg)
if plain_msg_len.value > 0:
libc.free(plain_msg)
def send_msg(session, msg):
if isinstance(msg, str): msg = msg.encode('utf8')
encap_msg = lib.Noise_XK_pack_message_with_conf_level(NOISE_XK_CONF_STRONG_FORWARD_SECRECY, len(msg), msg);
ct_len = c_uint32()
ct = POINTER(c_ubyte)()
if 0!=lib.Noise_XK_session_write(encap_msg, session, byref(ct_len), byref(ct)):
raise ValueError
lib.Noise_XK_encap_message_p_free(encap_msg)
res = bytes(ct[:ct_len.value])
if ct_len.value > 0:
libc.free(ct)
return res
def read_msg(session, msg):
encap_msg = c_void_p()
u_bytes = (c_ubyte * (len(msg)))()
u_bytes[:] = msg
if 0 != lib.Noise_XK_session_read(byref(encap_msg), session, len(msg), u_bytes):
raise ValueError
plain_msg_len = c_uint32()
plain_msg = POINTER(c_ubyte)()
if not lib.Noise_XK_unpack_message_with_auth_level(byref(plain_msg_len), byref(plain_msg),
NOISE_XK_AUTH_KNOWN_SENDER_NO_KCI, encap_msg):
raise ValueError
lib.Noise_XK_encap_message_p_free(encap_msg)
res = bytes(plain_msg[i] for i in range(plain_msg_len.value))
if plain_msg_len.value > 0:
libc.free(plain_msg)
return res
lib.Noise_XK_session_get_peer_id.restype = c_uint32
lib.Noise_XK_session_get_peer_id.argtypes = [c_void_p]
lib.Noise_XK_device_lookup_peer_by_id.restype = c_void_p
lib.Noise_XK_device_lookup_peer_by_id.argtypes = [c_void_p, c_uint32]
lib.Noise_XK_peer_get_static.argtypes = [(c_char * 32), c_void_p]
def get_pubkey(session, device):
peerid = lib.Noise_XK_session_get_peer_id(session)
peer = lib.Noise_XK_device_lookup_peer_by_id(device, peerid);
pubkey = ctypes.create_string_buffer(KEYSIZE)
lib.Noise_XK_peer_get_static(pubkey, peer);
return pubkey.raw
def initiator_session(initiator_privkey, responder_pubkey, iname=None,
rname=None, dst=None):
if dst is None:
dst = b"liboprf-noiseXK"
if iname is None:
iname = b"initiator"
if rname is None:
rname = b"responder"
#initiator_pubkey = pubkey(initiator_privkey)
dev = create_device(dst, iname, initiator_privkey)
peer = add_peer(dev, rname, responder_pubkey)
peerid = get_peerid(peer)
session = create_session_initiator(dev, peerid)
msg = initiator_1st_msg(session)
return session, msg
libc.malloc.restype = POINTER(c_ubyte)
def responder_session(responder_privkey, auth_keys, msg, dst=None, name=None):
if dst is None:
dst = b"liboprf-noiseXK"
if name is None:
name = b"responder"
#responder_pubkey = pubkey(responder_privkey)
dev = create_device(dst, name, responder_privkey)
for key, peer in auth_keys:
add_peer(dev,peer,key)
session = create_session_responder(dev)
msg = responder_1st_msg(session, msg)
return session, msg
def initiator_session_complete(session, msg):
return initiator_handshake_finish(session, msg)
def test():
from binascii import unhexlify, hexlify
# low level
alice_privkey = unhexlify("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552")
alice_pubkey = pubkey(alice_privkey)
bob_privkey = unhexlify("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552")
bob_pubkey = pubkey(bob_privkey)
adev = create_device("liboprf-noiseXK test", "Alice", alice_privkey)
bpeer = add_peer(adev, "Bob", bob_pubkey)
bobid = get_peerid(bpeer)
bdev = create_device("liboprf-noiseXK test", "Bob", bob_privkey)
add_peer(bdev, "Alice", alice_pubkey)
asession = create_session_initiator(adev, bobid)
bsession = create_session_responder(bdev)
msg = initiator_1st_msg(asession)
msg = responder_1st_msg(bsession, msg)
initiator_handshake_finish(asession, msg)
ct = send_msg(asession, "hello bob!")
pt = read_msg(bsession, ct)
peer_pk = get_pubkey(bsession, bdev)
print(hexlify(peer_pk))
print(pt)
ct = send_msg(bsession, "hello alice!")
pt = read_msg(asession, ct)
print(pt)
# high-level
a2session, msg = initiator_session(alice_privkey, bob_pubkey)
b2session, msg = responder_session(bob_privkey, [(alice_pubkey, "Alice")], msg)
initiator_session_complete(a2session, msg)
ct = send_msg(a2session, "hello bob!")
pt = read_msg(b2session, ct)
print(pt)
ct = send_msg(b2session, "hello alice!")
pt = read_msg(a2session, ct)
print(pt)
for _ in range(1000):
if ct[0] % 2 == 0:
sender = a2session
receiver = b2session
else:
sender = b2session
receiver = a2session
message = ct[:16+(ct[1]>>4)] * (ct[1] & 0xf)
ct = send_msg(sender, message)
pt = read_msg(receiver, ct)
assert(pt == message)
if __name__ == '__main__':
test()
liboprf-0.9.4/python/setup.py 0000775 0000000 0000000 00000002436 15146734002 0016226 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# SPDX-FileCopyrightText: 2023, Marsiske Stefan
# SPDX-License-Identifier: LGPL-3.0-or-later
import os
from setuptools import setup, find_packages
# Utility function to read the README file.
# Used for the long_description. It's nice, because now 1) we have a top level
# README file and 2) it's easier to type in the README file than to put a raw
# string in below ...
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(name = 'pyoprf',
version = '0.9.4',
description = 'python bindings for liboprf',
license = "LGPLv3",
author = 'Stefan Marsiske',
author_email = 'toprf@ctrlc.hu',
url = 'https://github.com/stef/liboprf/python',
long_description=read('README.md'),
long_description_content_type="text/markdown",
packages=find_packages(),
install_requires = ("pysodium", "SecureString", "pyserial", "pyudev", "ble_serial", "pyserial-asyncio"),
classifiers = ["Development Status :: 4 - Beta",
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
"Topic :: Security :: Cryptography",
"Topic :: Security",
],
#ext_modules = [liboprf],
)
liboprf-0.9.4/python/tests/ 0000775 0000000 0000000 00000000000 15146734002 0015646 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/python/tests/test.py 0000775 0000000 0000000 00000024376 15146734002 0017216 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import unittest
import pyoprf, pysodium, ctypes
from binascii import unhexlify
from itertools import combinations
class TestEndToEnd(unittest.TestCase):
def test_cfrg_irtf(self):
"""CFRG/IRTF spec compliant run"""
# Alice blinds the input "test"
r, alpha = pyoprf.blind(b"test")
# Bob generates a "secret" key
k = pyoprf.keygen()
# Bob evaluates Alices blinded value with it's key
beta = pyoprf.evaluate(k, alpha)
# Alice unblinds Bobs evaluation
N = pyoprf.unblind(r, beta)
# Alice finalizes the calculation
y = pyoprf.finalize(b"test", N)
# rerun and assert that oprf(k,"test") equals all runs
r, alpha = pyoprf.blind(b"test")
beta = pyoprf.evaluate(k, alpha)
N = pyoprf.unblind(r, beta)
y2 = pyoprf.finalize(b"test", N)
self.assertEqual(y, y2)
def test_cfrg_irtf_testvec1(self):
"""IRTF/CFRG testvector 1"""
x = unhexlify("00")
k = unhexlify("5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e")
out=unhexlify("527759c3d9366f277d8c6020418d96bb393ba2afb20ff90df23fb7708264e2f3ab9135e3bd69955851de4b1f9fe8a0973396719b7912ba9ee8aa7d0b5e24bcf6")
r, alpha = pyoprf.blind(x)
beta = pyoprf.evaluate(k, alpha)
N = pyoprf.unblind(r, beta)
y = pyoprf.finalize(x, N)
self.assertEqual(y,out)
def test_cfrg_irtf_testvec2(self):
"""IRTF/CFRG testvector 2"""
x=unhexlify("5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a")
k = unhexlify("5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e")
out=unhexlify("f4a74c9c592497375e796aa837e907b1a045d34306a749db9f34221f7e750cb4f2a6413a6bf6fa5e19ba6348eb673934a722a7ede2e7621306d18951e7cf2c73")
r, alpha = pyoprf.blind(x)
beta = pyoprf.evaluate(k, alpha)
N = pyoprf.unblind(r, beta)
y = pyoprf.finalize(x, N)
self.assertEqual(y, out)
def test_hashDH_update(self):
"""HashDH with update example"""
# Alice blinds the input "test"
r, alpha = pyoprf.blind(b"test")
# Bob generates a "secret" key
k = pyoprf.keygen()
# Bob evaluates Alices blinded value with it's key
beta = pyoprf.evaluate(k, alpha)
# Alice unblinds Bobs evaluation
N = pyoprf.unblind(r, beta)
# Bob updates his key, by generating delta
delta = pysodium.crypto_core_ristretto255_scalar_random()
k2 = pysodium.crypto_core_ristretto255_scalar_mul(k, delta)
# Alice updates her previous calculation of N with delta
N2 = pysodium.crypto_scalarmult_ristretto255(delta, N)
# rerun hashDH to verify if N2 is equal with a full run
r, alpha = pyoprf.blind(b"test")
beta = pyoprf.evaluate(k2, alpha)
N2_ = pyoprf.unblind(r, beta)
self.assertEqual(N2, N2_)
def test_toprf_sss(self):
"""tOPRF (hashDH), (3,5), with centrally shared key interpolation at client"""
k2 = pyoprf.keygen()
shares = pyoprf.create_shares(k2, 5, 3)
r, alpha = pyoprf.blind(b"test")
#print(' '.join(s.hex() for s in shares))
# we reuse values from te previous test
betas = tuple(s[:1]+pyoprf.evaluate(s[1:], alpha) for s in shares)
#print(''.join(b.hex() for b in betas))
beta = pyoprf.thresholdmult(betas)
Nt = pyoprf.unblind(r, beta)
beta = pyoprf.evaluate(k2, alpha)
N2 = pyoprf.unblind(r, beta)
self.assertEqual(N2, Nt)
def test_toprf_tcombine(self):
"""tOPRF (hashDH), (3,5), with centrally shared key interpolation at servers"""
k2 = pyoprf.keygen()
shares = pyoprf.create_shares(k2, 5, 3)
r, alpha = pyoprf.blind(b"test")
indexes=(4,2,1)
betas = tuple(pyoprf.threshold_evaluate(shares[i-1], alpha, i, indexes) for i in indexes)
beta = pyoprf.threshold_combine(betas)
beta = pyoprf.evaluate(k2, alpha)
Nt = pyoprf.unblind(r, beta)
Nt2 = pyoprf.unblind(r, beta)
self.assertEqual(Nt, Nt2)
def test_raw_dkg(self):
"""naked Distributed KeyGen (3,5)"""
n = 5
t = 3
mailboxes=[[] for _ in range(n)]
commitments=[]
for _ in range(n):
coms, shares = pyoprf.dkg_start(n,t)
commitments.append(coms)
for i,s in enumerate(shares):
mailboxes[i].append(s)
commitments=b''.join(commitments)
shares = []
for i in range(n):
fails = pyoprf.dkg_verify_commitments(n,t,i+1,
commitments,
mailboxes[i])
if len(fails) > 0:
for fail in fails:
print(f"fail: peer {fail}")
raise ValueError("failed to verify contributions, aborting")
xi = pyoprf.dkg_finish(n, mailboxes[i], i+1)
#print(i, xi.hex(), x_i.hex())
shares.append(xi)
# test if the final shares all reproduce the same shared `secret`
v0 = pyoprf.thresholdmult([bytes([i+1])+pysodium.crypto_scalarmult_ristretto255_base(shares[i][1:]) for i in (0,1,2)])
for peers in combinations(range(1,5), 3):
v1 = pyoprf.thresholdmult([bytes([i+1])+pysodium.crypto_scalarmult_ristretto255_base(shares[i][1:]) for i in peers])
self.assertEqual(v0, v1)
secret = pyoprf.dkg_reconstruct(shares[:t])
#print("secret", secret.hex())
self.assertEqual(v0, pysodium.crypto_scalarmult_ristretto255_base(secret))
def test_explicit_3hashtdh(self):
"""toprf based on 2024/1455 [JSPPJ24] https://eprint.iacr.org/2024/1455
using explicit implementation of 3hashtdh"""
print("tOPRF (3hashTDH), (3,5), with centrally shared key interpolation at client")
k2 = pyoprf.keygen()
shares = pyoprf.create_shares(k2, 5, 3)
zero_shares = pyoprf.create_shares(bytes([0]*32), 5, 3)
r, alpha = pyoprf.blind(b"test")
ssid_S = pysodium.randombytes(32)
betas = []
for k, z in zip(shares,zero_shares):
h2 = pyoprf.evaluate(
z[1:],
pysodium.crypto_core_ristretto255_from_hash(pysodium.crypto_generichash(ssid_S + alpha, outlen=64)),
)
beta = pyoprf.evaluate(k[1:], alpha)
betas.append(k[:1]+pysodium.crypto_core_ristretto255_add(beta, h2))
# normal 2hashdh(k2,"test")
beta = pyoprf.evaluate(k2, alpha)
Nt0 = pyoprf.unblind(r, beta)
for peers in combinations(betas, 3):
beta = pyoprf.thresholdmult(betas[:3])
Nt1 = pyoprf.unblind(r, beta)
self.assertEqual(Nt0, Nt1)
def test_native_3hashtdh(self):
"""toprf based on 2024/1455 [JSPPJ24] https://eprint.iacr.org/2024/1455
using libopr native implementation of 3hashtdh
tOPRF (3hashTDH), (3,5), with centrally shared key interpolation at client"""
k2 = pyoprf.keygen()
shares = pyoprf.create_shares(k2, 5, 3)
zero_shares = pyoprf.create_shares(bytes([0]*32), 5, 3)
r, alpha = pyoprf.blind(b"test")
ssid_S = pysodium.randombytes(32)
betas = []
for k, z in zip(shares,zero_shares):
betas.append(pyoprf._3hashtdh(k, z, alpha, ssid_S))
beta = pyoprf.evaluate(k2, alpha)
Nt0 = pyoprf.unblind(r, beta)
for peers in combinations(betas, 3):
beta = pyoprf.thresholdmult(betas[:3])
Nt1 = pyoprf.unblind(r, beta)
self.assertEqual(Nt0, Nt1)
def test_tp_dkg(self):
"""Trusted Party Distributed KeyGeneration"""
n = 5
t = 3
ts_epsilon = 5
# enable verbose logging for tp-dkg
#libc = ctypes.cdll.LoadLibrary('libc.so.6')
#cstderr = ctypes.c_void_p.in_dll(libc, 'stderr')
#log_file = ctypes.c_void_p.in_dll(pyoprf.liboprf,'log_file')
#log_file.value = cstderr.value
# create some long-term keypairs
peer_lt_pks = []
peer_lt_sks = []
for _ in range(n):
pk, sk = pysodium.crypto_sign_keypair()
peer_lt_pks.append(pk)
peer_lt_sks.append(sk)
# initialize the TP and get the first message
tp, msg0 = pyoprf.tpdkg_start_tp(n, t, ts_epsilon, "pyoprf tpdkg test", peer_lt_pks)
print(f"\nn: {pyoprf.tpdkg_tpstate_n(tp)}, t: {pyoprf.tpdkg_tpstate_t(tp)}, sid: {bytes(c for c in pyoprf.tpdkg_tpstate_sessionid(tp)).hex()}")
# initialize all peers with the 1st message from TP
peers=[]
for i in range(n):
peer = pyoprf.tpdkg_peer_start(ts_epsilon, peer_lt_sks[i], msg0)
peers.append(peer)
for i in range(n):
self.assertEqual(pyoprf.tpdkg_peerstate_sessionid(peers[i]), pyoprf.tpdkg_tpstate_sessionid(tp))
self.assertEqual(peer_lt_sks[i], pyoprf.tpdkg_peerstate_lt_sk(peers[i]))
peer_msgs = []
while pyoprf.tpdkg_tp_not_done(tp):
ret, sizes = pyoprf.tpdkg_tp_input_sizes(tp)
# peer_msgs = (recv(size) for size in sizes)
msgs = b''.join(peer_msgs)
cur_step = pyoprf.tpdkg_tpstate_step(tp)
try:
tp_out = pyoprf.tpdkg_tp_next(tp, msgs)
#print(f"tp: msg[{tp[0].step}]: {tp_out.raw.hex()}")
except Exception as e:
cheaters, cheats = pyoprf.tpdkg_get_cheaters(tp)
print(f"Warning during the distributed key generation the peers misbehaved: {sorted(cheaters)}")
for k, v in cheats:
print(f"\tmisbehaving peer: {k} was caught: {v}")
raise ValueError(f"{e} | tp step {cur_step}")
peer_msgs = []
while(len(b''.join(peer_msgs))==0 and pyoprf.tpdkg_peer_not_done(peers[0])):
for i in range(n):
if(len(tp_out)>0):
msg = pyoprf.tpdkg_tp_peer_msg(tp, tp_out, i)
#print(f"tp -> peer[{i+1}] {msg.hex()}")
else:
msg = ''
out = pyoprf.tpdkg_peer_next(peers[i], msg)
if(len(out)>0):
peer_msgs.append(out)
#print(f"peer[{i+1}] -> tp {peer_msgs[-1].hex()}")
tp_out = ''
# we are done, let's check the shares
shares = [pyoprf.tpdkg_peerstate_share(peers[i]) for i in range(n)]
for i, share in enumerate(shares):
print(f"share[{i+1}] {share.hex()}")
v0 = pyoprf.thresholdmult([bytes([i+1])+pysodium.crypto_scalarmult_ristretto255_base(shares[i][1:]) for i in (0,1,2)])
for peers_idxs in combinations(range(1,5), 3):
v1 = pyoprf.thresholdmult([bytes([i+1])+pysodium.crypto_scalarmult_ristretto255_base(shares[i][1:]) for i in peers_idxs])
self.assertEqual(v0, v1)
secret = pyoprf.dkg_reconstruct(shares[:t])
#print("secret", secret.hex())
self.assertEqual(v0, pysodium.crypto_scalarmult_ristretto255_base(secret))
# clean up allocated buffers
for i in range(n):
pyoprf.tpdkg_peer_free(peers[i])
liboprf-0.9.4/src/ 0000775 0000000 0000000 00000000000 15146734002 0013752 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/src/aux_/ 0000775 0000000 0000000 00000000000 15146734002 0014706 5 ustar 00root root 0000000 0000000 liboprf-0.9.4/src/aux_/crypto_kdf_hkdf_sha256.h 0000664 0000000 0000000 00000004501 15146734002 0021307 0 ustar 00root root 0000000 0000000 #ifndef crypto_kdf_hkdf_sha256_H
#define crypto_kdf_hkdf_sha256_H
#include
#include
#include
#include
//#include "crypto_kdf.h"
//#include "crypto_auth_hmacsha256.h"
//#include "export.h"
#ifdef __cplusplus
# ifdef __GNUC__
# pragma GCC diagnostic ignored "-Wlong-long"
# endif
extern "C" {
#endif
#define crypto_kdf_hkdf_sha256_KEYBYTES crypto_auth_hmacsha256_BYTES
SODIUM_EXPORT
size_t crypto_kdf_hkdf_sha256_keybytes(void);
#define crypto_kdf_hkdf_sha256_BYTES_MIN 0U
SODIUM_EXPORT
size_t crypto_kdf_hkdf_sha256_bytes_min(void);
#define crypto_kdf_hkdf_sha256_BYTES_MAX (0xff * crypto_auth_hmacsha256_BYTES)
SODIUM_EXPORT
size_t crypto_kdf_hkdf_sha256_bytes_max(void);
SODIUM_EXPORT
int crypto_kdf_hkdf_sha256_extract(unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES],
const unsigned char *salt, size_t salt_len,
const unsigned char *ikm, size_t ikm_len)
__attribute__ ((nonnull(4)));
SODIUM_EXPORT
void crypto_kdf_hkdf_sha256_keygen(unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES]);
SODIUM_EXPORT
int crypto_kdf_hkdf_sha256_expand(unsigned char *out, size_t out_len,
const char *ctx, size_t ctx_len,
const unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES])
__attribute__ ((nonnull(1)));
/* ------------------------------------------------------------------------- */
typedef struct crypto_kdf_hkdf_sha256_state {
crypto_auth_hmacsha256_state st;
} crypto_kdf_hkdf_sha256_state;
SODIUM_EXPORT
size_t crypto_kdf_hkdf_sha256_statebytes(void);
SODIUM_EXPORT
int crypto_kdf_hkdf_sha256_extract_init(crypto_kdf_hkdf_sha256_state *state,
const unsigned char *salt, size_t salt_len)
__attribute__ ((nonnull(1)));
SODIUM_EXPORT
int crypto_kdf_hkdf_sha256_extract_update(crypto_kdf_hkdf_sha256_state *state,
const unsigned char *ikm, size_t ikm_len)
__attribute__ ((nonnull));
SODIUM_EXPORT
int crypto_kdf_hkdf_sha256_extract_final(crypto_kdf_hkdf_sha256_state *state,
unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES])
__attribute__ ((nonnull));
#ifdef __cplusplus
}
#endif
#endif
liboprf-0.9.4/src/aux_/kdf_hkdf_sha256.c 0000664 0000000 0000000 00000007555 15146734002 0017716 0 ustar 00root root 0000000 0000000 #include
#include
#include "sodium/crypto_auth_hmacsha256.h"
#include "sodium/crypto_kdf.h"
#include "crypto_kdf_hkdf_sha256.h"
#include "sodium/randombytes.h"
#include "sodium/utils.h"
int
crypto_kdf_hkdf_sha256_extract_init(crypto_kdf_hkdf_sha256_state *state,
const unsigned char *salt, size_t salt_len)
{
return crypto_auth_hmacsha256_init(&state->st, salt, salt_len);
}
int
crypto_kdf_hkdf_sha256_extract_update(crypto_kdf_hkdf_sha256_state *state,
const unsigned char *ikm, size_t ikm_len)
{
return crypto_auth_hmacsha256_update(&state->st, ikm, ikm_len);
}
int
crypto_kdf_hkdf_sha256_extract_final(crypto_kdf_hkdf_sha256_state *state,
unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES])
{
crypto_auth_hmacsha256_final(&state->st, prk);
sodium_memzero(state, sizeof *state);
return 0;
}
int
crypto_kdf_hkdf_sha256_extract(
unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES],
const unsigned char *salt, size_t salt_len, const unsigned char *ikm,
size_t ikm_len)
{
crypto_kdf_hkdf_sha256_state state;
crypto_kdf_hkdf_sha256_extract_init(&state, salt, salt_len);
crypto_kdf_hkdf_sha256_extract_update(&state, ikm, ikm_len);
return crypto_kdf_hkdf_sha256_extract_final(&state, prk);
}
void
crypto_kdf_hkdf_sha256_keygen(unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES])
{
randombytes_buf(prk, crypto_kdf_hkdf_sha256_KEYBYTES);
}
int
crypto_kdf_hkdf_sha256_expand(unsigned char *out, size_t out_len,
const char *ctx, size_t ctx_len,
const unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES])
{
crypto_auth_hmacsha256_state st;
unsigned char tmp[crypto_auth_hmacsha256_BYTES];
size_t i;
size_t left;
unsigned char counter = 1U;
if (out_len > crypto_kdf_hkdf_sha256_BYTES_MAX) {
errno = EINVAL;
return -1;
}
for (i = (size_t) 0U; i + crypto_auth_hmacsha256_BYTES <= out_len;
i += crypto_auth_hmacsha256_BYTES) {
crypto_auth_hmacsha256_init(&st, prk, crypto_kdf_hkdf_sha256_KEYBYTES);
if (i != (size_t) 0U) {
crypto_auth_hmacsha256_update(&st,
&out[i - crypto_auth_hmacsha256_BYTES],
crypto_auth_hmacsha256_BYTES);
}
crypto_auth_hmacsha256_update(&st,
(const unsigned char *) ctx, ctx_len);
crypto_auth_hmacsha256_update(&st, &counter, (size_t) 1U);
crypto_auth_hmacsha256_final(&st, &out[i]);
counter++;
}
if ((left = out_len & (crypto_auth_hmacsha256_BYTES - 1U)) != (size_t) 0U) {
crypto_auth_hmacsha256_init(&st, prk, crypto_kdf_hkdf_sha256_KEYBYTES);
if (i != (size_t) 0U) {
crypto_auth_hmacsha256_update(&st,
&out[i - crypto_auth_hmacsha256_BYTES],
crypto_auth_hmacsha256_BYTES);
}
crypto_auth_hmacsha256_update(&st,
(const unsigned char *) ctx, ctx_len);
crypto_auth_hmacsha256_update(&st, &counter, (size_t) 1U);
crypto_auth_hmacsha256_final(&st, tmp);
memcpy(&out[i], tmp, left);
sodium_memzero(tmp, sizeof tmp);
}
sodium_memzero(&st, sizeof st);
return 0;
}
size_t
crypto_kdf_hkdf_sha256_keybytes(void)
{
return crypto_kdf_hkdf_sha256_KEYBYTES;
}
size_t
crypto_kdf_hkdf_sha256_bytes_min(void)
{
return crypto_kdf_hkdf_sha256_BYTES_MIN;
}
size_t
crypto_kdf_hkdf_sha256_bytes_max(void)
{
return crypto_kdf_hkdf_sha256_BYTES_MAX;
}
size_t crypto_kdf_hkdf_sha256_statebytes(void)
{
return sizeof(crypto_kdf_hkdf_sha256_state);
}
liboprf-0.9.4/src/dkg-vss.c 0000664 0000000 0000000 00000013264 15146734002 0015502 0 ustar 00root root 0000000 0000000 #include
#include
#include
#include "toprf.h"
#include "utils.h"
#include "dkg.h"
// nothing up my sleeve generator H, generated with:
// hash_to_group((uint8_t*)"DKG Generator H on ristretto255", 32, H)
const __attribute__((visibility("hidden"))) uint8_t H[crypto_core_ristretto255_BYTES]= {
0x66, 0x4e, 0x4c, 0xb5, 0x89, 0x0e, 0xb3, 0xe4,
0xc0, 0xd5, 0x48, 0x02, 0x74, 0x8a, 0xb2, 0x25,
0xf9, 0x73, 0xda, 0xe5, 0xc0, 0xef, 0xc1, 0x68,
0xf4, 0x4d, 0x1b, 0x60, 0x28, 0x97, 0x8f, 0x07};
int dkg_vss_commit(const uint8_t a[crypto_core_ristretto255_SCALARBYTES],
const uint8_t r[crypto_core_ristretto255_SCALARBYTES],
uint8_t C[crypto_core_ristretto255_BYTES]) {
// compute commitments
uint8_t X[crypto_core_ristretto255_BYTES];
uint8_t R[crypto_core_ristretto255_BYTES];
// x = g^a_ik
crypto_scalarmult_ristretto255_base(X, a);
// r = h^b_ik
if(crypto_scalarmult_ristretto255(R, r, H)) return 1;
// C_ik = x+r
crypto_core_ristretto255_add(C,X,R);
return 0;
}
int dkg_vss_share(const uint8_t n,
const uint8_t threshold,
const uint8_t secret[crypto_core_ristretto255_SCALARBYTES],
uint8_t commitments[n][crypto_core_ristretto255_BYTES],
TOPRF_Share shares[n][2],
uint8_t blind[crypto_core_ristretto255_SCALARBYTES]) {
if(threshold==0) return 1;
uint8_t a[threshold][crypto_core_ristretto255_SCALARBYTES];
uint8_t b[threshold][crypto_core_ristretto255_SCALARBYTES];
if(secret!=NULL) memcpy(a[0],secret, crypto_core_ristretto255_SCALARBYTES);
for(int k=0;kvalue, crypto_core_ristretto255_SCALARBYTES, "x[%d] ", self);
//dump(x_i->value, crypto_core_ristretto255_SCALARBYTES, "x'[%d] ", self);
if(0!=dkg_vss_commit(share[0].value, share[1].value, commitment)) return 1;
return 0;
}
static void sort_shares(const int n, uint8_t arr[n], uint8_t indexes[n]) {
for (uint8_t c = 1 ; c <= n - 1; c++) {
uint8_t d = c, t, t1;
while(d > 0 && arr[d] < arr[d-1]) {
t = arr[d];
t1 = indexes[d];
arr[d] = arr[d-1];
indexes[d] = indexes[d-1];
arr[d-1] = t;
indexes[d-1] = t1;
d--;
}
}
}
int dkg_vss_reconstruct(const uint8_t t,
const uint8_t x,
const size_t shares_len,
const TOPRF_Share shares[shares_len][2],
const uint8_t commitments[shares_len][crypto_scalarmult_ristretto255_BYTES],
uint8_t result[crypto_scalarmult_ristretto255_SCALARBYTES],
uint8_t blind[crypto_scalarmult_ristretto255_SCALARBYTES]) {
if(shares_len>128) return 1;
uint8_t qual[t];
uint8_t indexes[t];
unsigned j=0;
for(uint8_t i=0;i