Expose the pre-constructed keypair functionality to the HTTP API

This commit is contained in:
Jean-Paul Calderone 2023-01-03 11:31:29 -05:00
parent c7bb190290
commit 3423bfb351
4 changed files with 138 additions and 19 deletions

View File

@ -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")

View File

@ -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)

View File

@ -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"

View File

@ -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)