mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-22 22:32:23 +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 import storage_client
|
||||||
from allmydata.immutable.upload import Uploader
|
from allmydata.immutable.upload import Uploader
|
||||||
from allmydata.immutable.offloaded import Helper
|
from allmydata.immutable.offloaded import Helper
|
||||||
|
from allmydata.mutable.filenode import MutableFileNode
|
||||||
from allmydata.introducer.client import IntroducerClient
|
from allmydata.introducer.client import IntroducerClient
|
||||||
from allmydata.util import (
|
from allmydata.util import (
|
||||||
hashutil, base32, pollmixin, log, idlib,
|
hashutil, base32, pollmixin, log, idlib,
|
||||||
@ -1086,9 +1087,38 @@ class _Client(node.Node, pollmixin.PollMixin):
|
|||||||
def create_immutable_dirnode(self, children, convergence=None):
|
def create_immutable_dirnode(self, children, convergence=None):
|
||||||
return self.nodemaker.create_immutable_directory(children, convergence)
|
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,
|
return self.nodemaker.create_mutable_file(contents,
|
||||||
version=version)
|
version=version,
|
||||||
|
keypair=unique_keypair)
|
||||||
|
|
||||||
def upload(self, uploadable, reactor=None):
|
def upload(self, uploadable, reactor=None):
|
||||||
uploader = self.getServiceNamed("uploader")
|
uploader = self.getServiceNamed("uploader")
|
||||||
|
@ -105,6 +105,7 @@ from allmydata.scripts.common import (
|
|||||||
|
|
||||||
from ..crypto import (
|
from ..crypto import (
|
||||||
ed25519,
|
ed25519,
|
||||||
|
rsa,
|
||||||
)
|
)
|
||||||
from .eliotutil import (
|
from .eliotutil import (
|
||||||
EliotLoggedRunTest,
|
EliotLoggedRunTest,
|
||||||
@ -622,15 +623,28 @@ class FakeMutableFileNode(object): # type: ignore # incomplete implementation
|
|||||||
|
|
||||||
MUTABLE_SIZELIMIT = 10000
|
MUTABLE_SIZELIMIT = 10000
|
||||||
|
|
||||||
def __init__(self, storage_broker, secret_holder,
|
_public_key: rsa.PublicKey | None
|
||||||
default_encoding_parameters, history, all_contents):
|
_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.all_contents = all_contents
|
||||||
self.file_types = {} # storage index => MDMF_VERSION or SDMF_VERSION
|
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._k = default_encoding_parameters['k']
|
||||||
self._segsize = default_encoding_parameters['max_segment_size']
|
self._segsize = default_encoding_parameters['max_segment_size']
|
||||||
def create(self, contents, key_generator=None, keysize=None,
|
if keypair is None:
|
||||||
version=SDMF_VERSION):
|
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 \
|
if version == MDMF_VERSION and \
|
||||||
isinstance(self.my_uri, (uri.ReadonlySSKFileURI,
|
isinstance(self.my_uri, (uri.ReadonlySSKFileURI,
|
||||||
uri.WriteableSSKFileURI)):
|
uri.WriteableSSKFileURI)):
|
||||||
@ -826,9 +840,28 @@ class FakeMutableFileNode(object): # type: ignore # incomplete implementation
|
|||||||
return defer.succeed(consumer)
|
return defer.succeed(consumer)
|
||||||
|
|
||||||
|
|
||||||
def make_mutable_file_cap():
|
def make_mutable_file_cap(
|
||||||
return uri.WriteableSSKFileURI(writekey=os.urandom(16),
|
keypair: tuple[rsa.PublicKey, rsa.PrivateKey] | None = None,
|
||||||
fingerprint=os.urandom(32))
|
) -> 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():
|
def make_mdmf_mutable_file_cap():
|
||||||
return uri.WriteableMDMFFileURI(writekey=os.urandom(16),
|
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
|
encoding_params['max_segment_size'] = 128*1024
|
||||||
|
|
||||||
filenode = FakeMutableFileNode(None, None, encoding_params, None,
|
filenode = FakeMutableFileNode(None, None, encoding_params, None,
|
||||||
all_contents)
|
all_contents, None)
|
||||||
filenode.init_from_cap(cap)
|
filenode.init_from_cap(cap)
|
||||||
if mdmf:
|
if mdmf:
|
||||||
filenode.create(MutableData(contents), version=MDMF_VERSION)
|
filenode.create(MutableData(contents), version=MDMF_VERSION)
|
||||||
|
@ -8,6 +8,7 @@ from six import ensure_binary
|
|||||||
import os.path, re, time
|
import os.path, re, time
|
||||||
import treq
|
import treq
|
||||||
from urllib.parse import quote as urlquote, unquote as urlunquote
|
from urllib.parse import quote as urlquote, unquote as urlunquote
|
||||||
|
from base64 import urlsafe_b64encode
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
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.consumer import download_to_data
|
||||||
from allmydata.util.encodingutil import to_bytes
|
from allmydata.util.encodingutil import to_bytes
|
||||||
from ...util.connection_status import ConnectionStatus
|
from ...util.connection_status import ConnectionStatus
|
||||||
|
from ...crypto.rsa import PublicKey, PrivateKey, create_signing_keypair, der_string_from_signing_key
|
||||||
from ..common import (
|
from ..common import (
|
||||||
EMPTY_CLIENT_CONFIG,
|
EMPTY_CLIENT_CONFIG,
|
||||||
FakeCHKFileNode,
|
FakeCHKFileNode,
|
||||||
@ -59,6 +61,7 @@ from allmydata.interfaces import (
|
|||||||
MustBeReadonlyError,
|
MustBeReadonlyError,
|
||||||
)
|
)
|
||||||
from allmydata.mutable import servermap, publish, retrieve
|
from allmydata.mutable import servermap, publish, retrieve
|
||||||
|
from allmydata.mutable.common import derive_mutable_keys
|
||||||
from .. import common_util as testutil
|
from .. import common_util as testutil
|
||||||
from ..common_util import TimezoneMixin
|
from ..common_util import TimezoneMixin
|
||||||
from ..common_web import (
|
from ..common_web import (
|
||||||
@ -94,14 +97,19 @@ class FakeNodeMaker(NodeMaker):
|
|||||||
def _create_mutable(self, cap):
|
def _create_mutable(self, cap):
|
||||||
return FakeMutableFileNode(None, None,
|
return FakeMutableFileNode(None, None,
|
||||||
self.encoding_params, None,
|
self.encoding_params, None,
|
||||||
self.all_contents).init_from_cap(cap)
|
self.all_contents, None).init_from_cap(cap)
|
||||||
def create_mutable_file(self, contents=b"", keysize=None,
|
def create_mutable_file(self,
|
||||||
version=SDMF_VERSION,
|
contents=None,
|
||||||
keypair=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,
|
n = FakeMutableFileNode(None, None, self.encoding_params, None,
|
||||||
self.all_contents)
|
self.all_contents, keypair)
|
||||||
return n.create(contents, version=version)
|
return n.create(contents, version=version)
|
||||||
|
|
||||||
class FakeUploader(service.Service):
|
class FakeUploader(service.Service):
|
||||||
@ -2865,6 +2873,37 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
|||||||
"Unknown format: foo",
|
"Unknown format: foo",
|
||||||
method="post", data=body, headers=headers)
|
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 test_POST_upload_format(self):
|
||||||
def _check_upload(ign, format, uri_prefix, fn=None):
|
def _check_upload(ign, format, uri_prefix, fn=None):
|
||||||
filename = format + ".txt"
|
filename = format + ".txt"
|
||||||
|
@ -3,8 +3,10 @@ Ported to Python 3.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from base64 import urlsafe_b64decode
|
||||||
|
|
||||||
from twisted.web import http, static
|
from twisted.web import http, static
|
||||||
|
from twisted.web.iweb import IRequest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.web.resource import (
|
from twisted.web.resource import (
|
||||||
Resource,
|
Resource,
|
||||||
@ -45,6 +47,19 @@ from allmydata.web.check_results import (
|
|||||||
)
|
)
|
||||||
from allmydata.web.info import MoreInfo
|
from allmydata.web.info import MoreInfo
|
||||||
from allmydata.util import jsonbytes as json
|
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):
|
class ReplaceMeMixin(object):
|
||||||
@ -54,7 +69,8 @@ class ReplaceMeMixin(object):
|
|||||||
mutable_type = get_mutable_type(file_format)
|
mutable_type = get_mutable_type(file_format)
|
||||||
if mutable_type is not None:
|
if mutable_type is not None:
|
||||||
data = MutableFileHandle(req.content)
|
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):
|
def _uploaded(newnode):
|
||||||
d2 = self.parentnode.set_node(self.name, newnode,
|
d2 = self.parentnode.set_node(self.name, newnode,
|
||||||
overwrite=replace)
|
overwrite=replace)
|
||||||
@ -96,7 +112,8 @@ class ReplaceMeMixin(object):
|
|||||||
if file_format in ("SDMF", "MDMF"):
|
if file_format in ("SDMF", "MDMF"):
|
||||||
mutable_type = get_mutable_type(file_format)
|
mutable_type = get_mutable_type(file_format)
|
||||||
uploadable = MutableFileHandle(contents.file)
|
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):
|
def _uploaded(newnode):
|
||||||
d2 = self.parentnode.set_node(self.name, newnode,
|
d2 = self.parentnode.set_node(self.name, newnode,
|
||||||
overwrite=replace)
|
overwrite=replace)
|
||||||
|
Loading…
Reference in New Issue
Block a user