Skip to content

Commit

Permalink
Added more detail on how to generate the tokens, and a module to make…
Browse files Browse the repository at this point in the history
… it easy to do.
  • Loading branch information
pelson committed May 2, 2018
1 parent ea491a9 commit a621754
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 29 deletions.
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ $ conda-upload-server -d /path/to/channel
To test the webserver:

```
curl -F 'artifact=@/path/to/conda/binary/package.tar.bz2' --fail http://localhost:8080/
$ curl -F 'artifact=@/path/to/conda/binary/package.tar.bz2' --fail http://localhost:8080/
```

This will create a conda channel in /path/to/channel/<package-platform>/, with the
Expand All @@ -48,20 +48,31 @@ appropriate channel (``repodata.json``) content.

### Secure Usage Example

For example, to start a secure webserver running on port 9999 that will write
linux-64 packages to the (imaginary) conda channel at ``/path/to/conda/channel``:
First, we need to generate the token that must be kept secret on the server.
We start with a secret held by the client, and optionally a salt kept by the server:

```
$ conda-upload-server -d /path/to/conda/channel/linux-64 \
$ python -m conda_upload_server.token the_client_secret_token
Server salt: "42679ad04d44c96ed27470c02bfb28c3"
Client token: "the_client_secret_token"
Server token: "72a61a0d67edd573649354adc1fce4b7e1f56add1d03ddc328fa032060c8373f"
```

Now we start a secure webserver running on port 9999 that will write
linux-64 packages to the conda channel at ``/path/to/conda/channel/linux-64``:

```
$ conda-upload-server -d /path/to/conda/channel/ \
-p 9999 \
-e /path/to/env/bin/conda \
-c /path/to/mycertfile.crt \
-k /path/to/mykeyfile.key \
-t eccd989b
-t 72a61a0d67edd573649354adc1fce4b7e1f56add1d03ddc328fa032060c8373f
```

To test the webserver:
To test the webserver using python and requests:

```python
import requests
Expand All @@ -71,7 +82,7 @@ artifacts = [open('my-artifact1-1.0.0-2.tar.bz2', 'rb'),
open('my-artifact3-0.9.1-0.tar.bz2', 'rb'),
]
url = 'https://localhost:9999/'
token = 'mysecuretoken'
token = 'the_client_secret_token'

for artifact in artifacts:
requests.post(url, data={'token': token}, files={'artifact': artifact},
Expand Down
29 changes: 8 additions & 21 deletions conda_upload_server/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import tornado.web
from tornado.web import RequestHandler, Finish

from .token import DEFAULT_SALT, check_token as _check_token


logger = logging.getLogger(__name__)

Expand All @@ -47,23 +49,6 @@ def initialize(self, write_path, conda_exe, hash_expected):
self.conda_exe = conda_exe
self.hash_expected = hash_expected

def _check_token(self, token):
"""
Check we have been passed the *correct* secure token before uploading
the artifact to the channel.
"""
if self.hash_expected:
salt = '42679ad04d44c96ed27470c02bfb28c3'
to_hash = '{}{}'.format(salt, token)
hash_result = hashlib.sha256(to_hash.encode('utf-8')).hexdigest()
# Reduce the risk of timing analysis attacks.
result = hmac.compare_digest(self.hash_expected, hash_result)
else:
assert token is None
result = True
return result

def _conda_index(self, directory):
"""
Update the package index metadata files in the provided directory.
Expand All @@ -79,14 +64,14 @@ def post(self):
filename = file_data['filename']
body = file_data['body']

f = io.BytesIO(body)
subdir = subdir_of_binary(f)

if not self._check_token(token):
if not _check_token(self.hash_expected, DEFAULT_SALT, token):
self.set_status(401)
logger.info('Unauthorized token request')
raise Finish()

f = io.BytesIO(body)
subdir = subdir_of_binary(f)

directory = os.path.join(self.write_path, subdir)
if not os.path.exists(directory):
logger.info('Creating a new channel at {}'.format(directory))
Expand All @@ -105,6 +90,7 @@ def post(self):


def subdir_of_binary(fh):
"""Given a file handle to a tar.bz2 conda binary, identify the arch"""
subdir = None
with tarfile.open(fileobj=fh, mode="r:bz2") as tar:
fh = tar.extractfile('info/index.json')
Expand Down Expand Up @@ -158,6 +144,7 @@ def main():
keyfile = args.keyfile
token_hash = args.token_hash


if not conda_exe:
conda_exe = distutils.spawn.find_executable("conda")

Expand Down
57 changes: 57 additions & 0 deletions conda_upload_server/token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import argparse
import distutils.spawn
import hashlib
import hmac
import io
import json
import logging
import os
import subprocess
import tarfile


#: A default salt is provided, but it is recommended to use your own.
DEFAULT_SALT = '42679ad04d44c96ed27470c02bfb28c3'


def generate_hash(token, *, salt=DEFAULT_SALT):
to_hash = '{}{}'.format(salt, token)
return hashlib.sha256(to_hash.encode('utf-8')).hexdigest()


def check_token(hash_expected, salt, token):
"""Check we have been passed the correct secure token."""
if hash_expected:
hash_result = generate_hash(salt=salt, token=token)
# Reduce the risk of timing analysis attacks.
result = hmac.compare_digest(hash_expected, hash_result)
else:
assert token is None
result = True
return result


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--salt",
help="The (semi-)secret salt being used by the server",
default=DEFAULT_SALT,
)
parser.add_argument("token",
help="The secret token/password made available to the client in plain-text",
)

args = parser.parse_args()

server_token = generate_hash(salt=args.salt, token=args.token)

print('Server salt: "{}"'.format(args.salt))
print('Client token: "{}"'.format(args.token))
print('Server token: "{}"'.format(server_token))

assert check_token(server_token, args.salt, args.token)



if __name__ == '__main__':
main()

0 comments on commit a621754

Please sign in to comment.