expose the private-key feature in the `tahoe put` cli
This commit is contained in:
parent
e236cc95a5
commit
3ff9c45e95
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue