mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-22 14:22:25 +00:00
Expose the pre-constructed keypair functionality to the HTTP API
This commit is contained in:
parent
c7bb190290
commit
3423bfb351
@ -32,6 +32,7 @@ from allmydata.storage.server import StorageServer, FoolscapStorageServer
|
||||
from allmydata import storage_client
|
||||
from allmydata.immutable.upload import Uploader
|
||||
from allmydata.immutable.offloaded import Helper
|
||||
from allmydata.mutable.filenode import MutableFileNode
|
||||
from allmydata.introducer.client import IntroducerClient
|
||||
from allmydata.util import (
|
||||
hashutil, base32, pollmixin, log, idlib,
|
||||
@ -1086,9 +1087,38 @@ class _Client(node.Node, pollmixin.PollMixin):
|
||||
def create_immutable_dirnode(self, children, convergence=None):
|
||||
return self.nodemaker.create_immutable_directory(children, convergence)
|
||||
|
||||
def create_mutable_file(self, contents=None, version=None):
|
||||
def create_mutable_file(
|
||||
self,
|
||||
contents: bytes | None = None,
|
||||
version: int | None = None,
|
||||
*,
|
||||
unique_keypair: tuple[rsa.PublicKey, rsa.PrivateKey] | None = None,
|
||||
) -> MutableFileNode:
|
||||
"""
|
||||
Create *and upload* a new mutable object.
|
||||
|
||||
:param contents: If given, the initial contents for the new object.
|
||||
|
||||
:param version: If given, the mutable file format for the new object
|
||||
(otherwise a format will be chosen automatically).
|
||||
|
||||
:param unique_keypair: **Warning** This valuely independently
|
||||
determines the identity of the mutable object to create. There
|
||||
cannot be two different mutable objects that share a keypair.
|
||||
They will merge into one object (with undefined contents).
|
||||
|
||||
It is not common to pass a non-None value for this parameter. If
|
||||
None is given then a new random keypair will be generated.
|
||||
|
||||
If non-None, the given public/private keypair will be used for the
|
||||
new object.
|
||||
|
||||
:return: A Deferred which will fire with a representation of the new
|
||||
mutable object after it has been uploaded.
|
||||
"""
|
||||
return self.nodemaker.create_mutable_file(contents,
|
||||
version=version)
|
||||
version=version,
|
||||
keypair=unique_keypair)
|
||||
|
||||
def upload(self, uploadable, reactor=None):
|
||||
uploader = self.getServiceNamed("uploader")
|
||||
|
@ -105,6 +105,7 @@ from allmydata.scripts.common import (
|
||||
|
||||
from ..crypto import (
|
||||
ed25519,
|
||||
rsa,
|
||||
)
|
||||
from .eliotutil import (
|
||||
EliotLoggedRunTest,
|
||||
@ -622,15 +623,28 @@ class FakeMutableFileNode(object): # type: ignore # incomplete implementation
|
||||
|
||||
MUTABLE_SIZELIMIT = 10000
|
||||
|
||||
def __init__(self, storage_broker, secret_holder,
|
||||
default_encoding_parameters, history, all_contents):
|
||||
_public_key: rsa.PublicKey | None
|
||||
_private_key: rsa.PrivateKey | None
|
||||
|
||||
def __init__(self,
|
||||
storage_broker,
|
||||
secret_holder,
|
||||
default_encoding_parameters,
|
||||
history,
|
||||
all_contents,
|
||||
keypair: tuple[rsa.PublicKey, rsa.PrivateKey] | None
|
||||
):
|
||||
self.all_contents = all_contents
|
||||
self.file_types = {} # storage index => MDMF_VERSION or SDMF_VERSION
|
||||
self.init_from_cap(make_mutable_file_cap())
|
||||
self.init_from_cap(make_mutable_file_cap(keypair))
|
||||
self._k = default_encoding_parameters['k']
|
||||
self._segsize = default_encoding_parameters['max_segment_size']
|
||||
def create(self, contents, key_generator=None, keysize=None,
|
||||
version=SDMF_VERSION):
|
||||
if keypair is None:
|
||||
self._public_key = self._private_key = None
|
||||
else:
|
||||
self._public_key, self._private_key = keypair
|
||||
|
||||
def create(self, contents, version=SDMF_VERSION):
|
||||
if version == MDMF_VERSION and \
|
||||
isinstance(self.my_uri, (uri.ReadonlySSKFileURI,
|
||||
uri.WriteableSSKFileURI)):
|
||||
@ -826,9 +840,28 @@ class FakeMutableFileNode(object): # type: ignore # incomplete implementation
|
||||
return defer.succeed(consumer)
|
||||
|
||||
|
||||
def make_mutable_file_cap():
|
||||
return uri.WriteableSSKFileURI(writekey=os.urandom(16),
|
||||
fingerprint=os.urandom(32))
|
||||
def make_mutable_file_cap(
|
||||
keypair: tuple[rsa.PublicKey, rsa.PrivateKey] | None = None,
|
||||
) -> uri.WriteableSSKFileURI:
|
||||
"""
|
||||
Create a local representation of a mutable object.
|
||||
|
||||
:param keypair: If None, a random keypair will be generated for the new
|
||||
object. Otherwise, this is the keypair for that object.
|
||||
"""
|
||||
if keypair is None:
|
||||
writekey = os.urandom(16)
|
||||
fingerprint = os.urandom(32)
|
||||
else:
|
||||
pubkey, privkey = keypair
|
||||
pubkey_s = rsa.der_string_from_verifying_key(pubkey)
|
||||
privkey_s = rsa.der_string_from_signing_key(privkey)
|
||||
writekey = hashutil.ssk_writekey_hash(privkey_s)
|
||||
fingerprint = hashutil.ssk_pubkey_fingerprint_hash(pubkey_s)
|
||||
|
||||
return uri.WriteableSSKFileURI(
|
||||
writekey=writekey, fingerprint=fingerprint,
|
||||
)
|
||||
|
||||
def make_mdmf_mutable_file_cap():
|
||||
return uri.WriteableMDMFFileURI(writekey=os.urandom(16),
|
||||
@ -858,7 +891,7 @@ def create_mutable_filenode(contents, mdmf=False, all_contents=None):
|
||||
encoding_params['max_segment_size'] = 128*1024
|
||||
|
||||
filenode = FakeMutableFileNode(None, None, encoding_params, None,
|
||||
all_contents)
|
||||
all_contents, None)
|
||||
filenode.init_from_cap(cap)
|
||||
if mdmf:
|
||||
filenode.create(MutableData(contents), version=MDMF_VERSION)
|
||||
|
@ -8,6 +8,7 @@ from six import ensure_binary
|
||||
import os.path, re, time
|
||||
import treq
|
||||
from urllib.parse import quote as urlquote, unquote as urlunquote
|
||||
from base64 import urlsafe_b64encode
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
@ -32,6 +33,7 @@ from allmydata.util import fileutil, base32, hashutil, jsonbytes as json
|
||||
from allmydata.util.consumer import download_to_data
|
||||
from allmydata.util.encodingutil import to_bytes
|
||||
from ...util.connection_status import ConnectionStatus
|
||||
from ...crypto.rsa import PublicKey, PrivateKey, create_signing_keypair, der_string_from_signing_key
|
||||
from ..common import (
|
||||
EMPTY_CLIENT_CONFIG,
|
||||
FakeCHKFileNode,
|
||||
@ -59,6 +61,7 @@ from allmydata.interfaces import (
|
||||
MustBeReadonlyError,
|
||||
)
|
||||
from allmydata.mutable import servermap, publish, retrieve
|
||||
from allmydata.mutable.common import derive_mutable_keys
|
||||
from .. import common_util as testutil
|
||||
from ..common_util import TimezoneMixin
|
||||
from ..common_web import (
|
||||
@ -94,14 +97,19 @@ class FakeNodeMaker(NodeMaker):
|
||||
def _create_mutable(self, cap):
|
||||
return FakeMutableFileNode(None, None,
|
||||
self.encoding_params, None,
|
||||
self.all_contents).init_from_cap(cap)
|
||||
def create_mutable_file(self, contents=b"", keysize=None,
|
||||
version=SDMF_VERSION,
|
||||
keypair=None,
|
||||
self.all_contents, None).init_from_cap(cap)
|
||||
def create_mutable_file(self,
|
||||
contents=None,
|
||||
version=None,
|
||||
keypair: tuple[PublicKey, PrivateKey] | None=None,
|
||||
):
|
||||
assert keypair is None, "FakeNodeMaker does not support externally supplied keypairs"
|
||||
if contents is None:
|
||||
contents = b""
|
||||
if version is None:
|
||||
version = SDMF_VERSION
|
||||
|
||||
n = FakeMutableFileNode(None, None, self.encoding_params, None,
|
||||
self.all_contents)
|
||||
self.all_contents, keypair)
|
||||
return n.create(contents, version=version)
|
||||
|
||||
class FakeUploader(service.Service):
|
||||
@ -2865,6 +2873,37 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
||||
"Unknown format: foo",
|
||||
method="post", data=body, headers=headers)
|
||||
|
||||
async def test_POST_upload_keypair(self) -> None:
|
||||
"""
|
||||
A *POST* creating a new mutable object may include a *private-key*
|
||||
query argument giving a urlsafe-base64-encoded RSA private key to use
|
||||
as the "signature key". The given signature key is used, rather than
|
||||
a new one being generated.
|
||||
"""
|
||||
format = "sdmf"
|
||||
priv, pub = create_signing_keypair(2048)
|
||||
encoded_privkey = urlsafe_b64encode(der_string_from_signing_key(priv)).decode("ascii")
|
||||
filename = "predetermined-sdmf"
|
||||
actual_cap = uri.from_string(await self.POST(
|
||||
self.public_url +
|
||||
f"/foo?t=upload&format={format}&private-key={encoded_privkey}",
|
||||
file=(filename, self.NEWFILE_CONTENTS * 100),
|
||||
))
|
||||
# Ideally we would inspect the private ("signature") and public
|
||||
# ("verification") keys but they are not made easily accessible here
|
||||
# (ostensibly because we have a FakeMutableFileNode instead of a real
|
||||
# one).
|
||||
#
|
||||
# So, instead, re-compute the writekey and fingerprint and compare
|
||||
# those against the capability string.
|
||||
expected_writekey, _, expected_fingerprint = derive_mutable_keys((pub, priv))
|
||||
self.assertEqual(
|
||||
(expected_writekey, expected_fingerprint),
|
||||
(actual_cap.writekey, actual_cap.fingerprint),
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_POST_upload_format(self):
|
||||
def _check_upload(ign, format, uri_prefix, fn=None):
|
||||
filename = format + ".txt"
|
||||
|
@ -3,8 +3,10 @@ Ported to Python 3.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from base64 import urlsafe_b64decode
|
||||
|
||||
from twisted.web import http, static
|
||||
from twisted.web.iweb import IRequest
|
||||
from twisted.internet import defer
|
||||
from twisted.web.resource import (
|
||||
Resource,
|
||||
@ -45,6 +47,19 @@ from allmydata.web.check_results import (
|
||||
)
|
||||
from allmydata.web.info import MoreInfo
|
||||
from allmydata.util import jsonbytes as json
|
||||
from allmydata.crypto.rsa import PrivateKey, PublicKey, create_signing_keypair_from_string
|
||||
|
||||
|
||||
def get_keypair(request: IRequest) -> tuple[PublicKey, PrivateKey] | None:
|
||||
"""
|
||||
Load a keypair from a urlsafe-base64-encoded RSA private key in the
|
||||
**private-key** argument of the given request, if there is one.
|
||||
"""
|
||||
privkey_der = get_arg(request, "private-key", None)
|
||||
if privkey_der is None:
|
||||
return None
|
||||
privkey, pubkey = create_signing_keypair_from_string(urlsafe_b64decode(privkey_der))
|
||||
return pubkey, privkey
|
||||
|
||||
|
||||
class ReplaceMeMixin(object):
|
||||
@ -54,7 +69,8 @@ class ReplaceMeMixin(object):
|
||||
mutable_type = get_mutable_type(file_format)
|
||||
if mutable_type is not None:
|
||||
data = MutableFileHandle(req.content)
|
||||
d = client.create_mutable_file(data, version=mutable_type)
|
||||
keypair = get_keypair(req)
|
||||
d = client.create_mutable_file(data, version=mutable_type, unique_keypair=keypair)
|
||||
def _uploaded(newnode):
|
||||
d2 = self.parentnode.set_node(self.name, newnode,
|
||||
overwrite=replace)
|
||||
@ -96,7 +112,8 @@ class ReplaceMeMixin(object):
|
||||
if file_format in ("SDMF", "MDMF"):
|
||||
mutable_type = get_mutable_type(file_format)
|
||||
uploadable = MutableFileHandle(contents.file)
|
||||
d = client.create_mutable_file(uploadable, version=mutable_type)
|
||||
keypair = get_keypair(req)
|
||||
d = client.create_mutable_file(uploadable, version=mutable_type, unique_keypair=keypair)
|
||||
def _uploaded(newnode):
|
||||
d2 = self.parentnode.set_node(self.name, newnode,
|
||||
overwrite=replace)
|
||||
|
Loading…
Reference in New Issue
Block a user