├── .nojekyll
├── frontend
├── js
│ ├── services.js
│ ├── story.js
│ ├── settings.js
│ ├── app.js
│ ├── functions.js
│ ├── team.js
│ ├── rank.js
│ ├── home.js
│ └── challenges.js
├── locales
│ ├── en.js
│ └── pt.js
└── css
│ └── styles.css
├── nizkctf
├── __init__.py
├── cli
│ ├── __init__.py
│ ├── log.py
│ ├── teamsecrets.py
│ ├── team.py
│ ├── challenges.py
│ ├── localserver.py
│ ├── news.py
│ └── scoreboard.py
├── text.py
├── repohost
│ ├── __init__.py
│ ├── common.py
│ ├── github.py
│ └── gitlab.py
├── scoring.py
├── settings.py
├── six.py
├── localsettings.py
├── gen_iso3166
├── news.py
├── proof.py
├── serializable.py
├── iso3166.py
├── acceptedsubmissions.py
├── challenge.py
├── subrepo.py
├── team.py
└── proposal.py
├── pip-requirements.txt
├── challenges
├── index.json
├── test1.en.md
├── test2.en.md
├── test1.pt.md
├── g00d_b0y.en.md
├── g00d_b0y.pt.md
├── test2.pt.md
├── test1.json
├── test2.json
└── test3.json
├── .gitignore
├── Makefile
├── readme-fixer.sh
├── settings.json
├── README.md
├── known_hosts
├── setup-vpn
├── example-README.pt.md
├── index.html
├── example-README.en.md
├── lambda_function.py
└── ctf
/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/js/services.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nizkctf/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nizkctf/cli/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pip-requirements.txt:
--------------------------------------------------------------------------------
1 | requests[security] >= 2.13.0
2 | pysodium >= 0.7.4.1
3 |
--------------------------------------------------------------------------------
/challenges/index.json:
--------------------------------------------------------------------------------
1 | [
2 | "test1",
3 | "test2",
4 | "test3"
5 | ]
6 |
--------------------------------------------------------------------------------
/challenges/test1.en.md:
--------------------------------------------------------------------------------
1 | Just a fake challenge for homologation purposes. Flag: CTF-BR{oi}
2 |
--------------------------------------------------------------------------------
/challenges/test2.en.md:
--------------------------------------------------------------------------------
1 | Just another fake challenge for homologation purposes. Flag: CTF-BR{hi}
2 |
--------------------------------------------------------------------------------
/challenges/test1.pt.md:
--------------------------------------------------------------------------------
1 | Apenas um challenge genérico para propósitos de homologação. Flag: CTF-BR{oi}
2 |
--------------------------------------------------------------------------------
/challenges/g00d_b0y.en.md:
--------------------------------------------------------------------------------
1 | Now prove you were a good kid and show you learned the most basic lesson in CTFs!!
2 |
--------------------------------------------------------------------------------
/challenges/g00d_b0y.pt.md:
--------------------------------------------------------------------------------
1 | Dessa vez, prove que você foi um bom garoto e aprendeu a lição básica dos CTFs!!
2 |
--------------------------------------------------------------------------------
/challenges/test2.pt.md:
--------------------------------------------------------------------------------
1 | Just another fake challenge for homologation purposes. Flag: CTF-BR{hi}
2 | Apenas mais um challenge genérico para homologação. Flag: CTF-BR{hi}
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | @echo nothing yet
3 |
4 | nizkctf/iso3166.py: nizkctf/gen_iso3166 settings.json
5 | PYTHONPATH="." $< >$@
6 |
7 | ../lambda.zip: FORCE
8 | rm -f "$@"
9 | zip -ry "$@" * -x 'submissions/*'
10 |
11 | FORCE:
12 |
--------------------------------------------------------------------------------
/challenges/test1.json:
--------------------------------------------------------------------------------
1 | {"description": "Just a fake challenge for homologation purposes. Flag: CTF-BR{oi}", "title": "Test 01", "tags": ["Test"], "points": 20, "pk": "Z8rrCSjJnGRalHKc41aSUj9T2Opt88ckxnvJDWWbZZY=", "salt": "EjJvA9Ee/EPxjE1IGSM8lElae6YmCw5Fa/V4v+LY9PM=", "id": "test1"}
2 |
--------------------------------------------------------------------------------
/challenges/test2.json:
--------------------------------------------------------------------------------
1 | {"description": "Just another fake challenge for homologation purposes. Flag: CTF-BR{hi}", "title": "Test 02", "tags": ["Test"], "points": 25, "pk": "E7dq1U5GT39Y1kFfcdAq3qncNjKto++867xIpn/UTqc=", "salt": "ykmTVU5lEX2DLDWOv3YTOkyWCkXiqnSXFbPQwDDkX/w=", "id": "test2"}
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/challenges/test3.json:
--------------------------------------------------------------------------------
1 | {"id": "test3", "title": "Test 03 (dynamic score)", "description": "This is a fake challenge to test static and dynamic scores on the same repo. Flag: CTF-BR{10pts_to_gryffindor}", "tags": ["Test", "Dynamic_points"], "salt": "au3TpnX+QgHx0Jop+ppKBmLqGQ+5UTh/FEktVTSuhOo=", "pk": "BhI0OzpzZWrw/QZ9AACtfLThNDrqpzo8gThPENHrfR0="}
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/nizkctf/scoring.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from .settings import Settings
4 | from math import floor, log
5 |
6 |
7 | def compute_points(chall, num_solves):
8 | num_solves = max(1, num_solves)
9 |
10 | params = Settings.dynamic_scoring
11 | if not params or 'points' in chall.keys():
12 | return chall['points']
13 |
14 | # Google CTF 2017's formula
15 | K, V, minpts, maxpts = params['K'], params['V'], \
16 | params['minpts'], params['maxpts']
17 | return int(max(minpts, floor(maxpts - K*log((num_solves + V)/(1 + V), 2))))
18 |
--------------------------------------------------------------------------------
/readme-fixer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "Organization name (As in github. Ex. pwn2winctf): "
4 | read org
5 | echo "CTF name (ex. Pwn2Win 2019): "
6 | read name
7 | echo "CTF id/repo name (ex. 2019): "
8 | read ctf
9 | echo "Flag format example (ex. CTF-BR{fl4g}): "
10 | read flag
11 |
12 | sed -i 1,2d example-README.*.md
13 | sed -i 's/pwn2winctf/'$org'/Ig' example-README.*.md
14 | sed -i 's/NIZKCTF\ example\ CTF/'"$name"'/Ig' example-README.*.md
15 | sed -i 's/nizkctf/'$ctf'/Ig' example-README.*.md
16 | sed -i 's/CTF-BR{flag123}/'"$flag"'/Ig' example-README.*.md
17 |
18 | mv {example-,}README.en.md
19 | mv {example-,}README.pt.md
20 |
21 | echo "Done!"
22 |
--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ctf_name": "NIZKCTF upstream",
3 | "repository_host": "GitHub",
4 | "submissions_project": "pwn2winctf/test_submissions",
5 |
6 | "scrypt_ops_limit": 33554432,
7 | "scrypt_mem_limit": 402653184,
8 |
9 | "dynamic_scoring": {"K": 80.0, "V": 3.0, "minpts": 50, "maxpts": 500},
10 |
11 | "max_size_team_name": 50,
12 | "max_size_chall_id": 30,
13 | "max_size_team_countries": 5,
14 | "flag_icon_css_ver": "2.8.0",
15 |
16 | "github_api_endpoint": "https://api.github.com/",
17 | "github_ssh_url": "git@github.com:%s.git",
18 |
19 | "gitlab_api_endpoint": "https://gitlab.com/api/v4/",
20 | "gitlab_ssh_url": "git@gitlab.com:%s.git"
21 | }
22 |
--------------------------------------------------------------------------------
/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, os.pardir, '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 |
24 | load()
25 |
--------------------------------------------------------------------------------
/frontend/locales/en.js:
--------------------------------------------------------------------------------
1 | const enLocale = {
2 | 'home': 'Home',
3 | 'challenges': 'Challenges',
4 | 'rank': 'Rank',
5 | 'rules': 'Rules',
6 | 'settings': 'Settings',
7 | 'news': 'News',
8 | 'messages': 'Messages',
9 | 'solves': 'Solves',
10 | 'private-message': 'Your team has a new private msg, use console to read',
11 | 'solved': 'solved',
12 | 'total-solves': 'Total solves',
13 | 'score': 'Score',
14 | 'categories': 'Categories',
15 | 'isolated': 'Isolated',
16 | 'platform': 'Platform',
17 | 'position': 'Position',
18 | 'members': 'Members',
19 | 'solved-challenges': 'Solved Challenges',
20 | 'team': 'Team',
21 | 'language': 'Language'
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/locales/pt.js:
--------------------------------------------------------------------------------
1 | const ptLocale = {
2 | 'home': 'Home',
3 | 'challenges': 'Desafios',
4 | 'rank': 'Rank',
5 | 'rules': 'Regras',
6 | 'settings': 'Configurações',
7 | 'news': 'Notícias',
8 | 'messages': 'Mensagens',
9 | 'solves': 'Resolvidos',
10 | 'private-message': 'Seu time tem uma nova mensagem, use o console para lê-la.',
11 | 'solved': 'resolveu',
12 | 'total-solves': 'Total de soluções',
13 | 'score': 'Pontos',
14 | 'categories': 'Categorias',
15 | 'isolated': 'Isolado',
16 | 'platform': 'Plataforma',
17 | 'position': 'Posição',
18 | 'members': 'Membros',
19 | 'solved-challenges': 'Desafios Resolvidos',
20 | 'team': 'Time',
21 | 'language': 'Língua'
22 | }
--------------------------------------------------------------------------------
/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 | input = raw_input
18 | else:
19 | viewitems = operator.methodcaller("items")
20 | input = input
21 |
22 |
23 | def to_bytes(s):
24 | if isinstance(s, text_type):
25 | return bytes(s.encode('utf-8'))
26 | return s
27 |
28 |
29 | def to_unicode(s):
30 | if isinstance(s, text_type):
31 | return s
32 | encoding = sys.getfilesystemencoding()
33 | s = s.decode(encoding)
34 | return s
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NIZKCTF upstream repository
2 |
3 | This is the upstream repository for the Non-Interactive Zero-Knowledge CTF platform, a distributed and openly auditable CTF platform that uses zero-knowledge cryptographic proofs to submit and verify the flags for Capture The Flag competitions. It works from command line, but we also have a [javascript version](https://github.com/pwn2winctf/NIZKCTF-js) to have an easier and more friendly interface.
4 |
5 | The paper describing the platform can be found [here](https://arxiv.org/pdf/1708.05844.pdf).
6 |
7 | If you want to host your CTF using the platform, fork this repository and follow the instructions on the [NIZKCTF-tutorial repository](https://github.com/pwn2winctf/nizkctf-tutorial). We have also developed a [dynamic provisioner](https://github.com/pwn2winctf/NIZKCTF-provisioning) for isolated challenges if you need it. Contact us if you need any help.
8 |
9 |
--------------------------------------------------------------------------------
/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 | LOCALSETTINGS_FILE = 'local-settings.json'
11 |
12 |
13 | class DefaultLocalSettings(object):
14 | __lock__ = threading.Lock()
15 |
16 | def path(self):
17 | thisdir = os.path.dirname(os.path.realpath(__file__))
18 | return os.path.join(thisdir, os.pardir, LOCALSETTINGS_FILE)
19 |
20 | def __init__(self):
21 | if os.path.exists(self.path()):
22 | with open(self.path()) as f:
23 | self.__dict__.update(json.load(f))
24 |
25 | def __setattr__(self, k, v):
26 | self.__dict__[k] = v
27 | with self.__lock__:
28 | with open(self.path(), 'w') as f:
29 | json.dump(self.__dict__, f)
30 |
31 |
32 | LocalSettings = DefaultLocalSettings()
33 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/js/story.js:
--------------------------------------------------------------------------------
1 | const ChallengeComponent = Vue.component('challenge-card', {
2 | template: `
3 |
4 |
5 |
6 |
{{challenge.title}}
7 |
{{challenge.description.substr(0,100)}}...
8 |
9 | {{tag}}
10 |
11 |
12 |
15 |
16 |
17 | `,
18 | props: ['challenge']
19 | });
20 |
21 | const Challenges = {
22 | template: `
23 |
24 |
25 |
26 | `,
27 | data: () => ({
28 | challenges
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/js/settings.js:
--------------------------------------------------------------------------------
1 | const Settings = Vue.component('settings', {
2 | template: `
3 |
4 |
5 |
{{$t('team')}}:
6 |
7 | {{$t('language')}}:
8 |
12 |
13 |
Save
14 |
15 | `,
16 | data: () => ({
17 | team: Cookies.get('team'),
18 | language: Cookies.get('lang') || 'En'
19 | }),
20 | methods: {
21 | save() {
22 | const lang = $('select').val();
23 | Cookies.set('team', this.team);
24 | Cookies.set('lang', lang);
25 | app.$i18n.locale = lang;
26 | }
27 | },
28 | mounted() {
29 | $(document).ready(function() {
30 | $('select').material_select();
31 | });
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/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, os.pardir, os.pardir, 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 |
--------------------------------------------------------------------------------
/frontend/css/styles.css:
--------------------------------------------------------------------------------
1 | .news {
2 | height: 216px;
3 | word-wrap: break-word;
4 | overflow: auto;
5 | }
6 |
7 | .news-priv {
8 | color: red;
9 | font-weight: bold;
10 | }
11 |
12 | .card-content {
13 | height: 145px;
14 | }
15 |
16 | .card .card-content {
17 | padding: 10px;
18 | }
19 |
20 | .card .card-action {
21 | padding: 10px 10px 1px 10px;
22 | }
23 |
24 | .card .row {
25 | margin: 0 0 10px 0;
26 | }
27 |
28 | .clickable {
29 | cursor: pointer;
30 | }
31 |
32 | .btn-small {
33 | width: 30px;
34 | height: 30px;
35 | }
36 |
37 | .icon-header {
38 | font-size: 20px !important;
39 | line-height: 30px !important;
40 | }
41 |
42 | .input-header input {
43 | font-weight: bold;
44 | font-size: 15px !important;
45 | padding-left: 0px !important;
46 | width: 150px;
47 | }
48 |
49 | .team-selected {
50 | background-color: #ccc !important;
51 | }
52 |
53 | .blue-grey.darken-1.is-solved {
54 | background-color: green !important;
55 | }
56 |
57 | .country-flag {
58 | width: 25px;
59 | padding: 2px;
60 | }
61 |
62 | .categories {
63 | text-align: center;
64 | }
65 |
66 | .categories span.badge {
67 | float: initial;
68 | padding: 3px 6px;
69 | cursor: pointer;
70 | }
--------------------------------------------------------------------------------
/nizkctf/gen_iso3166:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 |
4 | from __future__ import unicode_literals, division, print_function,\
5 | absolute_import
6 |
7 | from nizkctf.settings import Settings
8 | import os
9 | import json
10 | import string
11 | import requests
12 |
13 |
14 | def iter_country_codes():
15 | r = requests.get('https://api.github.com/repos/lipis/flag-icon-css'
16 | '/contents/flags/1x1?ref=%s' % Settings.flag_icon_css_ver)
17 | r.raise_for_status()
18 | for item in r.json():
19 | if item['type'] == 'file':
20 | basename, ext = os.path.splitext(item['name'])
21 | if ext == '.svg':
22 | yield basename
23 |
24 |
25 | def gen_code():
26 | allowed_chars = set(string.ascii_lowercase).union(set('-'))
27 |
28 | py = 'valid_countries = set(['
29 | i = 0
30 |
31 | for country_code in iter_country_codes():
32 | if i % 8 == 0:
33 | py += '\n '
34 |
35 | assert set(country_code).issubset(allowed_chars)
36 | py += " '%s'," % country_code
37 |
38 | i += 1
39 |
40 | py += '\n])'
41 | return py
42 |
43 |
44 | def main():
45 | print(gen_code())
46 |
47 |
48 | if __name__ == '__main__':
49 | main()
50 |
--------------------------------------------------------------------------------
/nizkctf/news.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 | import pysodium
8 | from base64 import b64encode
9 | from .team import Team
10 | from .six import text_type
11 | from .subrepo import SubRepo
12 | from .serializable import SerializableList
13 |
14 |
15 | NEWS_FILE = 'news.json'
16 |
17 |
18 | class News(SerializableList):
19 | pretty_print = True
20 |
21 | def __init__(self):
22 | super(News, self).__init__()
23 |
24 | def path(self):
25 | return SubRepo.get_path(NEWS_FILE)
26 |
27 | def add(self, msg_text, to=None):
28 | current_time = int(time.time())
29 | if to is None:
30 | message = {"msg": msg_text, "time": current_time}
31 | else:
32 | team_pk = Team(name=to)['crypt_pk']
33 | encrypted_msg = pysodium.crypto_box_seal(msg_text.encode("utf-8"),
34 | team_pk)
35 | encoded_msg = b64encode(encrypted_msg)
36 |
37 | message = {"msg": encoded_msg.decode("utf-8"),
38 | "to": to,
39 | "time": current_time}
40 |
41 | self.append(message)
42 | self.save()
43 |
--------------------------------------------------------------------------------
/frontend/js/app.js:
--------------------------------------------------------------------------------
1 | Vue.use(VueI18n);
2 | if (!Cookies.get('lang')) {
3 | Cookies.set('lang', 'En');
4 | }
5 | const routes = [
6 | {
7 | path: '/',
8 | component: Home
9 | },
10 | {
11 | path: '/challenges',
12 | component: Challenges
13 | },
14 | {
15 | path: '/rank',
16 | component: Rank
17 | },
18 | {
19 | path: '/team/:name',
20 | component: Team
21 | },
22 | {
23 | path: '/settings',
24 | component: Settings
25 | }
26 | ];
27 |
28 | const router = new VueRouter({
29 | routes
30 | });
31 |
32 | const Title = Vue.component('app-title', {
33 | template: `
34 |
39 | `,
40 | props: ['title']
41 | })
42 |
43 | const app = new Vue({
44 | router,
45 | data: () => ({
46 | loaded: false
47 | }),
48 | async mounted() {
49 | settings = await getSettings();
50 | this.loaded = true;
51 | },
52 | i18n: new VueI18n({
53 | locale: Cookies.get('lang'),
54 | fallbackLocale: 'En',
55 | messages: {
56 | En: enLocale,
57 | Pt: ptLocale
58 | }
59 | })
60 | }).$mount('#app');
61 |
--------------------------------------------------------------------------------
/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 | assert isinstance(proof, bytes)
15 | proof = b64decode(proof)
16 |
17 | claimed_chall_id = proof[2*64:].decode('utf-8')
18 | claimed_chall = Challenge(claimed_chall_id)
19 |
20 | chall_pk = claimed_chall['pk']
21 | team_pk = team['sign_pk']
22 |
23 | membership_proof = pysodium.crypto_sign_open(proof, chall_pk)
24 | chall_id = pysodium.crypto_sign_open(membership_proof,
25 | team_pk).decode('utf-8')
26 |
27 | if claimed_chall_id != chall_id:
28 | raise ValueError('invalid proof')
29 |
30 | return claimed_chall
31 |
32 |
33 | def proof_create(chall_id, chall_sk):
34 | chall_id = chall_id.encode('utf-8')
35 |
36 | assert isinstance(chall_id, bytes)
37 | assert isinstance(chall_sk, bytes)
38 |
39 | team_sk = TeamSecrets['sign_sk']
40 |
41 | membership_proof = pysodium.crypto_sign(chall_id, team_sk)
42 | proof = pysodium.crypto_sign(membership_proof, chall_sk)
43 |
44 | return b64encode(proof)
45 |
--------------------------------------------------------------------------------
/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, countries):
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({'countries': countries,
38 | 'crypt_pk': crypt_pk,
39 | 'sign_pk': sign_pk})
40 | team.validate()
41 | team.save()
42 |
43 | SubRepo.push(commit_message='Register team %s' % team_name)
44 | log.success('team %s added successfully' % team_name)
45 |
46 | write_team_secrets(team.id, crypt_sk, sign_sk)
47 |
48 | return True
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, OTP=None):
15 | if not token and (username and password):
16 | token = cls.get_token(username, password, OTP)
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, OTP):
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 | pretty_print = False
13 |
14 | def __init__(self):
15 | self.load()
16 |
17 | def load(self):
18 | if self.exists():
19 | self.clear()
20 | with open(self.path()) as f:
21 | self.update(json.load(f))
22 | self._unserialize_inplace()
23 |
24 | def save(self):
25 | with open(self.path(), 'w') as f:
26 | kw = {'sort_keys': True, 'indent': 1} if self.pretty_print else {}
27 | json.dump(self._serialize(), f, **kw)
28 |
29 | def exists(self):
30 | return os.path.exists(self.path())
31 |
32 | def _unserialize_inplace(self):
33 | pass
34 |
35 | def _serialize(self):
36 | return self
37 |
38 |
39 | class SerializableDict(Serializable, dict):
40 | @staticmethod
41 | def _binary_field(k):
42 | return False
43 |
44 | def _unserialize_inplace(self):
45 | for k, v in viewitems(self):
46 | if self._binary_field(k):
47 | assert isinstance(v, text_type)
48 | self[k] = b64decode(v)
49 |
50 | def _serialize(self):
51 | return {k: b64encode(v).decode('utf-8') if self._binary_field(k) else v
52 | for k, v in viewitems(self)}
53 |
54 |
55 | class SerializableList(Serializable, list):
56 | def clear(self):
57 | del self[:]
58 |
59 | def update(self, l):
60 | self += l
61 |
--------------------------------------------------------------------------------
/nizkctf/iso3166.py:
--------------------------------------------------------------------------------
1 | valid_countries = set([
2 | 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'ao',
3 | 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az',
4 | 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi',
5 | 'bj', 'bl', 'bm', 'bn', 'bo', 'bq', 'br', 'bs',
6 | 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', 'cd',
7 | 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn',
8 | 'co', 'cr', 'cu', 'cv', 'cw', 'cx', 'cy', 'cz',
9 | 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee',
10 | 'eg', 'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj',
11 | 'fk', 'fm', 'fo', 'fr', 'ga', 'gb-eng', 'gb-nir', 'gb-sct',
12 | 'gb-wls', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi',
13 | 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt',
14 | 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht',
15 | 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq',
16 | 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke',
17 | 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw',
18 | 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr',
19 | 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
20 | 'me', 'mf', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn',
21 | 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv',
22 | 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
23 | 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz',
24 | 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
25 | 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa',
26 | 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc',
27 | 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl',
28 | 'sm', 'sn', 'so', 'sr', 'ss', 'st', 'sv', 'sx',
29 | 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj',
30 | 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv',
31 | 'tw', 'tz', 'ua', 'ug', 'um', 'un', 'us', 'uy',
32 | 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu',
33 | 'wf', 'ws', 'ye', 'yt', 'za', 'zm', 'zw',
34 | ])
35 |
--------------------------------------------------------------------------------
/frontend/js/functions.js:
--------------------------------------------------------------------------------
1 | const createPooling = (promise, cb, intervalTime) => {
2 | let interval;
3 | return {
4 | isStarted: false,
5 | async start() {
6 | if (this.isStarted) {
7 | return;
8 | }
9 |
10 | this.isStarted = true;
11 |
12 | cb(await promise());
13 | interval = setInterval(async () => {
14 | cb(await promise());
15 | }, intervalTime || 60000);
16 | },
17 | stop () {
18 | this.isStarted = false
19 | clearInterval(interval);
20 | }
21 | }
22 | };
23 |
24 | const converter = new showdown.Converter();
25 | const getSubmisionsPath = () => settings.submissions_project.split('/')[1];
26 | const getTeamPath = teamName => sha256(teamName).splice(1, 0, '/').splice(5, 0, '/');
27 | const mountUrl = (path, time = (1000 * 60 * 10)) => `${path}?_${Math.floor(+(new Date)/time)}`
28 |
29 | const getSettings = () => $.getJSON('settings.json');
30 | const getNews = () => $.getJSON(mountUrl(`/${getSubmisionsPath()}/news.json`));
31 | const getChallenges = () => $.getJSON(mountUrl('challenges/index.json'));
32 | const getChallenge = id => $.getJSON(mountUrl(`challenges/${id}.json`));
33 | const getChallengeDescription = (id, lang) => $.get(mountUrl(`challenges/${id}.${lang.toLowerCase()}.md`));
34 | const getSolvedChallenges = () => $.getJSON(mountUrl(`/${getSubmisionsPath()}/accepted-submissions.json`, 1000 * 60));
35 | const getTeam = teamName => $.getJSON(mountUrl(`/${getSubmisionsPath()}/${getTeamPath(teamName)}/team.json`));
36 | const getTeamMembers = teamName => $.getJSON(mountUrl(`/${getSubmisionsPath()}/${getTeamPath(teamName)}/members.json`));
37 | const getLocaleMessages = lang => $.getJSON(mountUrl(`frontend/locales/${lang}.json`));
38 |
39 | String.prototype.splice = function(idx, rem, str) {
40 | return this.slice(0, idx) + str + this.slice(idx + Math.abs(rem));
41 | };
42 |
--------------------------------------------------------------------------------
/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 | from ..acceptedsubmissions import AcceptedSubmissions
19 |
20 |
21 | def submit_flag(flag, chall_id=None):
22 | chall, chall_sk = lookup_flag(flag, chall_id)
23 |
24 | if chall_sk is None:
25 | return False, 'This is not the correct flag.'
26 |
27 | SubRepo.pull()
28 |
29 | submissions = my_team().submissions()
30 | if chall in submissions.challs():
31 | return False, 'Your team already solved %s.' % chall.id
32 |
33 | proof = proof_create(chall.id, chall_sk)
34 | submissions.submit(proof)
35 | SubRepo.push(commit_message='Proof: found flag for %s' % chall.id)
36 |
37 | return True, 'Congratulations! You found the right flag for %s.' % chall.id
38 |
39 |
40 | LINE_WIDTH = 72
41 |
42 |
43 | def pprint():
44 | print('')
45 | print('-'*LINE_WIDTH)
46 | print('')
47 | submissions = AcceptedSubmissions()
48 | for chall_id in Challenge.index():
49 | chall = Challenge(chall_id)
50 | print('ID: %s (%d points) [%s]' % (
51 | chall.id,
52 | submissions.compute_points(chall, additional_solves=1),
53 | ', '.join(chall['tags'])))
54 | print('')
55 | print(chall['title'])
56 | print('')
57 | print('\n'.join(textwrap.wrap(chall.description(),
58 | LINE_WIDTH,
59 | replace_whitespace=False,
60 | drop_whitespace=False,
61 | break_on_hyphens=False)))
62 | print('')
63 | print('-'*LINE_WIDTH)
64 | print('')
65 |
--------------------------------------------------------------------------------
/frontend/js/team.js:
--------------------------------------------------------------------------------
1 | const Team = Vue.component('team', {
2 | template: `
3 |
4 |
5 |
{{team.name}}
6 |
{{$t('position')}}: {{teamRank.pos}}
7 |
{{$t('score')}}: {{teamRank.score}}
8 |
Crypt PK: {{team.crypt_pk}}
9 |
Sign PK: {{team.sign_pk}}
10 |
{{$t('members')}}
11 |
12 | -
13 | {{index + 1}}. {{member.username}}
14 |
15 |
16 |
{{$t('solved-challenges')}}
17 |
18 | -
19 | {{index + 1}}. {{key}}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | `,
28 | data: () => ({
29 | loaded: false
30 | }),
31 | methods: {
32 | loadTeam: async function(teamName) {
33 | return getTeam(teamName);
34 | },
35 | loadMembers: async function(teamName) {
36 | return getTeamMembers(teamName);
37 | },
38 | loadTeamRank: async function(teamName) {
39 | const solvedChallenges = await getSolvedChallenges()
40 | return solvedChallenges.standings.filter(teamRank => teamRank.team === teamName)[0];
41 | }
42 | },
43 | mounted: async function() {
44 | await Promise.all([
45 | this.loadTeamRank(this.$route.params.name)
46 | .then(teamRank => { this.teamRank = teamRank }),
47 | this.loadTeam(this.$route.params.name)
48 | .then(team => { this.team = team }),
49 | this.loadMembers(this.$route.params.name)
50 | .then(members => { this.members = members })
51 | ]);
52 | this.loaded = true;
53 | }
54 | });
55 |
--------------------------------------------------------------------------------
/nizkctf/cli/localserver.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | from __future__ import unicode_literals, division, print_function,\
4 | absolute_import
5 | import os
6 | try:
7 | from BaseHTTPServer import HTTPServer
8 | from SimpleHTTPServer import SimpleHTTPRequestHandler
9 | except ImportError:
10 | from http.server import SimpleHTTPRequestHandler, HTTPServer
11 | from ..settings import Settings
12 | from ..subrepo import SubRepo
13 | from .teamsecrets import TeamSecrets
14 | from ..localsettings import LocalSettings
15 |
16 |
17 | def handler(routes, root_redir=None, forbidden=set()):
18 | forbidden = set(os.path.realpath(path) for path in forbidden)
19 |
20 | class RequestHandler(SimpleHTTPRequestHandler):
21 | protocol_version = 'HTTP/1.0'
22 |
23 | def do_GET(self):
24 | if root_redir and self.path == '/':
25 | self.send_response(301)
26 | self.send_header("Location", root_redir)
27 | self.end_headers()
28 | return
29 | SimpleHTTPRequestHandler.do_GET(self)
30 |
31 | def translate_path(self, path):
32 | path = path.split('?', 1)[0]
33 | path = path.split('#', 1)[0]
34 |
35 | root = None
36 | for url_prefix, cur_root in routes:
37 | if path.startswith(url_prefix):
38 | root = cur_root
39 | path = path[len(url_prefix):]
40 | break
41 | if not root:
42 | return ''
43 |
44 | os.chdir(root)
45 | path = SimpleHTTPRequestHandler.translate_path(self, path)
46 | if path in forbidden:
47 | return ''
48 | return path
49 |
50 | return RequestHandler
51 |
52 |
53 | def main(port=8000):
54 | thisdir = os.path.dirname(os.path.realpath(__file__))
55 | rootdir = os.path.realpath(os.path.join(thisdir, os.pardir, os.pardir))
56 | subdir = SubRepo.get_path()
57 |
58 | ctf = os.path.basename(rootdir)
59 | submissions = os.path.basename(Settings.submissions_project)
60 |
61 | routes = [
62 | ('/%s' % ctf, rootdir),
63 | ('/%s' % submissions, subdir),
64 | ]
65 |
66 | forbidden = {
67 | LocalSettings.path(),
68 | TeamSecrets.path(),
69 | }
70 |
71 | HandlerClass = handler(routes, '/%s' % ctf, forbidden)
72 |
73 | server_address = ('localhost', port)
74 | httpd = HTTPServer(server_address, HandlerClass)
75 | sa = httpd.socket.getsockname()
76 | print("Serving HTTP on", sa[0], "port", sa[1], "...")
77 | httpd.serve_forever()
78 |
--------------------------------------------------------------------------------
/setup-vpn:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 | if [[ $# != 3 ]]; then
3 | echo "usage: $0 vpn_addr team_id password"
4 | exit 1
5 | fi
6 | vpn_addr="$1"
7 | team_id="$2"
8 | password="$3"
9 |
10 | echo "team-${team_id}" > auth.txt
11 | echo "$password" >> auth.txt
12 |
13 | vpn_port=$((10000 + $team_id))
14 |
15 | cat > ctf.ovpn <
39 | -----BEGIN CERTIFICATE-----
40 | MIIDtTCCAp2gAwIBAgIJAKz96Ok7WRJ4MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
41 | BAYTAkJSMRAwDgYDVQQKEwdQd24yV2luMRMwEQYDVQQDEwpQd24yV2luIENBMQ8w
42 | DQYDVQQpEwZzZXJ2ZXIwHhcNMTcxMDExMDIxMTAxWhcNMjcxMDA5MDIxMTAxWjBF
43 | MQswCQYDVQQGEwJCUjEQMA4GA1UEChMHUHduMldpbjETMBEGA1UEAxMKUHduMldp
44 | biBDQTEPMA0GA1UEKRMGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
45 | CgKCAQEAv0ONla2e6+JrhRElPobYgXyZs9ZlGplo6NYH4n2iOUPODkFfydRMkhqs
46 | T48q7s3sWpHOezr5Qj9SepGUvcYK/9tc7uAn2psUW8FOOK3qGjvw4o6G2x9sI/tS
47 | J6OWKbu84Xy05l6BrRxI+qWVLcYgjogIflXgkwLcFLUA19uGaQYzaPO4csGtGVPC
48 | oS0mrn/GgyH6RSXN502LUO4b+3LihI5fxf2nQjTb3pdImVMtznbP8XNaq/je5h5q
49 | hQT67DWjXVdZd41awMJlbvbmywdROLYUVMO73q78C1vg6lrr44tNi4D3cYXNwA18
50 | S+99+dDSCiTrtlr0dtGR8AHdOwM8GwIDAQABo4GnMIGkMB0GA1UdDgQWBBQMMm2V
51 | mbmXdQrfFdEZ+A3Vj8lUbjB1BgNVHSMEbjBsgBQMMm2VmbmXdQrfFdEZ+A3Vj8lU
52 | bqFJpEcwRTELMAkGA1UEBhMCQlIxEDAOBgNVBAoTB1B3bjJXaW4xEzARBgNVBAMT
53 | ClB3bjJXaW4gQ0ExDzANBgNVBCkTBnNlcnZlcoIJAKz96Ok7WRJ4MAwGA1UdEwQF
54 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBALJIROdxRx7M+R+OUUK0soIZlIiJEuXA
55 | nPNXvvC3hhYeo54GaiPBmfrDEtp+dgTpTzVuW+nur7M/oSnCAwBvasaUXQU+Am/A
56 | Z1r8zBSIsDDRM3OCfKbqUymjpzGNz7S6GawYIcroak5NW/C8VcuZzo7FTXPSI32u
57 | thfeDTzWTIcXOaKi1efsKgR49JVQ6YVhv5dzHxYtfZa3AGiRQRD4lKfbeQcd+Eh+
58 | mzr8C4EuOK+YQiXHSyO9DxilNaR3t5LeNyiRH/xC2gFcBJtR1Ep/ZYNdA9TT41Gd
59 | ERKi59X9sSQJ7h+ZM8F56E99/7oW02PUpbxgf4CciLFcQKXk07uZJX8=
60 | -----END CERTIFICATE-----
61 |
62 | EOF
63 |
64 | cat > verify-cn <
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |