mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-06-03 08:10:52 +00:00
reproducible ssk vectors
This commit is contained in:
parent
1827834434
commit
4eec8113ee
@ -9,11 +9,13 @@ from hashlib import sha256
|
|||||||
from itertools import starmap, product
|
from itertools import starmap, product
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
|
||||||
|
from attrs import evolve
|
||||||
|
|
||||||
from pytest import mark
|
from pytest import mark
|
||||||
from pytest_twisted import ensureDeferred
|
from pytest_twisted import ensureDeferred
|
||||||
|
|
||||||
from . import vectors
|
from . import vectors
|
||||||
from .util import reconfigure, upload, TahoeProcess
|
from .util import CHK, SSK, reconfigure, upload, TahoeProcess
|
||||||
|
|
||||||
def digest(bs: bytes) -> bytes:
|
def digest(bs: bytes) -> bytes:
|
||||||
"""
|
"""
|
||||||
@ -75,9 +77,11 @@ ZFEC_PARAMS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
FORMATS = [
|
FORMATS = [
|
||||||
"chk",
|
CHK(),
|
||||||
# "sdmf",
|
# These start out unaware of a key but various keys will be supplied
|
||||||
# "mdmf",
|
# during generation.
|
||||||
|
SSK(name="sdmf", key=None),
|
||||||
|
SSK(name="mdmf", key=None),
|
||||||
]
|
]
|
||||||
|
|
||||||
@mark.parametrize('convergence', CONVERGENCE_SECRETS)
|
@mark.parametrize('convergence', CONVERGENCE_SECRETS)
|
||||||
@ -89,18 +93,15 @@ def test_convergence(convergence):
|
|||||||
assert len(convergence) == 16, "Convergence secret must by 16 bytes"
|
assert len(convergence) == 16, "Convergence secret must by 16 bytes"
|
||||||
|
|
||||||
|
|
||||||
@mark.parametrize('seed_params', ZFEC_PARAMS)
|
@mark.parametrize('case_and_expected', vectors.capabilities.items())
|
||||||
@mark.parametrize('convergence', CONVERGENCE_SECRETS)
|
|
||||||
@mark.parametrize('seed_data', OBJECT_DESCRIPTIONS)
|
|
||||||
@mark.parametrize('fmt', FORMATS)
|
|
||||||
@ensureDeferred
|
@ensureDeferred
|
||||||
async def test_capability(reactor, request, alice, seed_params, convergence, seed_data, fmt):
|
async def test_capability(reactor, request, alice, case_and_expected):
|
||||||
"""
|
"""
|
||||||
The capability that results from uploading certain well-known data
|
The capability that results from uploading certain well-known data
|
||||||
with certain well-known parameters results in exactly the previously
|
with certain well-known parameters results in exactly the previously
|
||||||
computed value.
|
computed value.
|
||||||
"""
|
"""
|
||||||
case = vectors.Case(seed_params, convergence, seed_data, fmt)
|
case, expected = case_and_expected
|
||||||
|
|
||||||
# rewrite alice's config to match params and convergence
|
# rewrite alice's config to match params and convergence
|
||||||
await reconfigure(reactor, request, alice, (1, case.params.required, case.params.total), case.convergence)
|
await reconfigure(reactor, request, alice, (1, case.params.required, case.params.total), case.convergence)
|
||||||
@ -109,7 +110,6 @@ async def test_capability(reactor, request, alice, seed_params, convergence, see
|
|||||||
actual = upload(alice, case.fmt, case.data)
|
actual = upload(alice, case.fmt, case.data)
|
||||||
|
|
||||||
# compare the resulting cap to the expected result
|
# compare the resulting cap to the expected result
|
||||||
expected = vectors.capabilities[case]
|
|
||||||
assert actual == expected
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@ -130,13 +130,27 @@ async def test_generate(reactor, request, alice):
|
|||||||
OBJECT_DESCRIPTIONS,
|
OBJECT_DESCRIPTIONS,
|
||||||
FORMATS,
|
FORMATS,
|
||||||
))
|
))
|
||||||
results = generate(reactor, request, alice, space)
|
iterresults = generate(reactor, request, alice, space)
|
||||||
vectors.DATA_PATH.setContent(safe_dump({
|
|
||||||
"version": "2023-01-12",
|
# Update the output file with results as they become available.
|
||||||
|
results = []
|
||||||
|
async for result in iterresults:
|
||||||
|
results.append(result)
|
||||||
|
write_results(vectors.DATA_PATH, results)
|
||||||
|
|
||||||
|
def write_results(path: FilePath, results: list[tuple[Case, str]]) -> None:
|
||||||
|
"""
|
||||||
|
Save the given results.
|
||||||
|
"""
|
||||||
|
path.setContent(safe_dump({
|
||||||
|
"version": vectors.CURRENT_VERSION,
|
||||||
"vector": [
|
"vector": [
|
||||||
{
|
{
|
||||||
"convergence": vectors.encode_bytes(case.convergence),
|
"convergence": vectors.encode_bytes(case.convergence),
|
||||||
"format": case.fmt,
|
"format": {
|
||||||
|
"kind": case.fmt.kind,
|
||||||
|
"params": case.fmt.to_json(),
|
||||||
|
},
|
||||||
"sample": {
|
"sample": {
|
||||||
"seed": vectors.encode_bytes(case.seed_data.seed),
|
"seed": vectors.encode_bytes(case.seed_data.seed),
|
||||||
"length": case.seed_data.length,
|
"length": case.seed_data.length,
|
||||||
@ -148,12 +162,11 @@ async def test_generate(reactor, request, alice):
|
|||||||
},
|
},
|
||||||
"expected": cap,
|
"expected": cap,
|
||||||
}
|
}
|
||||||
async for (case, cap)
|
for (case, cap)
|
||||||
in results
|
in results
|
||||||
],
|
],
|
||||||
}).encode("ascii"))
|
}).encode("ascii"))
|
||||||
|
|
||||||
|
|
||||||
async def generate(
|
async def generate(
|
||||||
reactor,
|
reactor,
|
||||||
request,
|
request,
|
||||||
@ -189,5 +202,7 @@ async def generate(
|
|||||||
case.convergence
|
case.convergence
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Give the format a chance to make an RSA key if it needs it.
|
||||||
|
case = evolve(case, fmt=case.fmt.customize())
|
||||||
cap = upload(alice, case.fmt, case.data)
|
cap = upload(alice, case.fmt, case.data)
|
||||||
yield case, cap
|
yield case, cap
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,11 @@
|
|||||||
General functionality useful for the implementation of integration tests.
|
General functionality useful for the implementation of integration tests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
from typing import TypeVar, Iterator, Awaitable, Callable
|
from typing import TypeVar, Iterator, Awaitable, Callable
|
||||||
|
from typing_extensions import Literal
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@ -21,8 +25,17 @@ from twisted.internet.protocol import ProcessProtocol
|
|||||||
from twisted.internet.error import ProcessExitedAlready, ProcessDone
|
from twisted.internet.error import ProcessExitedAlready, ProcessDone
|
||||||
from twisted.internet.threads import deferToThread
|
from twisted.internet.threads import deferToThread
|
||||||
|
|
||||||
|
from attrs import frozen, evolve
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives.serialization import (
|
||||||
|
Encoding,
|
||||||
|
PrivateFormat,
|
||||||
|
NoEncryption,
|
||||||
|
)
|
||||||
|
|
||||||
from paramiko.rsakey import RSAKey
|
from paramiko.rsakey import RSAKey
|
||||||
from boltons.funcutils import wraps
|
from boltons.funcutils import wraps
|
||||||
|
|
||||||
@ -225,7 +238,7 @@ class TahoeProcess(object):
|
|||||||
|
|
||||||
def restart_async(self, reactor, request):
|
def restart_async(self, reactor, request):
|
||||||
d = self.kill_async()
|
d = self.kill_async()
|
||||||
d.addCallback(lambda ignored: _run_node(reactor, self.node_dir, request, None))
|
d.addCallback(lambda ignored: _run_node(reactor, self.node_dir, request, None, finalize=False))
|
||||||
def got_new_process(proc):
|
def got_new_process(proc):
|
||||||
self._process_transport = proc.transport
|
self._process_transport = proc.transport
|
||||||
d.addCallback(got_new_process)
|
d.addCallback(got_new_process)
|
||||||
@ -603,8 +616,76 @@ def run_in_thread(f):
|
|||||||
return deferToThread(lambda: f(*args, **kwargs))
|
return deferToThread(lambda: f(*args, **kwargs))
|
||||||
return test
|
return test
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
class CHK:
|
||||||
|
"""
|
||||||
|
Represent the CHK encoding sufficiently to run a ``tahoe put`` command
|
||||||
|
using it.
|
||||||
|
"""
|
||||||
|
kind = "chk"
|
||||||
|
max_shares = 256
|
||||||
|
|
||||||
def upload(alice: TahoeProcess, fmt: str, data: bytes) -> str:
|
def customize(self) -> CHK:
|
||||||
|
# Nothing to do.
|
||||||
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, params: None) -> CHK:
|
||||||
|
assert params is None
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
def to_json(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def to_argv(self) -> None:
|
||||||
|
yield []
|
||||||
|
|
||||||
|
@frozen
|
||||||
|
class SSK:
|
||||||
|
"""
|
||||||
|
Represent the SSK encodings (SDMF and MDMF) sufficiently to run a
|
||||||
|
``tahoe put`` command using one of them.
|
||||||
|
"""
|
||||||
|
kind = "ssk"
|
||||||
|
|
||||||
|
# SDMF and MDMF encode share counts (N and k) into the share itself as an
|
||||||
|
# unsigned byte. They could have encoded (share count - 1) to fit the
|
||||||
|
# full range supported by ZFEC into the unsigned byte - but they don't.
|
||||||
|
# So 256 is inaccessible to those formats and we set the upper bound at
|
||||||
|
# 255.
|
||||||
|
max_shares = 255
|
||||||
|
|
||||||
|
name: Literal["sdmf", "mdmf"]
|
||||||
|
key: None | bytes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, params: dict) -> SSK:
|
||||||
|
assert params.keys() == {"format", "mutable", "key"}
|
||||||
|
return cls(params["format"], params["key"].encode("ascii"))
|
||||||
|
|
||||||
|
def customize(self) -> SSK:
|
||||||
|
"""
|
||||||
|
Return an SSK with a newly generated random RSA key.
|
||||||
|
"""
|
||||||
|
return evolve(self, key=generate_rsa_key())
|
||||||
|
|
||||||
|
def to_json(self) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"format": self.name,
|
||||||
|
"mutable": None,
|
||||||
|
"key": self.key.decode("ascii"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def to_argv(self) -> None:
|
||||||
|
with NamedTemporaryFile() as f:
|
||||||
|
f.write(self.key)
|
||||||
|
f.flush()
|
||||||
|
yield [f"--format={self.name}", "--mutable", f"--private-key-path={f.name}"]
|
||||||
|
|
||||||
|
|
||||||
|
def upload(alice: TahoeProcess, fmt: CHK | SSK, data: bytes) -> str:
|
||||||
"""
|
"""
|
||||||
Upload the given data to the given node.
|
Upload the given data to the given node.
|
||||||
|
|
||||||
@ -616,11 +697,13 @@ def upload(alice: TahoeProcess, fmt: str, data: bytes) -> str:
|
|||||||
|
|
||||||
:return: The capability for the uploaded data.
|
:return: The capability for the uploaded data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with NamedTemporaryFile() as f:
|
with NamedTemporaryFile() as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
f.flush()
|
f.flush()
|
||||||
return cli(alice, "put", f"--format={fmt}", f.name).decode("utf-8").strip()
|
with fmt.to_argv() as fmt_argv:
|
||||||
|
argv = [alice, "put"] + fmt_argv + [f.name]
|
||||||
|
return cli(*argv).decode("utf-8").strip()
|
||||||
|
|
||||||
α = TypeVar("α")
|
α = TypeVar("α")
|
||||||
β = TypeVar("β")
|
β = TypeVar("β")
|
||||||
@ -707,3 +790,18 @@ async def reconfigure(reactor, request, node: TahoeProcess, params: tuple[int, i
|
|||||||
print("Ready.")
|
print("Ready.")
|
||||||
else:
|
else:
|
||||||
print("Config unchanged, not restarting.")
|
print("Config unchanged, not restarting.")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_rsa_key() -> bytes:
|
||||||
|
"""
|
||||||
|
Generate a 2048 bit RSA key suitable for use with SSKs.
|
||||||
|
"""
|
||||||
|
return rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=2048,
|
||||||
|
backend=default_backend()
|
||||||
|
).private_bytes(
|
||||||
|
encoding=Encoding.PEM,
|
||||||
|
format=PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=NoEncryption(),
|
||||||
|
)
|
||||||
|
@ -3,7 +3,7 @@ A module that loads pre-generated test vectors.
|
|||||||
|
|
||||||
:ivar DATA_PATH: The path of the file containing test vectors.
|
:ivar DATA_PATH: The path of the file containing test vectors.
|
||||||
|
|
||||||
:ivar capabilities: The CHK test vectors.
|
:ivar capabilities: The capability test vectors.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -11,12 +11,16 @@ from __future__ import annotations
|
|||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
from attrs import frozen
|
from attrs import frozen
|
||||||
from yaml import safe_load
|
from yaml import safe_load
|
||||||
from pathlib import Path
|
|
||||||
from base64 import b64encode, b64decode
|
from base64 import b64encode, b64decode
|
||||||
|
|
||||||
from twisted.python.filepath import FilePath
|
from twisted.python.filepath import FilePath
|
||||||
|
|
||||||
DATA_PATH: FilePath = FilePath(__file__).sibling("test_vectors.yaml")
|
from .util import CHK, SSK
|
||||||
|
|
||||||
|
DATA_PATH: FilePath = FilePath(__file__).sibling("vectors").child("test_vectors.yaml")
|
||||||
|
|
||||||
|
# The version of the persisted test vector data this code can interpret.
|
||||||
|
CURRENT_VERSION: str = "2023-01-16.2"
|
||||||
|
|
||||||
@frozen
|
@frozen
|
||||||
class Sample:
|
class Sample:
|
||||||
@ -42,16 +46,6 @@ class Param:
|
|||||||
# dealing with.
|
# dealing with.
|
||||||
MAX_SHARES = "max"
|
MAX_SHARES = "max"
|
||||||
|
|
||||||
# SDMF and MDMF encode share counts (N and k) into the share itself as an
|
|
||||||
# unsigned byte. They could have encoded (share count - 1) to fit the full
|
|
||||||
# range supported by ZFEC into the unsigned byte - but they don't. So 256 is
|
|
||||||
# inaccessible to those formats and we set the upper bound at 255.
|
|
||||||
MAX_SHARES_MAP = {
|
|
||||||
"chk": 256,
|
|
||||||
"sdmf": 255,
|
|
||||||
"mdmf": 255,
|
|
||||||
}
|
|
||||||
|
|
||||||
@frozen
|
@frozen
|
||||||
class SeedParam:
|
class SeedParam:
|
||||||
"""
|
"""
|
||||||
@ -86,7 +80,7 @@ class Case:
|
|||||||
seed_params: Param
|
seed_params: Param
|
||||||
convergence: bytes
|
convergence: bytes
|
||||||
seed_data: Sample
|
seed_data: Sample
|
||||||
fmt: str
|
fmt: CHK | SSK
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
@ -94,7 +88,7 @@ class Case:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def params(self):
|
def params(self):
|
||||||
return self.seed_params.realize(MAX_SHARES_MAP[self.fmt])
|
return self.seed_params.realize(self.fmt.max_shares)
|
||||||
|
|
||||||
|
|
||||||
def encode_bytes(b: bytes) -> str:
|
def encode_bytes(b: bytes) -> str:
|
||||||
@ -125,16 +119,32 @@ def stretch(seed: bytes, size: int) -> bytes:
|
|||||||
return (seed * multiples)[:size]
|
return (seed * multiples)[:size]
|
||||||
|
|
||||||
|
|
||||||
|
def load_format(serialized: dict) -> CHK | SSK:
|
||||||
|
if serialized["kind"] == "chk":
|
||||||
|
return CHK.load(serialized["params"])
|
||||||
|
elif serialized["kind"] == "ssk":
|
||||||
|
return SSK.load(serialized["params"])
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unrecognized format: {serialized}")
|
||||||
|
|
||||||
|
|
||||||
def load_capabilities(f: TextIO) -> dict[Case, str]:
|
def load_capabilities(f: TextIO) -> dict[Case, str]:
|
||||||
data = safe_load(f)
|
data = safe_load(f)
|
||||||
if data is None:
|
if data is None:
|
||||||
return {}
|
return {}
|
||||||
|
if data["version"] != CURRENT_VERSION:
|
||||||
|
print(
|
||||||
|
f"Current version is {CURRENT_VERSION}; "
|
||||||
|
"cannot load version {data['version']} data."
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Case(
|
Case(
|
||||||
seed_params=SeedParam(case["zfec"]["required"], case["zfec"]["total"]),
|
seed_params=SeedParam(case["zfec"]["required"], case["zfec"]["total"]),
|
||||||
convergence=decode_bytes(case["convergence"]),
|
convergence=decode_bytes(case["convergence"]),
|
||||||
seed_data=Sample(decode_bytes(case["sample"]["seed"]), case["sample"]["length"]),
|
seed_data=Sample(decode_bytes(case["sample"]["seed"]), case["sample"]["length"]),
|
||||||
fmt=case["format"],
|
fmt=load_format(case["format"]),
|
||||||
): case["expected"]
|
): case["expected"]
|
||||||
for case
|
for case
|
||||||
in data["vector"]
|
in data["vector"]
|
||||||
|
16202
integration/vectors/test_vectors.yaml
Executable file
16202
integration/vectors/test_vectors.yaml
Executable file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user