expose the private-key feature in the `tahoe put` cli

This commit is contained in:
Jean-Paul Calderone 2023-01-06 15:40:48 -05:00
parent e236cc95a5
commit 3ff9c45e95
5 changed files with 108 additions and 34 deletions

View File

@ -181,8 +181,19 @@ class PutOptions(FileStoreOptions):
optFlags = [ optFlags = [
("mutable", "m", "Create a mutable file instead of an immutable one (like --format=SDMF)"), ("mutable", "m", "Create a mutable file instead of an immutable one (like --format=SDMF)"),
] ]
optParameters = [ optParameters = [
("format", None, None, "Create a file with the given format: SDMF and MDMF for mutable, CHK (default) for immutable. (case-insensitive)"), ("format", None, None, "Create a file with the given format: SDMF and MDMF for mutable, CHK (default) for immutable. (case-insensitive)"),
("private-key-path", None, None,
"***Warning*** "
"It is possible to use this option to spoil the normal security properties of mutable objects. "
"It is also possible to corrupt or destroy data with this option. "
"For mutables only, "
"this gives a file containing a PEM-encoded 2048 bit RSA private key to use as the signature key for the mutable. "
"The private key must be handled at least as strictly as the resulting capability string. "
"A single private key must not be used for more than one mutable."
),
] ]
def parseArgs(self, arg1=None, arg2=None): def parseArgs(self, arg1=None, arg2=None):

View File

@ -1,23 +1,31 @@
""" """
Ported to Python 3. Implement the ``tahoe put`` command.
""" """
from __future__ import unicode_literals from __future__ import annotations
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from future.utils import PY2
if PY2:
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
from io import BytesIO from io import BytesIO
from urllib.parse import quote as url_quote from urllib.parse import quote as url_quote
from base64 import urlsafe_b64encode
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from twisted.python.filepath import FilePath
from allmydata.crypto.rsa import der_string_from_signing_key
from allmydata.scripts.common_http import do_http, format_http_success, format_http_error from allmydata.scripts.common_http import do_http, format_http_success, format_http_error
from allmydata.scripts.common import get_alias, DEFAULT_ALIAS, escape_path, \ from allmydata.scripts.common import get_alias, DEFAULT_ALIAS, escape_path, \
UnknownAliasError UnknownAliasError
from allmydata.util.encodingutil import quote_output from allmydata.util.encodingutil import quote_output
def load_private_key(path: str) -> str:
"""
Load a private key from a file and return it in a format appropriate
to include in the HTTP request.
"""
privkey = load_pem_private_key(FilePath(path).getContent(), password=None)
derbytes = der_string_from_signing_key(privkey)
return urlsafe_b64encode(derbytes).decode("ascii")
def put(options): def put(options):
""" """
@param verbosity: 0, 1, or 2, meaning quiet, verbose, or very verbose @param verbosity: 0, 1, or 2, meaning quiet, verbose, or very verbose
@ -29,6 +37,10 @@ def put(options):
from_file = options.from_file from_file = options.from_file
to_file = options.to_file to_file = options.to_file
mutable = options['mutable'] mutable = options['mutable']
if options["private-key-path"] is None:
private_key = None
else:
private_key = load_private_key(options["private-key-path"])
format = options['format'] format = options['format']
if options['quiet']: if options['quiet']:
verbosity = 0 verbosity = 0
@ -79,6 +91,12 @@ def put(options):
queryargs = [] queryargs = []
if mutable: if mutable:
queryargs.append("mutable=true") queryargs.append("mutable=true")
if private_key is not None:
queryargs.append(f"private-key={private_key}")
else:
if private_key is not None:
raise Exception("Can only supply a private key for mutables.")
if format: if format:
queryargs.append("format=%s" % format) queryargs.append("format=%s" % format)
if queryargs: if queryargs:
@ -92,9 +110,6 @@ def put(options):
if verbosity > 0: if verbosity > 0:
print("waiting for file data on stdin..", file=stderr) print("waiting for file data on stdin..", file=stderr)
# We're uploading arbitrary files, so this had better be bytes: # We're uploading arbitrary files, so this had better be bytes:
if PY2:
stdinb = stdin
else:
stdinb = stdin.buffer stdinb = stdin.buffer
data = stdinb.read() data = stdinb.read()
infileobj = BytesIO(data) infileobj = BytesIO(data)

View File

@ -1,19 +1,17 @@
""" """
Ported to Python 3. Tests for the ``tahoe put`` CLI tool.
""" """
from __future__ import absolute_import from __future__ import annotations
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from future.utils import PY2
if PY2:
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
from typing import Callable, Awaitable, TypeVar
import os.path import os.path
from twisted.trial import unittest from twisted.trial import unittest
from twisted.python import usage from twisted.python import usage
from twisted.python.filepath import FilePath
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from allmydata.uri import from_string
from allmydata.util import fileutil from allmydata.util import fileutil
from allmydata.scripts.common import get_aliases from allmydata.scripts.common import get_aliases
from allmydata.scripts import cli from allmydata.scripts import cli
@ -22,6 +20,9 @@ from ..common_util import skip_if_cannot_represent_filename
from allmydata.util.encodingutil import get_io_encoding from allmydata.util.encodingutil import get_io_encoding
from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.util.fileutil import abspath_expanduser_unicode
from .common import CLITestMixin from .common import CLITestMixin
from allmydata.mutable.common import derive_mutable_keys
T = TypeVar("T")
class Put(GridTestMixin, CLITestMixin, unittest.TestCase): class Put(GridTestMixin, CLITestMixin, unittest.TestCase):
@ -215,6 +216,59 @@ class Put(GridTestMixin, CLITestMixin, unittest.TestCase):
return d return d
async def test_unlinked_mutable_specified_private_key(self) -> None:
"""
A new unlinked mutable can be created using a specified private
key.
"""
self.basedir = "cli/Put/unlinked-mutable-with-key"
await self._test_mutable_specified_key(
lambda do_cli, pempath, datapath: do_cli(
"put", "--mutable", "--private-key-path", pempath.path,
stdin=datapath.getContent(),
),
)
async def test_linked_mutable_specified_private_key(self) -> None:
"""
A new linked mutable can be created using a specified private key.
"""
self.basedir = "cli/Put/linked-mutable-with-key"
await self._test_mutable_specified_key(
lambda do_cli, pempath, datapath: do_cli(
"put", "--mutable", "--private-key-path", pempath.path, datapath.path,
),
)
async def _test_mutable_specified_key(
self,
run: Callable[[Callable[..., T], FilePath, FilePath], Awaitable[T]],
) -> None:
"""
A helper for testing mutable creation.
:param run: A function to do the creation. It is called with
``self.do_cli`` and the path to a private key PEM file and a data
file. It returns whatever ``do_cli`` returns.
"""
self.set_up_grid(oneshare=True)
pempath = FilePath(__file__).parent().sibling("data").child("openssl-rsa-2048.txt")
datapath = FilePath(self.basedir).child("data")
datapath.setContent(b"Hello world" * 1024)
(rc, out, err) = await run(self.do_cli, pempath, datapath)
self.assertEqual(rc, 0, (out, err))
cap = from_string(out.strip())
# The capability is derived from the key we specified.
privkey = load_pem_private_key(pempath.getContent(), password=None)
pubkey = privkey.public_key()
writekey, _, fingerprint = derive_mutable_keys((pubkey, privkey))
self.assertEqual(
(writekey, fingerprint),
(cap.writekey, cap.fingerprint),
)
def test_mutable(self): def test_mutable(self):
# echo DATA1 | tahoe put --mutable - uploaded.txt # echo DATA1 | tahoe put --mutable - uploaded.txt
# echo DATA2 | tahoe put - uploaded.txt # should modify-in-place # echo DATA2 | tahoe put - uploaded.txt # should modify-in-place

View File

@ -4,7 +4,6 @@ Ported to Python 3.
from __future__ import annotations from __future__ import annotations
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,

View File

@ -1,14 +1,7 @@
""" """
Ported to Python 3. Ported to Python 3.
""" """
from __future__ import absolute_import from __future__ import annotations
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from future.utils import PY2
if PY2:
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
from urllib.parse import quote as urlquote from urllib.parse import quote as urlquote
@ -25,6 +18,7 @@ from twisted.web.template import (
from allmydata.immutable.upload import FileHandle from allmydata.immutable.upload import FileHandle
from allmydata.mutable.publish import MutableFileHandle from allmydata.mutable.publish import MutableFileHandle
from allmydata.web.common import ( from allmydata.web.common import (
get_keypair,
get_arg, get_arg,
boolean_of_arg, boolean_of_arg,
convert_children_json, convert_children_json,
@ -48,7 +42,8 @@ def PUTUnlinkedSSK(req, client, version):
# SDMF: files are small, and we can only upload data # SDMF: files are small, and we can only upload data
req.content.seek(0) req.content.seek(0)
data = MutableFileHandle(req.content) data = MutableFileHandle(req.content)
d = client.create_mutable_file(data, version=version) keypair = get_keypair(req)
d = client.create_mutable_file(data, version=version, unique_keypair=keypair)
d.addCallback(lambda n: n.get_uri()) d.addCallback(lambda n: n.get_uri())
return d return d