Serve public keys through webapp

Add a utility script which uses the public key served over HTTP
to encrypt the secret.

Change-Id: If0e4e4f8509518c8440814e8088a343489b5c553
This commit is contained in:
James E. Blair 2017-03-16 14:56:32 -07:00
parent 18f86a38a3
commit c49e5e713f
5 changed files with 143 additions and 1 deletions

View File

@ -4,3 +4,5 @@
gerrit: gerrit:
config-repos: config-repos:
- common-config - common-config
project-repos:
- org/project

14
tests/fixtures/public.pem vendored Normal file
View File

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsGqZLUUwV/EZJKddMS20
6mH7qYmqYhWLo/TUlpDt2JuEaBqCYV8mF9LsjpoqM/Pp0U/r5aQLDUXbRLDn+K+N
qbvTJajYxHJicP1CAWg1eKUNZjUaya5HP4Ow1hS7AeiF4TSRdiwtHT/gJO2NSsav
yc30/meKt0WBgbYlrBB81HEQjYWnajf/4so5E8DdrC9tAqmmzde1qcTz7ULouIz5
3hjp/U3yVMFbpawv194jzHvddmAX3aEUByx2t6lP7dhOAEIEmzmh15hRbacxQI5a
YWv+ZR0z9PqdwwD+DBbb1AwiX5MJjtIoVCmkEZvcUFiDicyteNMCa5ulpj2SF0oH
4MlialOP6MiJnmxklDYO07AM/qomcU55pCD8ctu1yD/UydecLk0Uj/9XxqmPQJFE
cstdXJZQfr5ZNnChOEg6oQ9UImWjav8HQsA6mFW1oAKbDMrgEewooWriqGW5pYtR
7JBfph6Mt5HGaeH4uqYpb1fveHG1ODa7HBnlNo3qMendBb2wzHGCgtUgWnGfp24T
sUOUlndCXYhsYbOZbCTW5GwElK0Gri06KPpybY43AIaxcxqilVh5Eapmq7axBm4Z
zbTOfv15L0FIemEGgpnklevQbZNLIrcE0cS/13qJUvFaYX4yjrtEnzZ3ntjXrpFd
gLPBKn7Aqf6lWz6BPi07axECAwEAAQ==
-----END PUBLIC KEY-----

View File

@ -15,11 +15,12 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
import json import json
from six.moves import urllib from six.moves import urllib
from tests.base import ZuulTestCase from tests.base import ZuulTestCase, FIXTURE_DIR
class TestWebapp(ZuulTestCase): class TestWebapp(ZuulTestCase):
@ -85,3 +86,13 @@ class TestWebapp(ZuulTestCase):
self.assertEqual(1, len(data), data) self.assertEqual(1, len(data), data)
self.assertEqual("org/project1", data[0]['project'], data) self.assertEqual("org/project1", data[0]['project'], data)
def test_webapp_keys(self):
with open(os.path.join(FIXTURE_DIR, 'public.pem')) as f:
public_pem = f.read()
req = urllib.request.Request(
"http://localhost:%s/tenant-one/keys/gerrit/org/project.pub" %
self.port)
f = urllib.request.urlopen(req)
self.assertEqual(f.read(), public_pem)

88
tools/encrypt_secret.py Normal file
View File

@ -0,0 +1,88 @@
#!/usr/bin/env python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import os
import subprocess
import sys
import tempfile
from six.moves import urllib
DESCRIPTION = """Encrypt a secret for Zuul.
This program fetches a project-specific public key from a Zuul server and
uses that to encrypt a secret. The only pre-requisite is an installed
OpenSSL binary.
"""
def main():
parser = argparse.ArgumentParser(description=DESCRIPTION)
parser.add_argument('url',
help="The base URL of the zuul server and tenant. "
"E.g., https://zuul.example.com/tenant-name")
# TODO(jeblair,mordred): When projects have canonical names, use that here.
# TODO(jeblair): Throw a fit if SSL is not used.
parser.add_argument('source',
help="The Zuul source of the project.")
parser.add_argument('project',
help="The name of the project.")
parser.add_argument('--infile',
default=None,
help="A filename whose contents will be encrypted. "
"If not supplied, the value will be read from "
"standard input.")
parser.add_argument('--outfile',
default=None,
help="A filename to which the encrypted value will be "
"written. If not supplied, the value will be written "
"to standard output.")
args = parser.parse_args()
req = urllib.request.Request("%s/keys/%s/%s.pub" % (
args.url, args.source, args.project))
pubkey = urllib.request.urlopen(req)
if args.infile:
with open(args.infile) as f:
plaintext = f.read()
else:
plaintext = sys.stdin.read()
pubkey_file = tempfile.NamedTemporaryFile(delete=False)
try:
pubkey_file.write(pubkey.read())
pubkey_file.close()
p = subprocess.Popen(['openssl', 'rsautl', '-encrypt',
'-oaep', '-pubin', '-inkey',
pubkey_file.name],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
(stdout, stderr) = p.communicate(plaintext)
if p.returncode != 0:
raise Exception("Return code %s from openssl" % p.returncode)
ciphertext = stdout.encode('base64')
finally:
os.unlink(pubkey_file.name)
if args.outfile:
with open(args.outfile, "w") as f:
f.write(ciphertext)
else:
print(ciphertext)
if __name__ == '__main__':
main()

View File

@ -22,6 +22,7 @@ import time
from paste import httpserver from paste import httpserver
import webob import webob
from webob import dec from webob import dec
from cryptography.hazmat.primitives import serialization
"""Zuul main web app. """Zuul main web app.
@ -34,6 +35,7 @@ The supported urls are:
queue / pipeline structure of the system queue / pipeline structure of the system
- /status.json (backwards compatibility): same as /status - /status.json (backwards compatibility): same as /status
- /status/change/X,Y: return status just for gerrit change X,Y - /status/change/X,Y: return status just for gerrit change X,Y
- /keys/SOURCE/PROJECT.pub: return the public key for PROJECT
When returning status for a single gerrit change you will get an When returning status for a single gerrit change you will get an
array of changes, they will not include the queue structure. array of changes, they will not include the queue structure.
@ -96,9 +98,34 @@ class WebApp(threading.Thread):
return m.group(1) return m.group(1)
return None return None
def _handle_keys(self, request, path):
m = re.match('/keys/(.*?)/(.*?).pub', path)
if not m:
raise webob.exc.HTTPNotFound()
source_name = m.group(1)
project_name = m.group(2)
source = self.scheduler.connections.getSource(source_name)
if not source:
raise webob.exc.HTTPNotFound()
project = source.getProject(project_name)
if not project:
raise webob.exc.HTTPNotFound()
# Serialize public key
pem_public_key = project.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
response = webob.Response(body=pem_public_key,
content_type='text/plain')
return response.conditional_response_app
def app(self, request): def app(self, request):
tenant_name = request.path.split('/')[1] tenant_name = request.path.split('/')[1]
path = request.path.replace('/' + tenant_name, '') path = request.path.replace('/' + tenant_name, '')
if path.startswith('/keys'):
return self._handle_keys(request, path)
path = self._normalize_path(path) path = self._normalize_path(path)
if path is None: if path is None:
raise webob.exc.HTTPNotFound() raise webob.exc.HTTPNotFound()