├── .nojekyll
├── nizkctf
├── __init__.py
├── cli
│ ├── __init__.py
│ ├── log.py
│ ├── teamsecrets.py
│ ├── team.py
│ ├── challenges.py
│ └── scoreboard.py
├── text.py
├── repohost
│ ├── __init__.py
│ ├── common.py
│ ├── github.py
│ └── gitlab.py
├── settings.py
├── six.py
├── localsettings.py
├── acceptedsubmissions.py
├── proof.py
├── serializable.py
├── challenge.py
├── subrepo.py
├── team.py
└── proposal.py
├── pip-requirements.txt
├── Makefile
├── challenges
├── index.json
├── fixed_ip.json
├── p0l1sh.json
├── matrix-gift.json
├── encrypted.json
├── hmac.json
└── futuro.json
├── .gitignore
├── settings.json
├── known_hosts
├── index.html
├── README.md
├── frontend
└── js
│ └── functions.js
├── lambda_function.py
└── ctf
/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nizkctf/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nizkctf/cli/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pip-requirements.txt:
--------------------------------------------------------------------------------
1 | requests[security] >= 2.13.0
2 | pysodium >= 0.6.9.1
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | python -m SimpleHTTPServer 8080
3 |
4 | ../lambda.zip: FORCE
5 | rm -f "$@"
6 | zip -ry "$@" * .git*
7 |
8 | FORCE:
9 |
--------------------------------------------------------------------------------
/challenges/index.json:
--------------------------------------------------------------------------------
1 | [
2 | "encrypted",
3 | "fixed_ip",
4 | "hmac",
5 | "p0l1sh",
6 | "futuro",
7 | "matrix-gift"
8 | ]
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # our own stuff
2 | /submissions
3 | /local-settings.json
4 | /team-secrets.json
5 |
6 | # elm-make
7 | /elm-stuff
8 |
9 | # python
10 | *.pyc
11 | *.pyo
12 |
13 | # wingide
14 | *.wpr
15 | *.wpu
16 |
--------------------------------------------------------------------------------
/nizkctf/text.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import unicodedata
6 |
7 |
8 | def width(s):
9 | asian = sum(unicodedata.east_asian_width(c) in {'W', 'F'} for c in s)
10 | return len(s) + asian
11 |
--------------------------------------------------------------------------------
/challenges/fixed_ip.json:
--------------------------------------------------------------------------------
1 | {"description": "PT: Digite IP e porta para receber a FLAG! EN: Type IP and Port to receive the FLAG! http://138.197.9.129:8002/", "title": "fixed_ip", "tags": ["Web"], "points": 40, "pk": "bJn3vUZGeyVBA/L4yZiOQQcMnL0ijwAHNl6Zo76pXXg=", "salt": "eBl10Wg93p0u+CmeAIyCW3LfD6AcnqL+yfmcHzcNYrQ=", "id": "fixed_ip"}
2 |
--------------------------------------------------------------------------------
/nizkctf/repohost/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | from .github import GitHub
6 | from .gitlab import GitLab
7 |
8 | from ..settings import Settings
9 | RepoHost = globals()[Settings.repository_host]
10 |
--------------------------------------------------------------------------------
/nizkctf/cli/log.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 |
6 |
7 | def info(s):
8 | print('\033[93m[*]\033[00m %s' % s)
9 |
10 |
11 | def success(s):
12 | print('\033[92m[+]\033[00m %s' % s)
13 |
14 |
15 | def fail(s):
16 | print('\033[91m[!]\033[00m %s' % s)
17 |
--------------------------------------------------------------------------------
/challenges/p0l1sh.json:
--------------------------------------------------------------------------------
1 | {"description": "PT: c0n3c73 n4 p0r74 d0 pr0c3554d0r XT 3m 104.219.54.49 3 pr055164. Adicione CTF-BR{} ao submeter. EN: C0n3c7 t0 XT's CPU p0r7 1n 104.219.54.49 4nd m0v3 0n. Add CTF-BR{} on submission.", "title": "p0l1sh", "tags": ["Miscellaneous"], "points": 20, "pk": "iZDJdmuJQ6Ko5duhjMkz4ac+ZV+P87oB0z2kp+NvItM=", "salt": "IyRZI4jVh7RRokW+943L5orKR27lam0qR9O6nH5tEN0=", "id": "p0l1sh"}
--------------------------------------------------------------------------------
/challenges/matrix-gift.json:
--------------------------------------------------------------------------------
1 | {"description": "PT: Na matrix testo meus limites e ela me recompensa. EN: In the matrix I test my skills and I'm always rewarded. https://ctf.tecland.com.br/homologacao/Matrix-Gift.tar.gz", "title": "Matrix Gift", "tags": ["Reversing"], "points": 50, "pk": "f9Hq+0royBzlZ+LsaIB99ha059tfLBoSDJCxPdS3tyA=", "salt": "iZnUQk5/8Xlafki96En5P2FrvmvV2L9MbZ1f5mJ+QUI=", "id": "matrix-gift"}
2 |
--------------------------------------------------------------------------------
/challenges/encrypted.json:
--------------------------------------------------------------------------------
1 | {"description": "PT: Voc\u00ea consegue reverter isso? Basta reverter essa string com base no bin\u00e1rio, boa sorte. EN: Just revert this string based on the binary, good luck. Hash: https://goo.gl/USPE9t Bin: https://ctf.tecland.com.br/homologacao/encrypted.tar.gz", "title": "Encrypted", "tags": ["Reversing"], "points": 60, "pk": "mVtYzq/vX0qgvhEpFMHlizrODy/+69TtUlFEJL1VG9M=", "salt": "ag0ZFDH5eaf2suDcThqzrnk7vKVSWQoUrldqTCAostQ=", "id": "encrypted"}
2 |
--------------------------------------------------------------------------------
/challenges/hmac.json:
--------------------------------------------------------------------------------
1 | {"description": "PT: Voc\u00ea conseguiria quebrar esse hash pra n\u00f3s? \u00c9 formado por letras min\u00fasculas e a key leetz0rs_can. Submeta com CTF-BR{palavra} EN: Can you break this hash for us? ([a-z], key -> leetz0rs_can). Add CTF-BR{word} on submission. Hash: 97a4242c5a9082e6c5920e0e3dc0b9b4", "title": "HMAC", "tags": ["Cryptography"], "points": 30, "pk": "A9MwsdbveEZsGCpVkP5twdcfWNAeXrFCrnbxk0GELbE=", "salt": "V0/7FGubNKULkFO4r4Q8CakGdzuCZ32Tr3UYYs6qnFU=", "id": "hmac"}
--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ctf_name": "Pwn2Win Platform Test Edition",
3 | "repository_host": "GitHub",
4 | "submissions_project": "pwn2winctf/PTEsubmissions",
5 |
6 | "scrypt_ops_limit": 33554432,
7 | "scrypt_mem_limit": 1073741824,
8 |
9 | "max_size_team_name": 50,
10 | "max_size_chall_id": 15,
11 |
12 | "github_api_endpoint": "https://api.github.com/",
13 | "github_ssh_url": "git@github.com:%s.git",
14 |
15 | "gitlab_api_endpoint": "https://gitlab.com/api/v3/",
16 | "gitlab_ssh_url": "git@gitlab.com:%s.git"
17 | }
18 |
--------------------------------------------------------------------------------
/challenges/futuro.json:
--------------------------------------------------------------------------------
1 | {"description": "PT: Jakko, um finland\u00eas tamb\u00e9m conhecido como \"WiZ\", escondeu um tesouro em sustenido 0x1337 no treasure.ctf-br.org. Voc\u00ea \u00e9 capaz de encontr\u00e1-lo utilizando suas net skillz? :) EN: Jakko, a finnish also known as \"WiZ\", have a hidden treasure in sustained 0x1337 at treasure.ctf-br.org. Are you capable of find it using your net skillz? :)", "title": "Futuro", "tags": ["Networking"], "points": 50, "pk": "g6Je/ZZ+wf7XwF1TOIAzbvwG7OXJbMNU6xM5KbYg8jw=", "salt": "fqHNbTGV0o8PQ25JG6neAs6yyFxHDUBc09+9gnMxztA=", "id": "futuro"}
--------------------------------------------------------------------------------
/nizkctf/settings.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import json
7 | from .six import viewitems
8 |
9 |
10 | class Settings(object):
11 | pass
12 |
13 |
14 | def load():
15 | """ Load settings from json file """
16 | thisdir = os.path.dirname(os.path.realpath(__file__))
17 | with open(os.path.join(thisdir, '..', 'settings.json')) as f:
18 | settings = json.load(f)
19 | assert isinstance(settings, dict)
20 | for k, v in viewitems(settings):
21 | setattr(Settings, k, v)
22 |
23 | load()
24 |
--------------------------------------------------------------------------------
/nizkctf/six.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | # a subset of the six module
3 |
4 | from __future__ import unicode_literals, division, print_function,\
5 | absolute_import
6 | import operator
7 | import sys
8 |
9 |
10 | PY2 = sys.version_info[0] == 2
11 |
12 | text_type = type('')
13 |
14 |
15 | if PY2:
16 | viewitems = operator.methodcaller("viewitems")
17 | else:
18 | viewitems = operator.methodcaller("items")
19 |
20 |
21 | def to_bytes(s):
22 | if isinstance(s, text_type):
23 | return bytes(s.encode('utf-8'))
24 | return s
25 |
26 |
27 | def to_unicode(s):
28 | if isinstance(s, text_type):
29 | return s
30 | encoding = sys.getfilesystemencoding()
31 | s = s.decode(encoding)
32 | return s
33 |
--------------------------------------------------------------------------------
/nizkctf/localsettings.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import json
7 | import threading
8 |
9 |
10 | thisdir = os.path.dirname(os.path.realpath(__file__))
11 | path = os.path.join(thisdir, '..', 'local-settings.json')
12 |
13 |
14 | class DefaultLocalSettings(object):
15 | __lock__ = threading.Lock()
16 |
17 | def __init__(self):
18 | if os.path.exists(path):
19 | with open(path) as f:
20 | self.__dict__.update(json.load(f))
21 |
22 | def __setattr__(self, k, v):
23 | self.__dict__[k] = v
24 | with self.__lock__:
25 | with open(path, 'w') as f:
26 | json.dump(self.__dict__, f)
27 |
28 |
29 | LocalSettings = DefaultLocalSettings()
30 |
--------------------------------------------------------------------------------
/known_hosts:
--------------------------------------------------------------------------------
1 | github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
2 | gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
3 | gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
4 | gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf
5 |
--------------------------------------------------------------------------------
/nizkctf/acceptedsubmissions.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import time
7 | from .six import text_type
8 | from .subrepo import SubRepo
9 | from .serializable import SerializableList
10 |
11 |
12 | ACCEPTED_SUBMISSIONS_FILE = 'accepted-submissions.json'
13 | TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
14 |
15 |
16 | class AcceptedSubmissions(SerializableList):
17 | def __init__(self):
18 | super(AcceptedSubmissions, self).__init__()
19 |
20 | def path(self):
21 | return SubRepo.get_path(ACCEPTED_SUBMISSIONS_FILE)
22 |
23 | def add(self, chall_id, points, team_id):
24 | if (chall_id, team_id) in ((s['chall'], s['team']) for s in self):
25 | # Challenge already submitted by team
26 | return
27 | self.append({"chall": chall_id,
28 | "points": points,
29 | "team": team_id,
30 | "time": current_time()})
31 | self.save()
32 |
33 |
34 | def current_time():
35 | return time.strftime(TIME_FORMAT)
36 |
--------------------------------------------------------------------------------
/nizkctf/cli/teamsecrets.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import shutil
7 | import time
8 | from . import log
9 | from ..serializable import SerializableDict
10 |
11 |
12 | TEAMSECRETS_FILE = 'team-secrets.json'
13 |
14 |
15 | class DefaultTeamSecrets(SerializableDict):
16 | def path(self):
17 | thisdir = os.path.dirname(os.path.realpath(__file__))
18 | return os.path.join(thisdir, '..', '..', TEAMSECRETS_FILE)
19 |
20 | @staticmethod
21 | def _binary_field(k):
22 | return k.endswith('_sk')
23 |
24 |
25 | TeamSecrets = DefaultTeamSecrets()
26 |
27 |
28 | def write_team_secrets(team_id, crypt_sk, sign_sk):
29 | if TeamSecrets.exists():
30 | log.info('overriding %s' % TEAMSECRETS_FILE)
31 |
32 | backup = TeamSecrets.path() + time.strftime('.%Y-%m-%d-%H-%M-%S')
33 | shutil.copy(TeamSecrets.path(), backup)
34 |
35 | TeamSecrets['id'] = team_id
36 | TeamSecrets['crypt_sk'] = crypt_sk
37 | TeamSecrets['sign_sk'] = sign_sk
38 | TeamSecrets.save()
39 |
40 | log.success('ready, share %s with your team!' % TEAMSECRETS_FILE)
41 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/nizkctf/cli/team.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import hashlib
6 | import os
7 | import json
8 | import codecs
9 | import pysodium
10 | import base64
11 | from . import log
12 | from .teamsecrets import write_team_secrets
13 | from ..team import Team
14 | from ..subrepo import SubRepo
15 | from ..six import to_unicode
16 |
17 |
18 | def register(team_name):
19 | team_name = to_unicode(team_name)
20 |
21 | log.info('updating subrepo')
22 | SubRepo.pull()
23 |
24 | log.info('registering new team: %s' % team_name)
25 | team = Team(name=team_name)
26 |
27 | if team.exists():
28 | log.fail('team is already registered')
29 | return False
30 |
31 | log.info('generating encryption keypair')
32 | crypt_pk, crypt_sk = pysodium.crypto_box_keypair()
33 |
34 | log.info('generating signature keypair')
35 | sign_pk, sign_sk = pysodium.crypto_sign_keypair()
36 |
37 | team.update({'crypt_pk': crypt_pk,
38 | 'sign_pk': sign_pk})
39 | team.validate()
40 | team.save()
41 |
42 | SubRepo.push(commit_message='Register team %s' % team_name)
43 | log.success('team %s added successfully' % team_name)
44 |
45 | write_team_secrets(team.id, crypt_sk, sign_sk)
46 |
47 | return True
48 |
--------------------------------------------------------------------------------
/nizkctf/proof.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import pysodium
7 | from base64 import b64encode, b64decode
8 | from .six import text_type
9 | from .challenge import Challenge
10 | from .cli.teamsecrets import TeamSecrets
11 |
12 |
13 | def proof_open(team, proof):
14 | if isinstance(proof, text_type):
15 | proof = proof.encode('utf-8')
16 |
17 | assert isinstance(proof, bytes)
18 |
19 | proof = b64decode(proof)
20 |
21 | claimed_chall_id = proof[2*64:].decode('utf-8')
22 | claimed_chall = Challenge(claimed_chall_id)
23 |
24 | chall_pk = claimed_chall['pk']
25 | team_pk = team['sign_pk']
26 |
27 | membership_proof = pysodium.crypto_sign_open(proof, chall_pk)
28 | chall_id = pysodium.crypto_sign_open(membership_proof,
29 | team_pk).decode('utf-8')
30 |
31 | if claimed_chall_id != chall_id:
32 | raise ValueError('invalid proof')
33 |
34 | return claimed_chall
35 |
36 |
37 | def proof_create(chall_id, chall_sk):
38 | chall_id = chall_id.encode('utf-8')
39 |
40 | assert isinstance(chall_id, bytes)
41 | assert isinstance(chall_sk, bytes)
42 |
43 | team_sk = TeamSecrets['sign_sk']
44 |
45 | membership_proof = pysodium.crypto_sign(chall_id, team_sk)
46 | proof = pysodium.crypto_sign(membership_proof, chall_sk)
47 |
48 | return b64encode(proof).decode('utf-8')
49 |
--------------------------------------------------------------------------------
/nizkctf/repohost/common.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import requests
7 | import urllib
8 | import codecs
9 | from ..localsettings import LocalSettings
10 |
11 |
12 | class BaseRepoHost(object):
13 | @classmethod
14 | def login(cls, username=None, password=None, token=None):
15 | if not token and (username and password):
16 | token = cls.get_token(username, password)
17 | if not token:
18 | raise ValueError("Pass either a token or an username/password")
19 | LocalSettings.token = token
20 |
21 | @classmethod
22 | def instance(cls):
23 | token = os.getenv('REPOHOST_TOKEN') or \
24 | LocalSettings.token
25 | return cls(token)
26 |
27 | def __init__(self, token):
28 | self.token = token
29 | self.s = requests.Session()
30 | self._init_session()
31 |
32 | def _init_session(self):
33 | pass
34 |
35 | @classmethod
36 | def get_token(cls, username, password):
37 | pass
38 |
39 | @staticmethod
40 | def _raise_for_status(r):
41 | try:
42 | r.raise_for_status()
43 | except Exception as e:
44 | raise APIError(r.text, e)
45 |
46 |
47 | class APIError(Exception):
48 | pass
49 |
50 |
51 | class WebhookAuthError(Exception):
52 | pass
53 |
54 |
55 | quote_plus = urllib.quote_plus if hasattr(urllib, 'quote_plus') else \
56 | urllib.parse.quote_plus
57 |
--------------------------------------------------------------------------------
/nizkctf/serializable.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import json
7 | from base64 import b64encode, b64decode
8 | from .six import viewitems, text_type
9 |
10 |
11 | class Serializable(object):
12 | def __init__(self):
13 | self.load()
14 |
15 | def load(self):
16 | if self.exists():
17 | self.clear()
18 | with open(self.path()) as f:
19 | self.update(json.load(f))
20 | self._unserialize_inplace()
21 |
22 | def save(self):
23 | with open(self.path(), 'w') as f:
24 | json.dump(self._serialize(), f)
25 |
26 | def exists(self):
27 | return os.path.exists(self.path())
28 |
29 | def _unserialize_inplace(self):
30 | pass
31 |
32 | def _serialize(self):
33 | return self
34 |
35 |
36 | class SerializableDict(Serializable, dict):
37 | @staticmethod
38 | def _binary_field(k):
39 | return False
40 |
41 | def _unserialize_inplace(self):
42 | for k, v in viewitems(self):
43 | if self._binary_field(k):
44 | assert isinstance(v, text_type)
45 | self[k] = b64decode(v)
46 |
47 | def _serialize(self):
48 | return {k: b64encode(v).decode('utf-8') if self._binary_field(k) else v
49 | for k, v in viewitems(self)}
50 |
51 |
52 | class SerializableList(Serializable, list):
53 | def clear(self):
54 | del self[:]
55 |
56 | def update(self, l):
57 | self += l
58 |
--------------------------------------------------------------------------------
/nizkctf/cli/challenges.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import codecs
7 | import json
8 | import pysodium
9 | import base64
10 | import textwrap
11 |
12 | from . import log
13 | from .teamsecrets import TeamSecrets
14 | from ..team import my_team
15 | from ..proof import proof_create
16 | from ..challenge import Challenge, lookup_flag
17 | from ..subrepo import SubRepo
18 |
19 |
20 | def submit_flag(flag, chall_id=None):
21 | chall, chall_sk = lookup_flag(flag, chall_id)
22 |
23 | if chall_sk is None:
24 | return False, 'This is not the correct flag.'
25 |
26 | SubRepo.pull()
27 |
28 | submissions = my_team().submissions()
29 | if chall in submissions.challs():
30 | return False, 'Your team already solved %s.' % chall.id
31 |
32 | proof = proof_create(chall.id, chall_sk)
33 | submissions.submit(proof)
34 | SubRepo.push(commit_message='Proof: found flag for %s' % chall.id)
35 |
36 | return True, 'Congratulations! You found the right flag for %s.' % chall.id
37 |
38 |
39 | LINE_WIDTH = 72
40 |
41 |
42 | def pprint():
43 | print('')
44 | print('-'*LINE_WIDTH)
45 | print('')
46 | for chall_id in Challenge.index():
47 | chall = Challenge(chall_id)
48 | print('ID: %s (%d points) [%s]' % (
49 | chall['id'],
50 | chall['points'],
51 | ', '.join(chall['tags'])))
52 | print('')
53 | print(chall['title'])
54 | print('')
55 | print('\n'.join(textwrap.wrap(chall['description'],
56 | LINE_WIDTH)))
57 | print('')
58 | print('-'*LINE_WIDTH)
59 | print('')
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Platform Test Edition
2 |
3 | Welcome to the Pwn2Win CTF **Platform Test Edition**.
4 |
5 | ## Registration
6 |
7 | 1. Please make sure you are using an Unicode locale, e.g.
8 | ```bash
9 | export LC_ALL=en_US.UTF-8
10 | ```
11 |
12 | 2. All team members must have a GitHub account and [configure a SSH key in their account settings](https://github.com/settings/keys).
13 |
14 | **Note**: If you prefer team members to stay anonymous, you can create a single GitHub account for the entire team and share its credentials.
15 |
16 | 3. All team members must have the git client [correctly set up](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup). If you have never used git before, run:
17 | ```bash
18 | git config --global user.name "John Doe"
19 | git config --global user.email johndoe@example.com
20 | ```
21 |
22 | 4. All team members must clone the repository and install the dependencies:
23 | ```bash
24 | git clone git@github.com:pwn2winctf/PTE.git
25 | cd PTE
26 | sudo apt-get install libsodium18
27 | sudo -H pip install -r pip-requirements.txt
28 | ```
29 | **Note**: If you are using Ubuntu 14.04, add [ppa:elt/libsodium](https://launchpad.net/~elt/+archive/ubuntu/libsodium) to your system to be able to install `libsodium18`. If you are using Debian, you need to get the package from [sid](https://packages.debian.org/sid/libsodium18).
30 |
31 | 5. If dependencies are installed correctly, you should now see the help menu when calling:
32 | ```bash
33 | ./ctf -h
34 | ```
35 |
36 | 6. The **leader of the team** must execute the following command and follow the instructions to register the team:
37 | ```bash
38 | ./ctf init
39 | ```
40 |
41 | 7. The **other members of the team** must login to GitHub without registering a new team, by running:
42 | ```bash
43 | ./ctf login
44 | ```
45 |
46 | 8. After that, **the leader** must share the `team-secrets.json` with the members of the team. The **other members of the team** must place the `team-secrets.json` file shared by the leader in their `PTE` directory.
47 |
48 | ## Challenges
49 |
50 | Challenges are available on https://pwn2winctf.github.io.
51 |
52 | If you prefer to browse them locally, you may also run a local webserver by typing `make`, or list challenges through the command line interface:
53 | ```bash
54 | ./ctf challs
55 | ```
56 |
57 | ## Flag submission
58 |
59 | To submit a flag:
60 | ```bash
61 | ./ctf submit --chall chall-id 'CTF-BR{flag123}'
62 | ```
63 |
64 | You may omit `--chall chall-id` from the command, however it will be slower to run this way. In this case, we will look for the flag in every challenge released until now.
65 |
66 | ## Scoreboard
67 |
68 | Currently, the scoreboard is only available through the command line interface:
69 | ```bash
70 | ./ctf score --names --pull
71 | ```
72 |
73 | However we plan to make it available through the web interface in a future release.
74 |
--------------------------------------------------------------------------------
/nizkctf/challenge.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | from base64 import b64decode
6 | import os
7 | import re
8 | import json
9 | import pysodium
10 | from .serializable import SerializableDict, SerializableList
11 | from .settings import Settings
12 |
13 |
14 | CHALLENGES_DIR = 'challenges'
15 | INDEX_FILE = 'index.json'
16 |
17 | thisdir = os.path.dirname(os.path.realpath(__file__))
18 | chall_dir = os.path.realpath(os.path.join(thisdir, '..', CHALLENGES_DIR))
19 |
20 |
21 | class Challenge(SerializableDict):
22 | def __init__(self, id):
23 | self.validate_id(id)
24 | self.id = id
25 | super(Challenge, self).__init__()
26 |
27 | def __eq__(self, other):
28 | return self.id == other.id
29 |
30 | def __hash__(self):
31 | return hash(self.id)
32 |
33 | def path(self):
34 | return os.path.join(chall_dir, self.id + '.json')
35 |
36 | @staticmethod
37 | def validate_id(id):
38 | if len(id) > Settings.max_size_chall_id or \
39 | not re.match(r'^[a-zA-Z0-9-_]+$', id):
40 | raise ValueError('invalid challenge ID')
41 |
42 | @staticmethod
43 | def _binary_field(k):
44 | return k in {'salt', 'pk'}
45 |
46 | @staticmethod
47 | def index():
48 | return ChallengeIndex()
49 |
50 |
51 | class ChallengeIndex(SerializableList):
52 | def path(self):
53 | return os.path.join(chall_dir, INDEX_FILE)
54 |
55 |
56 | def derive_keypair(salt, flag):
57 | flag = flag.encode('utf-8')
58 | assert isinstance(salt, bytes)
59 | assert isinstance(flag, bytes)
60 | assert len(salt) == pysodium.crypto_pwhash_scryptsalsa208sha256_SALTBYTES
61 |
62 | chall_seed = pysodium.crypto_pwhash_scryptsalsa208sha256(
63 | pysodium.crypto_sign_SEEDBYTES,
64 | flag,
65 | salt,
66 | Settings.scrypt_ops_limit,
67 | Settings.scrypt_mem_limit)
68 |
69 | return pysodium.crypto_sign_seed_keypair(chall_seed)
70 |
71 |
72 | def random_salt():
73 | return pysodium.randombytes(
74 | pysodium.crypto_pwhash_scryptsalsa208sha256_SALTBYTES)
75 |
76 |
77 | def lookup_flag(flag, chall_id=None):
78 | if chall_id:
79 | # challenge provided, only try it
80 | try_challenges = [Challenge(chall_id)]
81 | if not try_challenges[0].exists():
82 | raise ValueError("A challenge named '%s' does not exist." %
83 | chall_id)
84 | else:
85 | # try every challenge
86 | try_challenges = map(Challenge, Challenge.index())
87 |
88 | try_salts = set(chall['salt'] for chall in try_challenges)
89 | pk_chall = {chall['pk']: chall for chall in try_challenges}
90 |
91 | for salt in try_salts:
92 | pk, sk = derive_keypair(salt, flag)
93 | match = pk_chall.get(pk)
94 |
95 | if match:
96 | return match, sk
97 |
98 | return None, None
99 |
--------------------------------------------------------------------------------
/frontend/js/functions.js:
--------------------------------------------------------------------------------
1 | (function(_) {
2 | _.nizkctf = {};
3 |
4 | var challengesDiv = $('#challenges');
5 | var modalsDiv = $('#modals');
6 |
7 | var challTagsTpl = function(tags) {
8 | return tags.map(function(tag){
9 | return '' + tag + '';
10 | }).join('');
11 | };
12 |
13 | var challModalTpl = function(challenge) {
14 | return ''
15 | + '
'
16 | + '
' + challenge.title + '
'
17 | + '
' + challenge.description + '
'
18 | + '
ID: ' + challenge.id + '>
'
19 | + '
Points: ' + challenge.points + '>
'
20 | + '
Tags: ' + challenge.tags.join(', ') + '>
'
21 | + '
'
22 | + ''
25 | + '
';
26 | }
27 |
28 | var challCardTpl = function(challenge) {
29 |
30 | return ''
31 | + '
'
32 | + '
'
33 | + '
'
34 | + challenge.title
35 | + '' + challenge.points + ''
36 | + ''
37 | + '
' + challenge.description.substr(0,100) + '...' + '
'
38 | + '
' + challTagsTpl(challenge.tags) + '
'
39 | + '
'
40 | + '
'
43 | + '
'
44 | + '
';
45 | };
46 |
47 | var getChallenges = function() {
48 | var mountChallTpl = function(challenge) {
49 | challengesDiv.append(challCardTpl(challenge));
50 | modalsDiv.append(challModalTpl(challenge));
51 | };
52 |
53 | var mountChallPromise = function(challUrl) {
54 | return $.getJSON('challenges/' + challUrl + '.json')
55 | .then(mountChallTpl);
56 | };
57 |
58 | var challPromiseMap = function(challList) {
59 | return $.when.apply($, challList.map(mountChallPromise));
60 | };
61 |
62 | return $.getJSON('challenges/index.json')
63 | .then(challPromiseMap);
64 | };
65 |
66 | var getSettings = function() {
67 | var handleSettings = function(settings) {
68 | $('#logo-container').text(settings.ctf_name);
69 | $('title').text(settings.ctf_name);
70 | };
71 |
72 | $.getJSON('settings.json')
73 | .then(handleSettings);
74 | };
75 |
76 | var renderChallenges = function() {
77 | challengesDiv.html('');
78 | getChallenges()
79 | .then(function() {
80 | $('.modal').modal();
81 | });
82 | }
83 |
84 | _.nizkctf.init = function() {
85 | renderChallenges();
86 | getSettings();
87 | }
88 |
89 | _.nizkctf.init();
90 |
91 | })(window);
92 |
--------------------------------------------------------------------------------
/nizkctf/subrepo.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import subprocess
7 | import base64
8 | import pysodium
9 | from .settings import Settings
10 | from .localsettings import LocalSettings
11 | from .repohost import RepoHost
12 |
13 |
14 | SUBREPO_NAME = 'submissions'
15 |
16 |
17 | class SubRepo(object):
18 | @classmethod
19 | def set_clone_into(cls, clone_into):
20 | cls.clone_into = clone_into
21 | cls.path = os.path.join(clone_into, SUBREPO_NAME)
22 |
23 | @classmethod
24 | def get_path(cls, subpath=''):
25 | if os.path.exists(cls.path):
26 | return os.path.join(cls.path, subpath)
27 | raise EnvironmentError("The subrepository path ('%s') was not created "
28 | "yet. Please call 'ctf login' to get it cloned "
29 | "before performing any further actions." %
30 | cls.path)
31 |
32 | @classmethod
33 | def clone(cls, fork=True):
34 | repohost = RepoHost.instance()
35 | upstream_url = repohost.get_ssh_url(Settings.submissions_project)
36 |
37 | if fork:
38 | forked_project, origin_url = \
39 | repohost.fork(Settings.submissions_project)
40 | LocalSettings.forked_project = forked_project
41 | else:
42 | origin_url = upstream_url
43 |
44 | cls.git(['clone', origin_url, SUBREPO_NAME], cwd=cls.clone_into)
45 | cls.git(['remote', 'add', 'upstream', upstream_url])
46 |
47 | @classmethod
48 | def pull(cls):
49 | cls.git(['checkout', 'master'])
50 | cls.git(['pull', '--rebase', 'upstream', 'master'])
51 |
52 | @classmethod
53 | def push(cls, commit_message='commit', merge_request=True):
54 | branch = 'master'
55 | if merge_request:
56 | branch = cls.random_branch()
57 | cls.git(['checkout', '-b', branch, 'master'])
58 |
59 | cls.git(['add', '-A'])
60 | cls.git(['commit', '-m', commit_message],
61 | returncodes={0, 1}) # do not fail on 'nothing to commit'
62 | cls.git(['push', '-u', 'origin', branch])
63 |
64 | if merge_request:
65 | repohost = RepoHost.instance()
66 | repohost.merge_request(LocalSettings.forked_project,
67 | Settings.submissions_project,
68 | source_branch=branch,
69 | title=commit_message)
70 |
71 | @staticmethod
72 | def random_branch():
73 | return base64.b32encode(pysodium.randombytes(10))\
74 | .decode('utf-8').lower()
75 |
76 | @classmethod
77 | def git(cls, args, **kwargs):
78 | returncodes = kwargs.pop('returncodes', {0})
79 | if 'cwd' not in kwargs:
80 | kwargs['cwd'] = cls.get_path()
81 |
82 | p = subprocess.Popen(['git'] + args, **kwargs)
83 |
84 | r = None
85 | if 'stdout' in kwargs:
86 | r = p.stdout.read()
87 |
88 | returncode = p.wait()
89 | if returncode not in returncodes:
90 | raise GitError(returncode)
91 |
92 | return r
93 |
94 |
95 | class GitError(Exception):
96 | def __init__(self, returncode, *args):
97 | self.returncode = returncode
98 | super(GitError, self).__init__(*args)
99 |
100 |
101 | thisdir = os.path.dirname(os.path.realpath(__file__))
102 | SubRepo.set_clone_into(os.path.realpath(os.path.join(thisdir, '..')))
103 |
--------------------------------------------------------------------------------
/lambda_function.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | from nizkctf.settings import Settings
6 | from nizkctf.subrepo import SubRepo
7 | from nizkctf.repohost import RepoHost
8 | from nizkctf.proposal import consider_proposal
9 | from nizkctf.six import to_bytes
10 | import os
11 | import json
12 | import base64
13 | import tempfile
14 | import traceback
15 |
16 |
17 | def run(merge_info):
18 | SubRepo.set_clone_into(tempfile.mkdtemp())
19 |
20 | # Prepare git and ssh for usage inside the container
21 | setup_environment()
22 |
23 | # Merge proposal if changes are valid
24 | consider_proposal(merge_info)
25 |
26 |
27 | def handle_payload(payload, context):
28 | merge_info = RepoHost.webhook.adapt_payload(payload)
29 |
30 | if not merge_info:
31 | # Message not of our interest (e.g. merge request closed)
32 | return
33 |
34 | try:
35 | run(merge_info)
36 | except:
37 | # Do not re-raise, we do not want automatic retries
38 | traceback.print_exc()
39 | # Send tracking number to the user
40 | send_cloudwatch_info(merge_info, context)
41 |
42 |
43 | def send_cloudwatch_info(merge_info, context):
44 | proj = Settings.submissions_project
45 | mr_id = merge_info['mr_id']
46 |
47 | comment = "Sorry. A failure has occurred when processing your proposal. " \
48 | "Please contact support and present the following info:\n\n" \
49 | "**Stream name**: %s\n" \
50 | "**Request ID**: %s\n" % \
51 | (context.log_stream_name, context.aws_request_id)
52 |
53 | repohost = RepoHost.instance()
54 | repohost.mr_comment(proj, mr_id, comment)
55 | repohost.mr_close(proj, mr_id)
56 |
57 |
58 | def handle_apigw(event, context):
59 | headers = event['params']['header']
60 | raw_payload = event['body']
61 |
62 | # autenticate the message
63 | secret = to_bytes(os.getenv('WEBHOOK_SECRET_TOKEN'))
64 | RepoHost.webhook.auth(secret, headers, to_bytes(raw_payload))
65 |
66 | payload = json.loads(raw_payload)
67 | return handle_payload(payload, context)
68 |
69 |
70 | def handle_sns(event, context):
71 | raw_payload = event['Records'][0]['Sns']['Message']
72 | payload = json.loads(raw_payload)
73 |
74 | # no way to authenticate, but also no need to
75 | # (publishing to the SNS topic should already be authenticated)
76 |
77 | return handle_payload(payload, context)
78 |
79 |
80 | def setup_environment():
81 | root = os.getenv('LAMBDA_TASK_ROOT')
82 | bin_dir = os.path.join(root, 'bin')
83 | os.environ['PATH'] += ':' + bin_dir
84 | os.environ['GIT_EXEC_PATH'] = bin_dir
85 |
86 | ssh_dir = tempfile.mkdtemp()
87 |
88 | ssh_identity = os.path.join(ssh_dir, 'identity')
89 | with os.fdopen(os.open(ssh_identity, os.O_WRONLY | os.O_CREAT, 0o600),
90 | 'w') as f:
91 | f.write(base64.b64decode(os.getenv('SSH_IDENTITY')))
92 |
93 | ssh_config = os.path.join(ssh_dir, 'config')
94 | with open(ssh_config, 'w') as f:
95 | f.write('CheckHostIP no\n'
96 | 'StrictHostKeyChecking yes\n'
97 | 'IdentityFile %s\n'
98 | 'UserKnownHostsFile %s\n' %
99 | (ssh_identity, os.path.join(root, 'known_hosts')))
100 |
101 | os.environ['GIT_SSH_COMMAND'] = 'ssh -F %s' % ssh_config
102 |
103 |
104 | def lambda_handler(event, context):
105 | if 'Records' in event:
106 | return handle_sns(event, context)
107 | elif 'body' in event:
108 | return handle_apigw(event, context)
109 | raise ValueError("Did not recognize a valid event originated by SNS nor "
110 | "by API Gateway. Did you configure it correctly?")
111 |
--------------------------------------------------------------------------------
/nizkctf/cli/scoreboard.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import sys
6 | import json
7 | import operator
8 | import tempfile
9 | import subprocess
10 | import codecs
11 | from ..team import Team
12 | from ..text import width
13 | from ..six import viewitems
14 | from ..acceptedsubmissions import AcceptedSubmissions, TIME_FORMAT
15 |
16 |
17 | def rank():
18 | '''
19 | Compute ranking given a submissions file.
20 |
21 | Args:
22 | f (file): File-like object with the submissions json.
23 |
24 | Returns:
25 | List containing the computed ranking for the submission list.
26 | The result is in the following format:
27 |
28 | (team, score)
29 |
30 | And a map containing submissions sorted by team.
31 | '''
32 |
33 | submissions = {}
34 | scores = {}
35 | last_submission = {}
36 | for subm_num, subm in enumerate(AcceptedSubmissions()):
37 | team = subm['team']
38 | score, _ = scores.get(team, (0, None))
39 | scores[team] = (score + subm['points'], -subm_num)
40 | submissions.setdefault(team, []).append(subm)
41 |
42 | scores = sorted(viewitems(scores), key=operator.itemgetter(1),
43 | reverse=True)
44 | scores = [(team, score) for team, (score, _) in scores]
45 |
46 | return (scores, submissions)
47 |
48 |
49 | def pprint(ranking, top=0, show_names=False):
50 | '''
51 | Pretty print scoreboard in terminal.
52 |
53 | Args:
54 | ranking (list): List of tuples containing teams and scores.
55 | top (int): Number of teams to show in scoreboard.
56 |
57 | '''
58 |
59 | if top == 0:
60 | top = len(ranking)
61 |
62 | if len(ranking) == 0:
63 | print('Nobody scored yet.')
64 | return
65 |
66 | ranking = ranking[:top]
67 |
68 | if show_names:
69 | ranking = [(Team(id=team)['name'], score) for team, score in ranking]
70 |
71 | team_len = max(width(team) for team, score in ranking)
72 | team_len = max(team_len, 10)
73 |
74 | pos_len = score_len = 6
75 |
76 | def hyph(n):
77 | return '-'*(n + 2)
78 |
79 | sep = hyph(pos_len) + '+' + hyph(team_len) + '+' + hyph(score_len)
80 |
81 | def fmtcol(s, n):
82 | return ' ' + s + ' '*(n - width(s) + 1)
83 |
84 | def fmt(pos, team, score):
85 | return fmtcol(pos, pos_len) + '|' + \
86 | fmtcol(team, team_len) + '|' + \
87 | fmtcol(score, score_len)
88 |
89 | print('')
90 | print(sep)
91 | print(fmt('Pos', 'Team', 'Score'))
92 | print(sep)
93 |
94 | for idx, (team, score) in enumerate(ranking):
95 | pos = '%d' % (idx + 1)
96 | print(fmt(pos, team, '%d' % score))
97 |
98 | print(sep)
99 | print('')
100 |
101 |
102 | def plot(ranking, submissions, top=3):
103 | '''
104 | Plot points for top teams.
105 |
106 | Args:
107 | ranking (list): List containing teams and scores sorted in
108 | descending order.
109 | submissions (dict): Dict [team] -> submission list.
110 | top (int): Number of teams to appear in chart.
111 | '''
112 | # generate temporary files with data points
113 | fnames = []
114 | for team, _ in ranking[0:top]:
115 | f = tempfile.NamedTemporaryFile(suffix='.dat',
116 | prefix='nizkctf-', delete=True)
117 | w = codecs.getwriter('utf-8')(f)
118 | partial = 0
119 | for subm in submissions[team]:
120 | partial += subm['points']
121 | w.write('%s, %d\n' % (subm['time'], partial))
122 | w.flush()
123 | fnames.append((team, f))
124 |
125 | # generate gnuplot file
126 | f = tempfile.NamedTemporaryFile(suffix='.gp',
127 | prefix='nizkctf-', delete=True)
128 | w = codecs.getwriter('utf-8')(f)
129 | w.write('set terminal dumb 120 30\n')
130 | w.write('set xdata time\n')
131 | w.write('set datafile sep \',\'\n')
132 | w.write('set timefmt "%s"\n' % TIME_FORMAT)
133 | w.write('set style data steps\n')
134 | w.write('plot ')
135 | fmt = '\'%s\' using 1:2 title \'%s\''
136 | w.write(fmt % (fnames[0][1].name, fnames[0][0]))
137 | for team, ft in fnames[1:]:
138 | w.write(', ')
139 | w.write(fmt % (ft.name, team))
140 | w.flush()
141 |
142 | # plot in terminal
143 | p = subprocess.Popen(['gnuplot', f.name],
144 | stderr=sys.stderr,
145 | stdout=sys.stdout)
146 | p.wait()
147 |
148 | # close/delete files
149 | f.close()
150 | for nm, f in fnames:
151 | f.close()
152 |
--------------------------------------------------------------------------------
/nizkctf/repohost/github.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import requests
6 | import hashlib
7 | import hmac
8 | from ..settings import Settings
9 | from ..six import to_bytes
10 | from .common import BaseRepoHost, APIError, WebhookAuthError
11 |
12 |
13 | class GitHubWebhook(object):
14 | @staticmethod
15 | def auth(secret, headers, raw_payload):
16 | received_sig = to_bytes(headers['X-Hub-Signature'])
17 |
18 | h = hmac.new(secret, raw_payload, hashlib.sha1).hexdigest()
19 | correct_sig = b'sha1=' + to_bytes(h)
20 |
21 | if not hmac.compare_digest(received_sig, correct_sig):
22 | raise WebhookAuthError()
23 |
24 | @staticmethod
25 | def adapt_payload(payload):
26 | # filtering
27 | if 'pull_request' not in payload:
28 | return None
29 | if payload['action'] not in {'opened', 'reopened'}:
30 | return None
31 | if payload['pull_request']['base']['repo']['full_name'] != \
32 | Settings.submissions_project:
33 | return None
34 | if payload['pull_request']['base']['ref'] != 'master':
35 | return None
36 | # mappings
37 | return {"mr_id": payload['pull_request']['number'],
38 | "source_ssh_url": payload['pull_request']['head']['repo']
39 | ['ssh_url'],
40 | "source_commit": payload['pull_request']['head']['sha'],
41 | "user_id": payload['pull_request']['user']['id'],
42 | "username": payload['pull_request']['user']['login']}
43 |
44 |
45 | class GitHub(BaseRepoHost):
46 | webhook = GitHubWebhook
47 |
48 | @classmethod
49 | def get_token(cls, username, password):
50 | authorization = {'scopes': 'public_repo',
51 | 'note': Settings.ctf_name}
52 |
53 | r = requests.post(Settings.github_api_endpoint +
54 | 'authorizations',
55 | json=authorization,
56 | auth=(username, password))
57 |
58 | data = r.json()
59 |
60 | if cls._has_error(data, 'already_exists'):
61 | raise APIError("Please visit https://github.com/settings/tokens "
62 | "and make sure you do not already have a personal "
63 | "access token called '%s'" % Settings.ctf_name)
64 | cls._raise_for_status(r)
65 |
66 | return data['token']
67 |
68 | @staticmethod
69 | def get_ssh_url(proj):
70 | return Settings.github_ssh_url % proj
71 |
72 | def fork(self, source):
73 | r = self.s.post(Settings.github_api_endpoint +
74 | 'repos/' + source + '/forks')
75 | self._raise_for_status(r)
76 | data = r.json()
77 | return data['full_name'], data['ssh_url']
78 |
79 | def merge_request(self, source, target,
80 | source_branch='master',
81 | target_branch='master',
82 | title='Pull Request'):
83 | source_branch = source.split('/', 2)[0] + ':' + source_branch
84 |
85 | pull_request = {'head': source_branch,
86 | 'base': target_branch,
87 | 'title': title}
88 |
89 | r = self.s.post(Settings.github_api_endpoint +
90 | 'repos/' + target + '/pulls',
91 | json=pull_request)
92 | self._raise_for_status(r)
93 | return r.json()
94 |
95 | def mr_comment(self, proj, mr_id, contents):
96 | r = self.s.post(Settings.github_api_endpoint +
97 | 'repos/' + proj + '/issues/%d' % mr_id + '/comments',
98 | json={'body': contents})
99 | self._raise_for_status(r)
100 | return r.json()
101 |
102 | def mr_close(self, proj, mr_id):
103 | r = self.s.patch(Settings.github_api_endpoint +
104 | 'repos/' + proj + '/pulls/%d' % mr_id,
105 | json={'state': 'closed'})
106 | self._raise_for_status(r)
107 | return r.json()
108 |
109 | def mr_accept(self, proj, mr_id, sha):
110 | r = self.s.put(Settings.github_api_endpoint +
111 | 'repos/' + proj + '/pulls/%d' % mr_id + '/merge',
112 | json={'sha': sha})
113 | self._raise_for_status(r)
114 | return r.json()
115 |
116 | def _init_session(self):
117 | self.s.headers.update({'Authorization': 'token ' + self.token})
118 |
119 | @staticmethod
120 | def _has_error(data, err_code):
121 | return any(err.get('code') == err_code
122 | for err in data.get('errors', []))
123 |
--------------------------------------------------------------------------------
/nizkctf/team.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import re
7 | import hashlib
8 | import pysodium
9 | from .six import text_type
10 | from .settings import Settings
11 | from .subrepo import SubRepo
12 | from .serializable import SerializableDict, SerializableList
13 | from .proof import proof_open
14 | from .cli.teamsecrets import TeamSecrets
15 |
16 |
17 | TEAM_FILE = 'team.json'
18 | MEMBERS_FILE = 'members.json'
19 | SUBMISSIONS_FILE = 'submissions.csv'
20 |
21 |
22 | class Team(SerializableDict):
23 | def __init__(self, name=None, id=None):
24 | if name:
25 | id = self.name_to_id(name)
26 | self.update({'name': name})
27 | if id:
28 | self.validate_id(id)
29 | self.id = id
30 | else:
31 | raise ValueError('Either name or id are required')
32 |
33 | super(Team, self).__init__()
34 |
35 | if self.exists():
36 | self.validate()
37 |
38 | def dir(self):
39 | return SubRepo.get_path(self.id)
40 |
41 | def path(self):
42 | return os.path.join(self.dir(), TEAM_FILE)
43 |
44 | def save(self):
45 | if not self.exists():
46 | os.makedirs(self.dir())
47 | super(Team, self).save()
48 |
49 | def members(self):
50 | return TeamMembers(self)
51 |
52 | def submissions(self):
53 | return TeamSubmissions(self)
54 |
55 | @staticmethod
56 | def name_to_id(name):
57 | assert isinstance(name, text_type)
58 | sha = hashlib.sha256(name.encode('utf-8')).hexdigest()
59 | return sha[0:1] + '/' + sha[1:4] + '/' + sha[4:]
60 |
61 | @staticmethod
62 | def validate_id(id):
63 | assert isinstance(id, text_type)
64 | if not re.match(r'^[0-9a-f]/[0-9a-f]{3}/[0-9a-f]{60}$', id):
65 | raise ValueError('Invalid Team ID')
66 |
67 | @staticmethod
68 | def _binary_field(k):
69 | return k.endswith('_pk')
70 |
71 | def validate(self):
72 | expected_keys = {'name', 'crypt_pk', 'sign_pk'}
73 | if set(self.keys()) != expected_keys:
74 | raise ValueError("Team should contain, and only contain: %s" %
75 | ', '.join(expected_keys))
76 |
77 | assert isinstance(self['name'], text_type)
78 | if len(self['name']) > Settings.max_size_team_name:
79 | raise ValueError("Team name must have at most %d chars." %
80 | Settings.max_size_team_name)
81 | if self.name_to_id(self['name']) != self.id:
82 | raise ValueError("Team name does not match its ID")
83 |
84 | assert isinstance(self['crypt_pk'], bytes)
85 | if len(self['crypt_pk']) != pysodium.crypto_box_PUBLICKEYBYTES:
86 | raise ValueError("Team's crypt_pk has incorrect size")
87 |
88 | assert isinstance(self['sign_pk'], bytes)
89 | if len(self['sign_pk']) != pysodium.crypto_sign_PUBLICKEYBYTES:
90 | raise ValueError("Team's sign_pk has incorrect size")
91 |
92 |
93 | class TeamMembers(SerializableList):
94 | def __init__(self, team):
95 | self.team = team
96 | self.team_dir = team.dir()
97 | super(TeamMembers, self).__init__()
98 |
99 | def path(self):
100 | return os.path.join(self.team_dir, MEMBERS_FILE)
101 |
102 | def projection(self, attr):
103 | return [member[attr] for member in self]
104 |
105 | def add(self, id=None, username=None):
106 | assert isinstance(id, int) or isinstance(id, long)
107 | assert isinstance(username, text_type)
108 |
109 | another_team = lookup_member(id=id)
110 | if another_team:
111 | if another_team != self.team:
112 | raise ValueError("User '%s' is already member of team '%s'" %
113 | (username, another_team['name']))
114 | else:
115 | # do nothing, but do not fail if it is the same team
116 | return
117 |
118 | self.append({'id': id, 'username': username})
119 | self.save()
120 |
121 |
122 | class TeamSubmissions(object):
123 | def __init__(self, team):
124 | self.team = team
125 | self.path = os.path.join(team.dir(), SUBMISSIONS_FILE)
126 |
127 | def submit(self, proof):
128 | with open(self.path, 'a') as f:
129 | f.write(proof + b'\n')
130 |
131 | def challs(self):
132 | r = []
133 | if os.path.exists(self.path):
134 | with open(self.path) as f:
135 | for proof in f:
136 | r.append(proof_open(self.team, proof.strip()))
137 | if len(set(r)) != len(r):
138 | raise ValueError('Team submissions contain repeated challenges')
139 | return r
140 |
141 |
142 | def my_team():
143 | return Team(id=TeamSecrets['id'])
144 |
145 |
146 | def all_teams():
147 | root = SubRepo.get_path()
148 | for path, dirs, files in os.walk(root):
149 | if TEAM_FILE in files:
150 | assert path.startswith(root)
151 | id = path[len(root):].strip('/')
152 | yield Team(id=id)
153 |
154 |
155 | def lookup_member(id=None, username=None):
156 | if id:
157 | attr = 'id'
158 | value = id
159 | elif username:
160 | attr = 'username'
161 | value = username
162 | else:
163 | raise ValueError('Provide either an id or an username')
164 |
165 | for team in all_teams():
166 | if value in team.members().projection(attr):
167 | return team
168 |
169 | return None
170 |
--------------------------------------------------------------------------------
/nizkctf/repohost/gitlab.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import requests
6 | import hmac
7 | import re
8 | from ..settings import Settings
9 | from ..six import to_bytes
10 | from .common import BaseRepoHost, APIError, WebhookAuthError, quote_plus
11 |
12 |
13 | class GitLabWebhook(object):
14 | @staticmethod
15 | def auth(secret, headers, raw_payload):
16 | received_token = to_bytes(headers['X-Gitlab-Token'])
17 | if not hmac.compare_digest(secret, received_token):
18 | raise WebhookAuthError()
19 |
20 | @staticmethod
21 | def adapt_payload(payload):
22 | # filtering
23 | if payload['object_kind'] != 'merge_request':
24 | return None
25 | if payload['object_attributes']['action'] not in {'open', 'reopen'}:
26 | return None
27 | if payload['object_attributes']['target']['path_with_namespace'] != \
28 | Settings.submissions_project:
29 | return None
30 | if payload['object_attributes']['target_branch'] != 'master':
31 | return None
32 | # mappings
33 | return {"mr_id": payload['object_attributes']['id'],
34 | "source_ssh_url": payload['object_attributes']['source']
35 | ['git_ssh_url'],
36 | "source_commit": payload['object_attributes']['last_commit']
37 | ['id'],
38 | "user_id": payload['object_attributes']['author_id'],
39 | "username": payload['user']['username']}
40 |
41 |
42 | class GitLab(BaseRepoHost):
43 | webhook = GitLabWebhook
44 |
45 | @classmethod
46 | def get_token(cls, username, password):
47 | auth = {'login': username,
48 | 'password': password}
49 |
50 | r = requests.post(Settings.gitlab_api_endpoint +
51 | 'session',
52 | json=auth)
53 | cls._raise_for_status(r)
54 |
55 | data = r.json()
56 | return data['private_token']
57 |
58 | @staticmethod
59 | def get_ssh_url(proj):
60 | return Settings.gitlab_ssh_url % proj
61 |
62 | def fork(self, source):
63 | r = self.s.post(Settings.gitlab_api_endpoint +
64 | 'projects/fork/' + quote_plus(source))
65 |
66 | data = r.json()
67 | if self._has_error(data, 'name', 'has already been taken'):
68 | # Simulate GitHub API behaviour (return already existing fork)
69 | username = self._get_user_namespace()
70 | sink = username + '/' + source.split('/', 2)[1]
71 | sink_proj = self._get_project(sink)
72 | forked_from = sink_proj.get('forked_from_project', {})\
73 | .get('path_with_namespace')
74 | if forked_from != source:
75 | raise APIError("Project '%s' already exists and is not a fork "
76 | "from '%s'. Please remove or rename it to "
77 | "allow a fork to be made." % (sink, source))
78 | data = sink_proj
79 | else:
80 | self._raise_for_status(r)
81 |
82 | return data['path_with_namespace'], data['ssh_url_to_repo']
83 |
84 | def merge_request(self, source, target,
85 | source_branch='master',
86 | target_branch='master',
87 | title='Merge Request'):
88 | target_id = self._get_project(target)['id']
89 |
90 | merge_request = {'source_branch': source_branch,
91 | 'target_branch': target_branch,
92 | 'title': title,
93 | 'target_project_id': target_id}
94 |
95 | r = self.s.post(Settings.gitlab_api_endpoint +
96 | 'projects/' + quote_plus(source) + '/merge_requests',
97 | json=merge_request)
98 | self._raise_for_status(r)
99 | return r.json()
100 |
101 | def mr_comment(self, proj, mr_id, contents):
102 | r = self.s.post(Settings.gitlab_api_endpoint +
103 | 'projects/' + quote_plus(proj) +
104 | '/merge_requests/%d' % mr_id + '/notes',
105 | json={'body': contents})
106 | self._raise_for_status(r)
107 | return r.json()
108 |
109 | def mr_close(self, proj, mr_id):
110 | r = self.s.put(Settings.gitlab_api_endpoint +
111 | 'projects/' + quote_plus(proj) +
112 | '/merge_requests/%d' % mr_id,
113 | json={'state_event': 'close'})
114 | self._raise_for_status(r)
115 | return r.json()
116 |
117 | def mr_accept(self, proj, mr_id, sha):
118 | r = self.s.put(Settings.gitlab_api_endpoint +
119 | 'projects/' + quote_plus(proj) +
120 | '/merge_requests/%d' % mr_id + '/merge',
121 | json={'sha': sha})
122 | self._raise_for_status(r)
123 | return r.json()
124 |
125 | def _get_user_namespace(self):
126 | return (n['path'] for n in self._get_namespaces()
127 | if n['kind'] == 'user').next()
128 |
129 | def _get_namespaces(self):
130 | r = self.s.get(Settings.gitlab_api_endpoint +
131 | 'namespaces')
132 | self._raise_for_status(r)
133 | return r.json()
134 |
135 | def _get_project(self, proj):
136 | r = self.s.get(Settings.gitlab_api_endpoint +
137 | 'projects/' + quote_plus(proj))
138 | self._raise_for_status(r)
139 | return r.json()
140 |
141 | def _init_session(self):
142 | self.s.headers.update({'PRIVATE-TOKEN': self.token})
143 |
144 | @staticmethod
145 | def _has_error(data, key, msg):
146 | return msg in data.get('message', {}).get(key, [])
147 |
--------------------------------------------------------------------------------
/ctf:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 |
4 | from __future__ import unicode_literals, division, print_function,\
5 | absolute_import
6 | import os
7 | import sys
8 | import codecs
9 | import argparse
10 | import getpass
11 | import base64
12 |
13 | from nizkctf.cli import scoreboard, challenges, team
14 | from nizkctf.repohost import RepoHost
15 | from nizkctf.subrepo import SubRepo
16 | from nizkctf.challenge import Challenge, derive_keypair, random_salt
17 | from nizkctf.six import to_unicode, PY2
18 |
19 |
20 | def read_opt(msg, opts):
21 | while True:
22 | inp = raw_input(msg)
23 | if inp.strip() in opts:
24 | return inp.strip()
25 |
26 |
27 | def read_auth(opt):
28 | while True:
29 | if opt == '1':
30 | print('Enter your username and password:')
31 | username = raw_input('Username: ')
32 | password = getpass.getpass('Password: ')
33 | RepoHost.login(username=username, password=password)
34 | return True
35 | elif opt == '2':
36 | print('Enter your auth token:')
37 | token = raw_input('Token: ')
38 | RepoHost.login(token=token)
39 | return True
40 |
41 |
42 | def cmd_scoreboard(args):
43 | if args.pull:
44 | SubRepo.pull()
45 |
46 | ranking, submissions = scoreboard.rank()
47 | scoreboard.pprint(ranking, top=args.top, show_names=args.names)
48 |
49 | if args.chart:
50 | scoreboard.plot(ranking, submissions)
51 |
52 |
53 | def cmd_register(args):
54 | team.register(args.name)
55 |
56 |
57 | def cmd_login(args):
58 | if args.token:
59 | RepoHost.login(token=args.token)
60 | elif args.username and args.password:
61 | RepoHost.login(username=args.username, password=args.password)
62 | else:
63 | read_auth('1')
64 | print('Credentials stored')
65 | print('Cloning submissions repository')
66 | SubRepo.clone()
67 |
68 |
69 | def cmd_init(args):
70 | print('NIZKCTF initializing your environment.')
71 | print('First of all, we need your github/gitlab credentials:')
72 | print('[1] auth via username / password')
73 | print('[2] auth with personal access token')
74 | opt = read_opt('>> ', {'1', '2'})
75 | print('')
76 |
77 | read_auth(opt)
78 | print('')
79 |
80 | print('Cloning submissions repository')
81 | SubRepo.clone()
82 |
83 | print('Do you want to register a new team? [y/n]')
84 | opt = read_opt('>> ', {'y', 'n'})
85 | if opt == 'y':
86 | print('Enter your team name:')
87 | team_name = raw_input('>> ')
88 | team.register(team_name)
89 | print('')
90 |
91 | print('We are all set!')
92 |
93 |
94 | def cmd_submit(args):
95 | print('Checking flag: %s' % args.flag)
96 | ret, msg = challenges.submit_flag(args.flag, args.chall)
97 | print(msg)
98 | if not ret:
99 | sys.exit(1)
100 |
101 |
102 | def cmd_challenges(args):
103 | challenges.pprint()
104 |
105 |
106 | def cmd_add_challenge(args):
107 | id = raw_input('Challenge id (digits, letters, underscore): ').strip()
108 | title = raw_input('Title: ').strip()
109 | description = raw_input('Description: ').strip()
110 | points = int(raw_input('Points: ').strip())
111 | tags = raw_input('Tags (separate tags with space): ').strip().split()
112 | salt = raw_input('Salt (empty string for random salt): ').strip()
113 | if salt == '':
114 | salt = random_salt()
115 | flag = raw_input('Flag: ').strip()
116 |
117 | pk, sk = derive_keypair(salt, flag)
118 | chall = Challenge(id=id)
119 | chall['id'] = id
120 | chall['title'] = title
121 | chall['description'] = description
122 | chall['points'] = points
123 | chall['tags'] = tags
124 | chall['salt'] = salt
125 | chall['pk'] = pk
126 | chall.save()
127 |
128 |
129 | def main():
130 | if PY2:
131 | sys.argv = map(to_unicode, sys.argv)
132 |
133 | commands = {
134 | 'init': cmd_init,
135 | 'login': cmd_login,
136 | 'score': cmd_scoreboard,
137 | 'register': cmd_register,
138 | 'submit': cmd_submit,
139 | 'challs': cmd_challenges,
140 | 'add': cmd_add_challenge,
141 | }
142 |
143 | parser = argparse.ArgumentParser(description='nizk CTF cli')
144 | subparsers = parser.add_subparsers(help='command help')
145 |
146 | parser_init = subparsers.add_parser('init', help='init ctf environment')
147 | parser_init.set_defaults(command='init')
148 |
149 | parser_login = subparsers.add_parser('login',
150 | help='authenticate in gitlab/github')
151 | parser_login.set_defaults(command='login')
152 | parser_login.add_argument('--username', type=str, default=None,
153 | metavar='USERNAME',
154 | help='username for logging in')
155 | parser_login.add_argument('--password', type=str, default=None,
156 | metavar='PASSWORD',
157 | help='password for logging in')
158 | parser_login.add_argument('--token', type=str, default=None,
159 | metavar='TOKEN',
160 | help='use personal access token instead of '
161 | 'username/password')
162 |
163 | parser_score = subparsers.add_parser('score', help='scoreboard help')
164 | parser_score.set_defaults(command='score')
165 | parser_score.add_argument('--top', type=int,
166 | default=0, metavar='N',
167 | help='size of ranking to display')
168 | parser_score.add_argument('--pull', action='store_true',
169 | help='pull submissions before displaying scores')
170 | parser_score.add_argument('--names', action='store_true',
171 | help='display team names')
172 | parser_score.add_argument('--chart', action='store_true',
173 | help='display chart')
174 |
175 | parser_register = subparsers.add_parser('register',
176 | help='register a new team')
177 | parser_register.set_defaults(command='register')
178 | parser_register.add_argument('name', metavar='NAME', help='team name')
179 |
180 | parser_submit = subparsers.add_parser('submit', help='submit a flag')
181 | parser_submit.set_defaults(command='submit')
182 | parser_submit.add_argument('flag', metavar='FLAG', help='flag')
183 | parser_submit.add_argument('--chall', type=str, default=None,
184 | metavar='CHALL_ID', help='challenge id')
185 |
186 | parser_challenges = subparsers.add_parser('challs', help='list challenges')
187 | parser_challenges.set_defaults(command='challs')
188 |
189 | parser_add_challenge = subparsers.add_parser('add', help='add challenge')
190 | parser_add_challenge.set_defaults(command='add')
191 |
192 | args = parser.parse_args()
193 | commands[args.command](args)
194 |
195 | if __name__ == '__main__':
196 | main()
197 |
--------------------------------------------------------------------------------
/nizkctf/proposal.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | import re
7 | import subprocess
8 | from .settings import Settings
9 | from .repohost import RepoHost
10 | from .subrepo import SubRepo
11 | from .team import Team, TEAM_FILE, SUBMISSIONS_FILE
12 | from .acceptedsubmissions import AcceptedSubmissions
13 |
14 |
15 | DIFF_MAX_SIZE = 5000
16 | PUSH_RETRIES = 5
17 |
18 |
19 | def consider_proposal(merge_info):
20 | # Clone official repository
21 | SubRepo.clone(fork=False)
22 |
23 | # Set CI user name / email
24 | setup_user_name_and_email()
25 |
26 | # Fetch proposal
27 | add_proposal_remote(merge_info)
28 |
29 | # Get commits between which to compute diffs
30 | commit = merge_info['source_commit']
31 | merge_base = get_merge_base(commit)
32 |
33 | # Check if there is a single commit in the merge request
34 | check_rev_count(merge_base, commit)
35 | # Check if the diff is not too big
36 | check_diff_size(merge_base, commit)
37 | # Check if only allowed ops were done (add/modify)
38 | check_no_unallowed_ops(merge_base, commit)
39 | # Validate and get files added/modified
40 | added_file = get_added_file(merge_base, commit)
41 | modified_file = get_modified_file(merge_base, commit)
42 |
43 | if added_file and modified_file:
44 | raise ValueError("We only allow commits doing a single operation")
45 |
46 | changed_file = added_file or modified_file
47 | if not changed_file:
48 | raise ValueError("You managed to make a commit which does nothing")
49 | changed_basename = os.path.basename(changed_file)
50 |
51 | if added_file and changed_basename == TEAM_FILE:
52 | team_registration(merge_info, added_file)
53 | elif changed_file and changed_basename == SUBMISSIONS_FILE:
54 | flag_submission(merge_info, changed_file)
55 | else:
56 | raise ValueError("unrecognized operation")
57 |
58 |
59 | def team_registration(merge_info, added_file):
60 | # Checkout first to get the new team file
61 | commit = merge_info['source_commit']
62 | checkout(commit)
63 |
64 | team = filename_owner(added_file)
65 | team.validate()
66 |
67 | def local_changes():
68 | # Back to branch, do local modifications
69 | checkout('master')
70 | add_member(team, merge_info)
71 |
72 | local_changes() # Validate local modifications before accepting MR
73 | accept_proposal(merge_info)
74 |
75 | for _ in retry_push('Add member who registered team'):
76 | local_changes()
77 |
78 |
79 | def flag_submission(merge_info, modified_file):
80 | team = filename_owner(modified_file)
81 | challs_before = set(team.submissions().challs())
82 |
83 | # Checkout to get the newly submitted challenge
84 | commit = merge_info['source_commit']
85 | checkout(commit)
86 |
87 | challs_after = set(team.submissions().challs())
88 |
89 | new_challs = challs_after - challs_before
90 | assert len(new_challs) == 1
91 | chall, = new_challs
92 |
93 | def local_changes():
94 | # Back to branch, do local modifications
95 | checkout('master')
96 | add_member(team, merge_info)
97 | AcceptedSubmissions().add(chall.id, chall['points'], team.id)
98 |
99 | local_changes() # Validate local modifications before accepting MR
100 | accept_proposal(merge_info)
101 |
102 | for _ in retry_push('Accept challenge solution'):
103 | local_changes()
104 |
105 |
106 | def add_member(team, merge_info):
107 | team_dir = team.dir()
108 | if not os.path.exists(team_dir):
109 | os.makedirs(team_dir)
110 |
111 | team.members().add(id=merge_info['user_id'],
112 | username=merge_info['username'])
113 |
114 |
115 | def accept_proposal(merge_info):
116 | proj = Settings.submissions_project
117 | mr_id = merge_info['mr_id']
118 | commit = merge_info['source_commit']
119 |
120 | repohost = RepoHost.instance()
121 | repohost.mr_accept(proj, mr_id, commit)
122 |
123 |
124 | def retry_push(commit_message, retries=PUSH_RETRIES):
125 | for retry in range(retries):
126 | try:
127 | checkout('master')
128 | SubRepo.git(['reset', '--hard', 'upstream/master'])
129 | SubRepo.pull()
130 | yield retry # do local modifications
131 | SubRepo.push(commit_message, merge_request=False)
132 | break
133 | except:
134 | if retry == retries - 1:
135 | raise
136 |
137 |
138 | def filename_owner(filename):
139 | team_id, basename = os.path.split(filename)
140 | return Team(id=team_id)
141 |
142 |
143 | def add_proposal_remote(merge_info):
144 | url = merge_info['source_ssh_url']
145 | SubRepo.git(['remote', 'add', 'proposal', url])
146 | SubRepo.git(['fetch', '--all'])
147 |
148 |
149 | def setup_user_name_and_email():
150 | ci_user_name = os.getenv('CI_USER_NAME')
151 | ci_user_email = os.getenv('CI_USER_EMAIL')
152 | if ci_user_name:
153 | SubRepo.git(['config', 'user.name', ci_user_name])
154 | if ci_user_email:
155 | SubRepo.git(['config', 'user.email', ci_user_email])
156 |
157 |
158 | def checkout(commit):
159 | SubRepo.git(['checkout', commit])
160 |
161 |
162 | def get_added_file(src, dest):
163 | return get_file(src, dest, 'A', {TEAM_FILE, SUBMISSIONS_FILE})
164 |
165 |
166 | def get_modified_file(src, dest):
167 | return get_file(src, dest, 'M', {SUBMISSIONS_FILE})
168 |
169 |
170 | def get_file(src, dest, filt, whitelist):
171 | stats = diff_stats(src, dest, ['--diff-filter=' + filt])
172 | if len(stats) == 0:
173 | return None
174 | if len(stats) != 1:
175 | raise ValueError("We only allow a single file to be added or modified "
176 | "per commit")
177 |
178 | stat, = stats
179 | lines_added, lines_removed, filename = stat
180 | if lines_removed != 0:
181 | raise ValueError("We do not allow lines to be removed from files")
182 | if lines_added != 1:
183 | raise ValueError("Changes can only add a single line to a file")
184 |
185 | check_whitelist(filename, whitelist)
186 | return filename
187 |
188 |
189 | def check_no_unallowed_ops(src, dest):
190 | stats = diff_stats(src, dest, ['--diff-filter=am'])
191 | if len(stats) != 0:
192 | raise ValueError("We only allow files to be added or modified")
193 |
194 |
195 | def check_whitelist(filename, whitelist):
196 | basename = os.path.basename(filename)
197 | if basename not in whitelist:
198 | raise ValueError("Filename '%s' not in the whitelist" % basename)
199 |
200 |
201 | def diff_stats(src, dest, args=[]):
202 | stats = SubRepo.git(['diff', '--numstat'] + args + [src, dest],
203 | stdout=subprocess.PIPE)
204 | lines = [re.split(r'\s+', line.strip(), 2) for line in
205 | stats.split('\n')]
206 | lines = [line for line in lines if line != ['']]
207 | return [(int(lines_added), int(lines_removed), filename)
208 | for lines_added, lines_removed, filename
209 | in lines]
210 |
211 |
212 | def check_rev_count(src, dest):
213 | revs = int(SubRepo.git(['rev-list', '--count', src+'...'+dest],
214 | stdout=subprocess.PIPE).strip())
215 |
216 | if revs != 1:
217 | raise ValueError("We only accept a single commit per merge request")
218 |
219 |
220 | def check_diff_size(src, dest):
221 | diff = SubRepo.git(['diff', '--no-color', '-U0', src, dest],
222 | stdout=subprocess.PIPE)
223 |
224 | if len(diff) > DIFF_MAX_SIZE:
225 | raise ValueError("Diff size (%d bytes) is above the maximum permitted "
226 | "(%d bytes)" % (len(diff), DIFF_MAX_SIZE))
227 |
228 |
229 | def get_merge_base(commit):
230 | merge_base = SubRepo.git(['merge-base', 'upstream/master', commit],
231 | stdout=subprocess.PIPE).strip()
232 | return merge_base
233 |
--------------------------------------------------------------------------------