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:
@ -4,3 +4,5 @@
|
||||
gerrit:
|
||||
config-repos:
|
||||
- common-config
|
||||
project-repos:
|
||||
- org/project
|
||||
|
14
tests/fixtures/public.pem
vendored
Normal file
14
tests/fixtures/public.pem
vendored
Normal 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-----
|
@ -15,11 +15,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
from six.moves import urllib
|
||||
|
||||
from tests.base import ZuulTestCase
|
||||
from tests.base import ZuulTestCase, FIXTURE_DIR
|
||||
|
||||
|
||||
class TestWebapp(ZuulTestCase):
|
||||
@ -85,3 +86,13 @@ class TestWebapp(ZuulTestCase):
|
||||
|
||||
self.assertEqual(1, len(data), 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
88
tools/encrypt_secret.py
Normal 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()
|
@ -22,6 +22,7 @@ import time
|
||||
from paste import httpserver
|
||||
import webob
|
||||
from webob import dec
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
"""Zuul main web app.
|
||||
|
||||
@ -34,6 +35,7 @@ The supported urls are:
|
||||
queue / pipeline structure of the system
|
||||
- /status.json (backwards compatibility): same as /status
|
||||
- /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
|
||||
array of changes, they will not include the queue structure.
|
||||
@ -96,9 +98,34 @@ class WebApp(threading.Thread):
|
||||
return m.group(1)
|
||||
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):
|
||||
tenant_name = request.path.split('/')[1]
|
||||
path = request.path.replace('/' + tenant_name, '')
|
||||
if path.startswith('/keys'):
|
||||
return self._handle_keys(request, path)
|
||||
path = self._normalize_path(path)
|
||||
if path is None:
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
Reference in New Issue
Block a user