Browse Source

Serve public keys through webapp

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

Change-Id: If0e4e4f8509518c8440814e8088a343489b5c553
changes/56/446756/8
James E. Blair 4 years ago
parent
commit
c49e5e713f
5 changed files with 143 additions and 1 deletions
  1. +2
    -0
      tests/fixtures/config/single-tenant/main.yaml
  2. +14
    -0
      tests/fixtures/public.pem
  3. +12
    -1
      tests/unit/test_webapp.py
  4. +88
    -0
      tools/encrypt_secret.py
  5. +27
    -0
      zuul/webapp.py

+ 2
- 0
tests/fixtures/config/single-tenant/main.yaml View File

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

+ 14
- 0
tests/fixtures/public.pem 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-----

+ 12
- 1
tests/unit/test_webapp.py View File

@ -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
- 0
tools/encrypt_secret.py 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()

+ 27
- 0
zuul/webapp.py View File

@ -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()


Loading…
Cancel
Save