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:
parent
18f86a38a3
commit
c49e5e713f
|
@ -4,3 +4,5 @@
|
||||||
gerrit:
|
gerrit:
|
||||||
config-repos:
|
config-repos:
|
||||||
- common-config
|
- common-config
|
||||||
|
project-repos:
|
||||||
|
- org/project
|
||||||
|
|
|
@ -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
|
# 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)
|
||||||
|
|
|
@ -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
|
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()
|
||||||
|
|
Loading…
Reference in New Issue