Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Implement public key authentication #97

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions PUBLIC_KEY_AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
`sudo ./__main__.py --clean --isfrontend --authdomain=./scripts/demo_public_key_auth_app.py`

`./__main__.py --clean --publickeyauth=scripts/example-public-key-auth.sh --frontend=127.0.0.1:80 --service_on=https:test.example.com:localhost:3000:xxx`
528 changes: 0 additions & 528 deletions doc/MANPAGE.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions pagekite/manual.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@
--all __Terminate early if any tunnels fail to register.
--new __Don't attempt to connect to any kites' old front-ends.
--noprobes __Reject all probes for service state.
--publickeyauth</b>=<a>/path/to/script</a> __
Use public key authentication with script

""")
MAN_OPT_FRONTEND = ("""\
--isfrontend __Enable front-end operation.
Expand Down
8 changes: 7 additions & 1 deletion pagekite/pk.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
'logfile=', 'daemonize', 'nodaemonize', 'runas=', 'pidfile=',
'isfrontend', 'noisfrontend', 'settings',
'defaults', 'whitelabel=', 'whitelabels=', 'local=', 'domain=',
'publickeyauth=',
'auththreads=', 'authdomain=', 'authfail_closed',
'motd=', 'register=', 'host=', 'noupgradeinfo', 'upgradeinfo=',
'ports=', 'protos=', 'portalias=', 'rawports=',
Expand Down Expand Up @@ -1180,6 +1181,8 @@ def ResetConfiguration(self):
self.kite_disable = False
self.kite_remove = False

self.public_key_auth = None

# Searching for our configuration file! We prefer the documented
# 'standard' locations, but if nothing is found there and something local
# exists, use that instead.
Expand Down Expand Up @@ -2577,6 +2580,7 @@ def Configure(self, argv):
elif opt == '--defaults': self.SetServiceDefaults()
elif opt == '--whitelabel': self.SetWhitelabelDefaults(arg, secure=False)
elif opt == '--whitelabels': self.SetWhitelabelDefaults(arg, secure=True)
elif opt == '--publickeyauth': self.public_key_auth = arg
elif opt in ('--clean', '--nopyopenssl', '--nossl', '--settings',
'--signup', '--friendly'):
# These are handled outside the main loop, we just ignore them.
Expand Down Expand Up @@ -3566,7 +3570,9 @@ def iping(ip):
def ConnectFrontend(self, conns, server):
self.ui.Status('connect', color=self.ui.YELLOW,
message='Front-end connect: %s' % server)
tun = Tunnel.BackEnd(server, self.backends, self.require_all, conns)

print("PUBLIC_KEY_AUTH: " + self.public_key_auth)
tun = Tunnel.BackEnd(server, self.backends, self.require_all, conns, self.public_key_auth)
if tun:
tun.filters.append(HaproxyProtocolFilter(self.ui))
tun.filters.append(HttpHeaderFilter(self.ui))
Expand Down
4 changes: 2 additions & 2 deletions pagekite/proto/conns.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ def HandlePageKiteResponse(self, parse):

return have_kite_info and have_kites

def _BackEnd(server, backends, require_all, conns):
def _BackEnd(server, backends, require_all, conns, public_key_auth):
"""This is the back-end end of a tunnel."""
self = Tunnel(conns)
if conns and not conns.config.isfrontend:
Expand Down Expand Up @@ -666,7 +666,7 @@ def _BackEnd(server, backends, require_all, conns):
if tryagain:
if self.server_info[self.S_ADD_KITES]:
request = PageKiteRequestHeaders(server, conns.config.backends,
tokens)
tokens, public_key_auth=public_key_auth)
abort = not self.SendChunked(('NOOP: 1\r\n%s\r\n\r\n!'
) % ''.join(request),
compress=False, just_buffer=True)
Expand Down
42 changes: 32 additions & 10 deletions pagekite/proto/proto.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import absolute_import
from __future__ import division


LICENSE = """\
This file is part of pagekite.py.
Copyright 2010-2020, the Beanstalks Project ehf. and Bjarni Runar Einarsson
Expand Down Expand Up @@ -33,6 +34,11 @@
import struct
import time

try:
import subprocess
except ImportError:
subprocess = None

from pagekite.compat import *
from pagekite.common import *
import pagekite.logging as logging
Expand Down Expand Up @@ -66,7 +72,8 @@ def globalSecret():

TOKEN_LENGTH=36
def signToken(token=None, secret=None, payload='', timestamp=None,
length=TOKEN_LENGTH):
length=TOKEN_LENGTH, public_key_auth=None):

"""
This will generate a random token with a signature which could only have come
from this server. If a token is provided, it is re-signed so the original
Expand All @@ -75,17 +82,31 @@ def signToken(token=None, secret=None, payload='', timestamp=None,
If a timestamp is provided it will be embedded in the signature to a
resolution of 10 minutes, and the signature will begin with the letter 't'

If the path to a script is passed in public_key_auth, this script will be called
with payload + ts as parameter and the output of the script will be returned.

Note: This is only as secure as random.randint() is random.
"""
if not secret: secret = globalSecret()
if not token: token = sha1hex('%s%8.8x' % (globalSecret(),
random.randint(0, 0x7FFFFFFD)+1))
if timestamp:
tok = 't' + token[1:]
ts = '%x' % int(timestamp/600) # Integer division
return tok[0:8] + sha1hex(secret + payload + ts + tok[0:8])[0:length-8]


if public_key_auth:
signText = payload
if timestamp:
ts = '%x' % int(timestamp / 600) # Integer division
signText += ts
result = subprocess.check_output([public_key_auth, signText]).strip()
else:
return token[0:8] + sha1hex(secret + payload + token[0:8])[0:length-8]
if not token: token = sha1hex('%s%8.8x' % (globalSecret(),
random.randint(0, 0x7FFFFFFD)+1))
if timestamp:
tok = 't' + token[1:]
ts = '%x' % int(timestamp/600) # Integer division
result = tok[0:8] + sha1hex(secret + payload + ts + tok[0:8])[0:length-8]
else:
result = token[0:8] + sha1hex(secret + payload + token[0:8])[0:length-8]

return result

def checkSignature(sign='', secret='', payload=''):
"""
Expand All @@ -103,7 +124,7 @@ def checkSignature(sign='', secret='', payload=''):
valid = signToken(token=sign, secret=secret, payload=payload)
return sign == valid

def PageKiteRequestHeaders(server, backends, tokens=None, testtoken=None, replace=None):
def PageKiteRequestHeaders(server, backends, tokens=None, testtoken=None, replace=None, public_key_auth=None):
req = ['X-PageKite-Version: %s\r\n' % APPVER]
if replace:
req.append('X-PageKite-Replace: %s\r\n' % replace)
Expand All @@ -126,7 +147,8 @@ def PageKiteRequestHeaders(server, backends, tokens=None, testtoken=None, replac
# Sign the payload with the shared secret (random salt).
sign = signToken(secret=backends[d][BE_SECRET],
payload=data,
token=testtoken)
token=testtoken,
public_key_auth=public_key_auth)

req.append('X-PageKite: %s:%s\r\n' % (data, sign))
return req
Expand Down
90 changes: 90 additions & 0 deletions scripts/demo_public_key_auth_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/python2 -u

from __future__ import absolute_import

#
# This is a trivial demo auth server, which just approves any requests
# it sees, while printing debug information to STDERR.
#
# This code is in the public domain, feel free to adapt to your needs.
#
import getopt
import json
import sys
import subprocess


CAPABILITIES = ('ZK-AUTH')


def ZkAuth(domain):

parts = domain.split('.')
sys.stderr.write(str(parts) + '\n')

jws = ".".join(parts[2:5])

tunnel_domain = '.'.join(parts[6:-4])

sys.stderr.write(jws + '\n' + tunnel_domain + '\n')

jws_check_process = subprocess.Popen(['jose-util', 'verify', '--key=./scripts/jwk-sig-example.com-pub.json'],stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
jws_payload, _ = jws_check_process.communicate(input=jws)

if jws_check_process.returncode != 0:
return {'hostname': domain, 'alias': '', 'ips': ['0.0.0.0']}

sys.stderr.write(jws_payload + '\n')

payload_dict = json.loads(jws_payload)

if tunnel_domain != payload_dict.get('domain'):
sys.stderr.write('Domain mismatch!\n%s != %s' % (tunnel_domain, payload_dict.get('domain')))
return {'hostname': domain, 'alias': '', 'ips': ['0.0.0.0']}

return {'hostname': domain, 'alias': '', 'ips': ['255.255.255.0']}


def P(string):
# Delete the sys.stderr line if you're not debugging.
sys.stderr.write('>> ' + string + '\n')
print(string)


def ProcessArgs(args, server=False):
o, a = getopt.getopt(args, 'a:z:',
([] if server else ['capabilities', 'server']) +
['auth=', 'zk-auth='])

for opt, arg in o:
sys.stderr.write('<< %s=%s\n' % (opt, arg))

if opt == '--capabilities':
P(CAPABILITIES)
return

if opt == '--server':
ServerLoop()
return

if opt in ('-z', '--zk-auth'):
P(json.dumps(ZkAuth(arg), indent=None))
return


def ServerLoop():
while True:
line = sys.stdin.readline()
if not line:
return

args = line.strip().split()
if args and not args[0][:2] == '--':
args[0] = '--' + args[0]
ProcessArgs(args, server=True)


if __name__ == '__main__':
ProcessArgs(sys.argv[1:])
3 changes: 3 additions & 0 deletions scripts/example-public-key-auth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

cat './scripts/jws-example-clear.json' | jose-util sign --key ./scripts/jwk-sig-example.com-priv.json --alg EdDSA
9 changes: 9 additions & 0 deletions scripts/jwk-sig-example.com-priv.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"use": "sig",
"kty": "OKP",
"kid": "example.com",
"crv": "Ed25519",
"alg": "EdDSA",
"x": "FeCpsQsUAfhGDxrqXx6rjnD26gTaIB8dJDJNyasg7RI",
"d": "IGDhGoWn5OMFx9YWypRCG1XCWd7iRwucYgReobbqsV0"
}
8 changes: 8 additions & 0 deletions scripts/jwk-sig-example.com-pub.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"use": "sig",
"kty": "OKP",
"kid": "example.com",
"crv": "Ed25519",
"alg": "EdDSA",
"x": "FeCpsQsUAfhGDxrqXx6rjnD26gTaIB8dJDJNyasg7RI"
}
3 changes: 3 additions & 0 deletions scripts/jws-example-clear.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"domain": "test.example.com"
}