Expand Up @@ -163,7 +163,7 @@ def validate_voters_lists_consistency(cfg_objects):
)
break
voters_reg = dict(
[voter[0], voter[3:5]] for voter in cfg['voters'])
[voter[1], voter[3:5]] for voter in cfg['voters'])
changeset = 1
continue

Expand All @@ -182,9 +182,9 @@ def validate_voters_lists_consistency(cfg_objects):
log.info("Checking voters list changeset #%d consistency", changeset)
voters_in_patch = set()
for rec_no, voter in enumerate(cfg['voters']):
voter_id = voter[0]
voter_id = voter[1]

if voter[2] == 'lisamine':
if voter[0] == 'lisamine':
if (voter_id not in voters_reg
and voter_id not in voters_in_patch):
voters_reg[voter_id] = voter[3:5]
Expand All @@ -199,12 +199,8 @@ def validate_voters_lists_consistency(cfg_objects):
f'Record #{rec_no}: Removing ID {voter_id} '
'that is added with this patch')
try:
district = voters_reg.pop(voter_id)
if district != voter[3:5]:
errors.append(
f'Record #{rec_no}: Removing voter ID {voter_id} '
f'from invalid district {voter[3:5]}. '
f'Voter is registered in district {district}')
# just to test that voter id is valid before removing it
voters_reg.pop(voter_id)
except KeyError:
errors.append(
f'Record #{rec_no}: Removing voter ID {voter_id} '
Expand Down Expand Up @@ -295,11 +291,13 @@ def validate_voters_consistency(

# check consistency
for voter in voters_cfg['voters']:

# if action is "kustutamine" then don't check districts/parish
if voter[0] == "kustutamine":
continue
# Voterlist contains voter EHAK and district no which must be
# translated into full district identifier
voter_district = None
voter_id = voter[0]
voter_id = voter[1]
voter_ehak = voter[3]
voter_district_no = voter[4]

Expand Down
Expand Up @@ -41,7 +41,7 @@ def main():
Key file must be in PEM format and must be not password protected.
mid-token-key - Mobile ID identity token for
mid-token-key - Mobile-ID/Smart-ID identity token for
choices, mobile-id and voting services.
Key file must be 32 bytes long.
Expand Down
Expand Up @@ -151,7 +151,7 @@ def main():
def write_voter_list_zip(content, version, output_filepath):
"""Write voter list ZIP file."""
# write list to temporary ZIP file
tmp_filepath = output_filepath + ".tmp"
tmp_filepath = f"{output_filepath}.tmp"
with open(tmp_filepath, "bw") as fd:
tmp_filepath = fd.name
log.info("Writing VIS response to temporary file %r", tmp_filepath)
Expand Down
25 changes: 12 additions & 13 deletions collector-admin/ivxv_admin/cli_utils/service_utils.py
Expand Up @@ -77,10 +77,9 @@ def export_votes(must_consolidate, output_filename):
try:
subprocess.run(['ivxv-backup', 'ballot-box'], check=True)
except OSError as err:
raise IvxvError(
'Creating ballot box backup failed: {}'.format(err.strerror))
raise IvxvError(f"Creating ballot box backup failed: {err.strerror}")
except subprocess.CalledProcessError as err:
raise IvxvError('Creating ballot box backup failed: {}'.format(err))
raise IvxvError(f"Creating ballot box backup failed: {err}")

# create handler for backup service
services = get_services(include_types=['backup'])
Expand Down Expand Up @@ -779,13 +778,14 @@ def voting_sessions_util():
Session data is in CSV format.
Usage: ivxv-voting-sessions (vote | verify) <output_file> [--anonymize]
Usage: ivxv-voting-sessions (vote | verify) <output_file> [--anonymize] [--uniq]
[--log-level=<level>]
Options:
<output_file> Write sessions to file.
--anonymize Anonymize session data
(IP addresses and ID codes).
--uniq Consolidate session data.
--log-level=<level> Logging level [Default: INFO].
"""
)
Expand Down Expand Up @@ -819,6 +819,7 @@ def voting_sessions_util():
logmon_account,
session_type="vote" if args["vote"] else "verify",
anonymize=args["--anonymize"],
uniq=args["--uniq"],
output_filepath=args["<output_file>"],
log_level=args["--log-level"],
)
Expand All @@ -830,21 +831,19 @@ def voting_sessions_util():


def import_voting_sessions(
logmon_account, session_type, anonymize, output_filepath, log_level
logmon_account, session_type, anonymize, uniq, output_filepath, log_level
):
"""Import voting sessions from Log Monitor."""
# generate CSV
log.info("Generating voting sessions file in Log Monitor")
remote_outfile = f"~logmon/voting-sessions-{session_type}-{os.getpid()}.json"
remote_cmd = [
"ivxv-export-voting-sessions",
session_type,
f"--log-level={log_level}",
]
cmd = ["ssh", logmon_account] + remote_cmd
remote_cmd = ["ivxv-export-voting-sessions", f"--log-level={log_level}"]
if anonymize:
cmd.append("--anonymize")
cmd.append(remote_outfile)
remote_cmd.append("--anonymize")
if uniq:
remote_cmd.append("--uniq")
remote_cmd += [session_type, remote_outfile]
cmd = ["ssh", logmon_account] + remote_cmd
try:
subprocess.run(cmd, stdout=subprocess.PIPE, check=True)
except subprocess.CalledProcessError as err:
Expand Down
2 changes: 1 addition & 1 deletion collector-admin/ivxv_admin/cli_utils/status_utils.py
Expand Up @@ -27,7 +27,7 @@ def users_list_util():
user_no = 0
for user_cn, permissions in sorted(users.items()):
user_no += 1
print('%d. %s Permissions: %s' % (user_no, user_cn, permissions))
print(f"{user_no}. {user_cn} Permissions: {permissions}")

if not user_no:
print('No users defined')
Expand Down
9 changes: 4 additions & 5 deletions collector-admin/ivxv_admin/command_file.py
Expand Up @@ -42,7 +42,7 @@ def load_collector_cmd_file(cmd_type, filename, plain=False):
cmd_filename = filename
filename = None
elif cmd_type in CFG_TYPES:
cmd_filename = re.compile(r'(.+\.)?{}.yaml$'.format(cmd_type))
cmd_filename = re.compile(rf"(.+\.)?{cmd_type}.yaml$")
elif cmd_type in VOTING_LIST_TYPES:
cmd_filename = None
else:
Expand Down Expand Up @@ -83,8 +83,7 @@ def load_collector_cmd_file(cmd_type, filename, plain=False):
elif cmd_type in ['choices', 'districts', 'voters']:
cfg_election_id = cfg.get('election')
else:
raise NotImplementedError(
'Unknown config type {}'.format(cmd_type))
raise NotImplementedError(f"Unknown config type {cmd_type}")
if election_id != cfg_election_id:
log.error(
"Election ID %r in config file does not match "
Expand Down Expand Up @@ -294,7 +293,7 @@ def check_cmd_signature(cmd_type, filename):
all_signatures = []
for line in proc.stdout.strip().split('\n'):
if not re.match(r'.+,.+,[0-9]{11} ', line):
raise LookupError('Invalid signature line: %s' % line)
raise LookupError(f"Invalid signature line: {line}")
signer, timestamp_str = line.split(' ')
timestamp = datetime.datetime.strptime(
timestamp_str, RFC3339_DATE_FORMAT_WO_FRACT).timestamp()
Expand Down Expand Up @@ -383,7 +382,7 @@ def log_cfg_validation_errors(items, prefix="/"):
"""Log validation errors."""
for field_name, val in items.items():
if isinstance(val, dict):
log_cfg_validation_errors(val, prefix + field_name + "/")
log_cfg_validation_errors(val, f"{prefix}{field_name}/")
else:
log.error("Validation error for field %r: %s", prefix + field_name, val)

Expand Down
26 changes: 26 additions & 0 deletions collector-admin/ivxv_admin/config_validator/election_conf.py
Expand Up @@ -15,6 +15,7 @@
ModelType,
StringType,
URLType,
DictType,
)

from .fields import CertificateType, ElectionIdType, PublicKeyType
Expand Down Expand Up @@ -77,6 +78,13 @@ class VisSchema(Model):

vis = ModelType(VisSchema, required=True)

class XroadSchema(Model):
"""Validating schema for VIS service config."""

ca = CertificateType(required=True)

xroad = ModelType(XroadSchema, required=True)

class AuthSchema(Model):
"""Validating schema for voter authentication config."""

Expand Down Expand Up @@ -138,6 +146,24 @@ def validate_phonerequired(self, data, value):

mid = ModelType(MIDSchema)

class SmartIDSchema(Model):
"""Validating schema for Smart ID config."""
url = URLType(required=True)
relyingpartyuuid = StringType(required=True)
relyingpartyname = StringType(required=True)
certificatelevel = StringType(
required=True, choices=["QUALIFIED", "ADVANCED", "QSCD"]
)
authinteractionsorder = ListType(DictType(StringType), required=True)
signinteractionsorder = ListType(DictType(StringType), required=True)
authchallengesize = IntType()
statustimeoutms = IntType()
roots = ListType(CertificateType, required=True)
intermediates = ListType(CertificateType)
ocsp = ModelType(OCSPSchema)

smartid = ModelType(SmartIDSchema)

qualification = ListType(
protocol_cfg({
"ocsp": OCSPSchema,
Expand Down
12 changes: 6 additions & 6 deletions collector-admin/ivxv_admin/config_validator/fields.py
Expand Up @@ -22,8 +22,8 @@ def validate(self, value, context=None):

if len(value) < min_length or len(value) > max_length:
raise ValidationError(
'Election ID length must be between {} and {}'.format(
min_length, max_length))
f"Election ID length must be between {min_length} and {max_length}"
)
if any(ws in value for ws in list(string.whitespace)):
raise ValidationError('Election ID contains whitespace')

Expand All @@ -40,8 +40,8 @@ def validate(self, value, context=None):
except OpenSSL.crypto.Error as err:
err_lib, err_func, err_reason = err.args[0][0]
raise ValidationError(
'Error in {} library {} function: {}'
.format(err_lib, err_func, err_reason))
f"Error in {err_lib} library {err_func} function: {err_reason}"
)

return super().validate(value, context)

Expand All @@ -56,7 +56,7 @@ def validate(self, value, context=None):
except OpenSSL.crypto.Error as err:
err_lib, err_func, err_reason = err.args[0][0]
raise ValidationError(
'Error in {} library {} function: {}'
.format(err_lib, err_func, err_reason))
f"Error in {err_lib} library {err_func} function: {err_reason}"
)

return super().validate(value, context)
3 changes: 2 additions & 1 deletion collector-admin/ivxv_admin/config_validator/schemas.py
Expand Up @@ -35,9 +35,10 @@ def protocol_cfg(mapping, **kwargs):
models = []
for protocol, model in mapping.items():
wrapper = type(
model.__name__ + "Wrapper", (Model, ), {
f"{model.__name__}Wrapper", (Model, ), {
"protocol": StringType(required=True, choices=[protocol]),
"conf": ModelType(model, required=True),
"ordertimeout": IntType(required=False, min_value=1)
})
mapping[protocol] = wrapper
models.append(wrapper)
Expand Down
3 changes: 3 additions & 0 deletions collector-admin/ivxv_admin/config_validator/tech_conf.py
Expand Up @@ -30,6 +30,8 @@ class ServiceSchema(Model):

proxy = ListType(ModelType(ServiceSchema))
mid = ListType(ModelType(ServiceSchema))
smartid = ListType(ModelType(ServiceSchema))
votesorder = ListType(ModelType(ServiceSchema))
voting = ListType(ModelType(ServiceSchema))
choices = ListType(ModelType(ServiceSchema))
verification = ListType(ModelType(ServiceSchema))
Expand All @@ -41,6 +43,7 @@ class ServiceSchema(Model):
class CollectorTechnicalConfigSchema(Model):
"""Validating schema for collector technical config."""
debug = BooleanType(default=False)
snidomain = StringType(required=True)

class FilterSchema(Model):
"""Validating schema for connection filter config."""
Expand Down
64 changes: 37 additions & 27 deletions collector-admin/ivxv_admin/config_validator/voters_list.py
Expand Up @@ -93,30 +93,40 @@ def parse_voters_list(list_content):
def validate_voter_record(fields, is_original_list):
"""Validate voter record in voters list."""
# field count
try:
voter_personalcode, voter_name, action, adminunit_code, no_district = fields
except ValueError:
raise ValueError(f"Invalid field count {len(fields)}, expected 5 fields")
# voter-personalcode = 11DIGIT
if not re.match(r"[0-9]{11}$", voter_personalcode):
raise ValueError(f"Invalid voter-personalcode {voter_personalcode!r}")
# voter-name = 1*100UTF-8-CHAR
if not voter_name:
raise ValueError("voter-name is empty")
if len(voter_name) > 100:
raise ValueError(f"voter-name lenght {len(voter_name)} exceeds 100 chars")
# action = "lisamine" | "kustutamine"
if action not in ["lisamine", "kustutamine"]:
raise ValueError(
f"Unknown action {action!r}. Must be 'lisamine' or 'kustutamine'"
)
if is_original_list and action != "lisamine":
raise ValueError(f"Action {action!r} is not allowed in initial list")
# adminunit-code = 1*4UTF-8-CHAR | "FOREIGN"
if not adminunit_code:
raise ValueError("Missing adminunit-code")
if len(adminunit_code) > 4 and adminunit_code != "FOREIGN":
raise ValueError(f"adminunit-code {adminunit_code!r} is longer than 4 chars")
# no-district = 1*10DIGIT
if not re.match(r"[0-9]{1,10}$", no_district):
raise ValueError(f"Invalid no-district {no_district!r}")
if len(fields) == 5:
action, voter_personalcode, voter_name, adminunit_code, no_district = fields
if not voter_name:
raise ValueError("voter-name is empty")
if action != "lisamine":
raise ValueError(
f"Unknown action {action!r}. Must be 'lisamine' or 'kustutamine'"
)
# voter-personalcode = 11DIGIT
if not re.match(r"[0-9]{11}$", voter_personalcode):
raise ValueError(f"Invalid voter-personalcode {voter_personalcode!r}")
if len(voter_name) > 100:
raise ValueError(f"voter-name lenght {len(voter_name)} exceeds 100 chars")
# adminunit-code = 1*4UTF-8-CHAR | "FOREIGN"
if not adminunit_code:
raise ValueError("Missing adminunit-code")
if len(adminunit_code) > 4 and adminunit_code != "FOREIGN":
raise ValueError(
f"adminunit-code {adminunit_code!r} is longer than 4 chars")
# no-district = 1*10DIGIT
if not re.match(r"[0-9]{1,10}$", no_district):
raise ValueError(f"Invalid no-district {no_district!r}")

elif len(fields) == 2:
action, voter_personalcode = fields
if action != "kustutamine":
raise ValueError(
f"Unknown action {action!r}. Must be 'lisamine' or 'kustutamine'"
)
# voter-personalcode = 11DIGIT
if not re.match(r"[0-9]{11}$", voter_personalcode):
raise ValueError(f"Invalid voter-personalcode {voter_personalcode!r}")
if is_original_list:
raise ValueError(f"Action {action!r} is not allowed in initial list")

else:
raise ValueError(f"Invalid field count {len(fields)}, expected 2 or 5 fields")
10 changes: 5 additions & 5 deletions collector-admin/ivxv_admin/db.py
Expand Up @@ -180,7 +180,7 @@ def set_value(self, key, value, safe=False):
# validate value
if isinstance(value, datetime.datetime):
value = value.strftime(RFC3339_DATE_FORMAT)
assert isinstance(value, str), 'Invalid value type: %s' % type(value)
assert isinstance(value, str), f"Invalid value type: {type(value)}"

# set value
if key in [
Expand Down Expand Up @@ -210,12 +210,12 @@ def set_value(self, key, value, safe=False):
assert value in ["PENDING", "APPLIED", "INVALID", "SKIPPED"]
elif re.match(r'host/.+/.+$', key):
key_type = key.split('/')[2]
assert key_type in DB_HOST_SUBKEYS, (
'Invalid host key type %s' % key_type)
assert key_type in DB_HOST_SUBKEYS, f"Invalid host key type {key_type}"
elif re.match(r'service/.+/.+$', key):
key_type = key.split('/')[2]
assert key_type in ALLOWED_SERVICE_KEYS, (
'Invalid service key type %s' % key_type)
assert (
key_type in ALLOWED_SERVICE_KEYS
), f"Invalid service key type {key_type}"
if key_type == 'state':
assert value in SERVICE_STATES, f"Invalid value for {key!r}: {value!r}"
elif key_type == 'backup-times':
Expand Down
3 changes: 3 additions & 0 deletions collector-admin/ivxv_admin/http_daemon.py
Expand Up @@ -166,6 +166,9 @@ def download_voting_sessions():
if request.forms.get("anonymize"):
cmd.append("--anonymize")
filename += "-anonymized"
if request.forms.get("uniq"):
cmd.append("--uniq")
filename += "-uniq"
filename += "-{:%Y.%m.%d_%H.%M}.csv".format(datetime.datetime.now()) # timestamp
filepath = f"/var/lib/ivxv/admin-ui-data/{filename}"
cmd.append(filepath)
Expand Down
18 changes: 9 additions & 9 deletions collector-admin/ivxv_admin/lib/__init__.py
Expand Up @@ -177,7 +177,7 @@ def gen_service_record_defaults(db, cfg):
for service_id, service_defaults in sorted(service_values.items()):
db_key_prefix = f'service/{service_id}'
try:
db.get_value(db_key_prefix + '/service-type')
db.get_value(f"{db_key_prefix}/service-type")
continue
except KeyError:
log.info('Registering new service %s in management service',
Expand All @@ -188,7 +188,7 @@ def gen_service_record_defaults(db, cfg):
params={'service_type': service_defaults['service-type']})

for key, val in service_defaults.items():
db.set_value(db_key_prefix + '/' + key, val)
db.set_value(f"{db_key_prefix}/{key}", val)

set_tech_cfg_service_cond_values(db, cfg)

Expand Down Expand Up @@ -225,8 +225,8 @@ def manage_db_cond_value(db, service_id, key, set_value, value=None):
pass


def manage_db_mid_fields(db):
"""Create/remove service Mobile ID token keys in database.
def manage_db_mobileid_fields(db):
"""Create/remove service Mobile-ID/Smart-ID token keys in database.
Check 'ticket' authentication method in election config and manage
"mid-token-key" keys in management database for mid, choices and voting
Expand All @@ -235,7 +235,7 @@ def manage_db_mid_fields(db):
"""
ticket_auth = 'ticket' in db.get_all_values('election').get('auth', {})
for service_id, service_data in db.get_all_values('service').items():
if service_data['service-type'] not in ['mid', 'choices', 'voting']:
if service_data['service-type'] not in ['mid', 'smartid', 'choices', 'voting']:
continue
key = f'service/{service_id}/mid-token-key'
if ticket_auth:
Expand Down Expand Up @@ -284,13 +284,13 @@ def gen_host_record_defaults(db, cfg):
for hostname in sorted(hostnames):
db_key_prefix = f'host/{hostname}'
try:
db.get_value(db_key_prefix + '/state')
db.get_value(f"{db_key_prefix}/state")
continue
except KeyError:
log.info(
'Registering new service host %s in management service',
hostname)
db.set_value(db_key_prefix + '/state', '')
db.set_value(f"{db_key_prefix}/state", "")


def gen_logmon_data(db, logging_params):
Expand Down Expand Up @@ -392,7 +392,7 @@ def populate_user_permissions(db):

for user_cn, permissions in db.get_all_values('user').items():
for permission_name in permissions.split(','):
permissions_to_create.append(user_cn + '-' + permission_name)
permissions_to_create.append(f"{user_cn}-{permission_name}")

# removing permission files
for permission_name in os.listdir(permissions_path):
Expand All @@ -407,7 +407,7 @@ def populate_user_permissions(db):
if not os.path.exists(filepath):
log.info("Creating Apache Web Server user permission file %r", filepath)
with open(filepath, 'x') as fp:
fp.write('Created %s' % datetime.datetime.now().strftime('%c'))
fp.write(f"Created {datetime.datetime.now().strftime('%c')}")


def get_current_voter_list_changeset_no(db):
Expand Down
2 changes: 1 addition & 1 deletion collector-admin/ivxv_admin/lib/lockfile.py
Expand Up @@ -42,7 +42,7 @@ def __init__(self, pidfile_name):
self.fp = open(pidfile_name, "ab")
fcntl.flock(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
atexit.register(self.rm_pid)
self.fp.write(bytes("{}\n".format(os.getpid()), "ASCII"))
self.fp.write(bytes(f"{os.getpid()}\n", "ASCII"))
self.fp.flush()

def rm_pid(self):
Expand Down
11 changes: 6 additions & 5 deletions collector-admin/ivxv_admin/service/__init__.py
Expand Up @@ -64,10 +64,11 @@ def get_service_cfg_state(db, cfg):
db_key_prefix = f'service/{service["id"]}'

service_tech_cfg_ver = db.get_value(
db_key_prefix + '/technical-conf-version')
service_election_cfg_ver = (
election_cfg_ver and
db.get_value(db_key_prefix + '/election-conf-version'))
f"{db_key_prefix}/technical-conf-version"
)
service_election_cfg_ver = election_cfg_ver and db.get_value(
f"{db_key_prefix}/election-conf-version"
)

service_list[service['id']] = {
'technical': service_tech_cfg_ver != tech_cfg_ver,
Expand Down Expand Up @@ -151,7 +152,7 @@ def generate_service_hints(services):
]
if service_type_params['mobile_id']:
hints.append(
['Install mobile ID identity token key',
['Install Mobile-ID/Smart-ID identity token key',
not params.get('mid-token-key', True)])
if service_type_params['tspreg']:
hints.append(
Expand Down
51 changes: 21 additions & 30 deletions collector-admin/ivxv_admin/service/logging.py
@@ -1,42 +1,33 @@
# IVXV Internet voting framework
"""Logging helper for microservice management."""

import datetime
import logging

from .. import RFC3339_DATE_FORMAT_WO_FRACT

# create logger
log = logging.getLogger('.'.join(__name__.split('.')[:-1]))


class ServiceLogger:
"""Logger wrapper for service object.
class ServiceLogger(logging.LoggerAdapter):
"""Logger adapter for service object.
Prepend service ID for logged messages.
Message level is also prepended for levels other than INFO.
Include service ID for logged messages.
Include message level for levels other than INFO.
"""
log_prefix = None #: Prefix for log messages (str)
storage = None #: Collection for logged records (list of strings)

def __init__(self, service_id):
"""Constructor."""
self.log_prefix = 'SERVICE %s: ' % service_id
self.storage = []

def __getattr__(self, name):
"""Get wrapper for logger method."""
log_method = getattr(log, name)

def log_method_wrapper(*args):
"""Wrapper for logger method to prepend log message prefix."""
args = list(args)
if name != 'info':
args[0] = '{}: {}'.format(name.upper(), args[0])
args[0] = self.log_prefix + args[0]
self.storage.append('{} {}'.format(
datetime.datetime.now().strftime(RFC3339_DATE_FORMAT_WO_FRACT),
args[0] % tuple(args[1:])))
return log_method(*tuple(args))

return log_method_wrapper
def process(self, msg, kwargs):
"""
Process the logging message and keyword arguments passed in to
a logging call to insert contextual information.
"""
return f"SERVICE {self.extra['service_id']}: {msg}", kwargs

def log(self, level, msg, *args, **kwargs):
"""
Delegate a log call to the underlying logger, after adding
contextual information from this adapter instance.
"""
if self.isEnabledFor(level):
if level != logging.INFO:
msg = f"{logging.getLevelName(level).upper()}: {msg}"
msg, kwargs = self.process(msg, kwargs)
self.logger.log(level, msg, *args, **kwargs)
30 changes: 21 additions & 9 deletions collector-admin/ivxv_admin/service/service.py
@@ -1,12 +1,14 @@
# IVXV Internet voting framework
"""Microservice management helper."""

import datetime
import json
import os
import re
import shutil
import subprocess
import tempfile
from logging.handlers import MemoryHandler

from jinja2 import Environment, PackageLoader

Expand All @@ -15,6 +17,7 @@
from .. import (
COLLECTOR_PKG_FILENAMES,
DEB_PKG_VERSION,
RFC3339_DATE_FORMAT_WO_FRACT,
SERVICE_SECRET_TYPES,
SERVICE_STATE_CONFIGURED,
SERVICE_STATE_INSTALLED,
Expand All @@ -26,7 +29,7 @@
from ..event_log import register_service_event
from . import IVXV_ADMIN_SSH_PUBKEY_FILE, RSYSLOG_CFG_FILENAME, generate_service_hints
from .backup_service import install_backup_crontab
from .logging import ServiceLogger
from .logging import ServiceLogger, log
from .remote_exec import exec_remote_cmd

#: Path to service directory.
Expand All @@ -38,6 +41,7 @@ class Service:
service_id = None #: Service ID (str)
data = None #: Service data (dict)
log = None #: Logger for service
memory_log_handler = MemoryHandler(1000) #: Memory handler for log buffering
#: State report file name for currently applied config (str)
cfg_state_filepath = None
cfg_state = None #: State report for currently applied config (dict)
Expand All @@ -55,7 +59,8 @@ def __init__(self, service_id, service_data):
'ip-address': service_data.get('address'),
}
self.data = service_data
self.log = ServiceLogger(service_id)
self.log = ServiceLogger(log, {"service_id": service_id})
log.addHandler(self.memory_log_handler)

def __repr__(self):
"""Printable representation of an object."""
Expand All @@ -78,7 +83,7 @@ def service_account_name(self):
return 'ivxv-admin'
if self.service_type == 'proxy':
return 'haproxy'
return 'ivxv-' + self.service_type
return f"ivxv-{self.service_type}"

@property
def service_systemctl_id(self):
Expand Down Expand Up @@ -536,8 +541,7 @@ def configure_logging(self, tech_cfg):
ext_log_collectors=ext_log_collectors)

# read existing config file
cmd = 'test -f {filename} && cat {filename}'.format(
filename=RSYSLOG_CFG_FILENAME)
cmd = f"test -f {RSYSLOG_CFG_FILENAME} && cat {RSYSLOG_CFG_FILENAME}"
proc = self.ssh(cmd, account='ivxv-admin', stdout=subprocess.PIPE)
existing_rsyslog_cfg = proc.stdout.decode()

Expand Down Expand Up @@ -595,9 +599,8 @@ def load_apply_state(self, filepath, attempt_no=0):
def update_apply_state(self, **kw):
"""Update config applying state file."""
self.cfg_state.update(kw)
self.cfg_state['log'][-1] += self.log.storage
self.log.storage = []
tmp_filepath = self.cfg_state_filepath + '.tmp'
self.cfg_state["log"][-1] = list(self.get_log_buffer())
tmp_filepath = f"{self.cfg_state_filepath}.tmp"
with open(tmp_filepath, 'x') as fp:
json.dump(self.cfg_state, fp, indent=4, sort_keys=True)
shutil.move(tmp_filepath, self.cfg_state_filepath)
Expand Down Expand Up @@ -879,7 +882,7 @@ def register_cfg_version(self, cfg_type, cfg_ver, service_state):
cfg_ver,
)
db.set_value(
self.get_db_key('%s-conf-version' % cfg_type), cfg_ver)
self.get_db_key(f"{cfg_type}-conf-version"), cfg_ver)

if service_state is not None:
self.register_state(db, service_state)
Expand Down Expand Up @@ -1198,3 +1201,12 @@ def ssh(self, cmd, account=None, fwd_auth_agent=False, **kw):
account or self.service_account_name, self.hostname))

return exec_remote_cmd(ssh_cmd + cmd, **kw)

def get_log_buffer(self):
"""Get formatted log messages from log buffer."""
for rec in self.memory_log_handler.buffer:
timestamp = datetime.datetime.fromtimestamp(rec.created).strftime(
RFC3339_DATE_FORMAT_WO_FRACT
)
msg = rec.getMessage()
yield f"{timestamp} {msg}"
2 changes: 1 addition & 1 deletion collector-admin/ivxv_admin/templates/ivxv_status.jinja
Expand Up @@ -88,7 +88,7 @@ Services: {{ service|count }} services in {{ network|count }} network(s)
Service TLS certificate: {{ service['tls-cert'] or '-' }}
{%- endif %}
{%- if 'mid-token-key' in service %}
Mobile ID identity token: {{ service['mid-token-key'] or '-' }}
Mobile-ID/Smart-ID identity token key: {{ service['mid-token-key'] or '-' }}
{%- endif %}
{%- if 'tspreg-key' in service %}
PKIX TSP registration key: {{ service['tspreg-key'] or '-' }}
Expand Down
2 changes: 1 addition & 1 deletion collector-admin/ivxv_admin/wsgi.py
Expand Up @@ -149,7 +149,7 @@ def fwd_request():

data = urllib.parse.urlencode(request.forms).encode("UTF-8")
fwd_request = urllib.request.Request(daemon_url, data=data, method="GET")
max_response_size = 1024 * 1024
max_response_size = 1024 * 1024 * 1024 # 1GB
with urllib.request.urlopen(fwd_request) as req_fp:
daemon_response = req_fp.read(max_response_size)
if len(daemon_response) == max_response_size:
Expand Down
2 changes: 1 addition & 1 deletion collector-admin/site/ivxv/about.html
Expand Up @@ -137,7 +137,7 @@ <h1 class="page-header">IVXV Kogumisteenuse haldusteenus</h1>
<div>Kogumisteenuse haldusteenus.</div>
<div>
Versioon:
<span class="fa-2x" id="version">1.7.7</span>
<span class="fa-2x" id="version">1.8.2</span>
<div>
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion collector-admin/site/ivxv/downloads.html
Expand Up @@ -180,7 +180,12 @@ <h1 class="page-header">Väljavõtete allalaadimised</h1>
<form action="/ivxv/cgi/download-voting-sessions" method="post">
<div class="checkbox">
<label>
<input name="anonymize" type="checkbox"> Anonüümistadud seansid
<input name="uniq" type="checkbox"> Unikaalsed seansid
</label>
</div>
<div class="checkbox">
<label>
<input name="anonymize" type="checkbox"> Anonüümistatud seansid
</label>
</div>
<div class="checkbox">
Expand Down
42 changes: 31 additions & 11 deletions collector-admin/site/js/config.js
Expand Up @@ -13,6 +13,12 @@ function loadPageData() {

// load collector state
$.getJSON('data/status.json', function(state) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json
* HTTP GET response that contains HTML tags is not allowed!
* state is always an JSON object
*/
state = sanitizeJSON(state);
display_cfg_panel(
'trust', state, state['config']['trust'], 'Usaldusjuure seadistus');
display_cfg_panel(
Expand Down Expand Up @@ -66,8 +72,16 @@ var state_filenames = {};

/**
* Display config state panel
*
* @param {string} id_prefix
* @param {Object} state
* @param {Object} cfg
* @param {string} title
*/
function display_cfg_panel(id_prefix, state, cfg, title) {
id_prefix = sanitizePrimitive(id_prefix);
state = sanitizeJSON(state);
cfg = sanitizeJSON(cfg);
// Create panel if required
var panel = $('#' + id_prefix + '-cfg-state-panel');
if (!panel.length) {
Expand All @@ -76,7 +90,7 @@ function display_cfg_panel(id_prefix, state, cfg, title) {
' <div class="col-lg-12">' +
' <div id="' + id_prefix + '-cfg-state-panel" class="panel">' +
' <div class="panel-heading">' +
' <h4 class="panel-title">' + title + '</h4>' +
' <h4 class="panel-title">' + sanitizePrimitive(title) + '</h4>' +
' </div>' +
' <div class="panel-body">' +
' <div />' + // Placeholder for config info text
Expand Down Expand Up @@ -129,15 +143,15 @@ function display_cfg_panel(id_prefix, state, cfg, title) {
panel_body
.find('div:first')
.html(
'<div>Seisund: ' + stateStr + '</div>' +
'<div>Seisund: ' + sanitizePrimitive(stateStr) + '</div>' +
'<div>Rakendatav versioon: <span id="cfg-ver-' + id_prefix + '">[ määramata ]</a></div>' +
'<div>Rakendamise katseid: 0</div>'
);
} else {
panel_body
.find('div:first')
.html(
'<div>Seisund: ' + stateStr + '</div>' +
'<div>Seisund: ' + sanitizePrimitive(stateStr) + '</div>' +
'<div>Rakendatav versioon: <span id="cfg-ver-' + id_prefix + '">' + cfg['version'] + '</a></div>' +
'<div>Rakendamise katseid: ' + cfg['attempts'] + '</div>'
);
Expand Down Expand Up @@ -176,11 +190,17 @@ function toggle_apply_log(toggle_button) {
*/
function refresh_log(logbox, filename) {
var url = '/ivxv/data/commands/' + filename;
$.getJSON(url, function(state) {
$.getJSON(encodeURI(url), function(state) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/commands/??
* HTTP GET response that contains HTML tags is not allowed!
* state is always an JSON object
*/
state = sanitizeJSON(state);
logbox.text(state['log'][state['attempts'] - 1].join('\n'));
})
.fail(function(response) {
logbox.text(response.responseText);
logbox.text(sanitizePrimitive(response.responseText));
});
}

Expand Down Expand Up @@ -218,12 +238,12 @@ function uploadFiles(event) {
// Create a formdata object and add the files
var data = new FormData();
data.append('upload', files[0]);
data.append('type', $('#drop').find(':selected').val());
data.append('type', sanitizePrimitive($('#drop').find(':selected').val()));

var form = $('#config-upload-form');
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
url: encodeURI(form.attr('action')),
type: sanitizePrimitive(form.attr('method')),
data: data,
cache: false,
dataType: 'json',
Expand All @@ -235,9 +255,9 @@ function uploadFiles(event) {
console.log(jqXHR.responseJSON.message);
$('#upload-message')
.html(
jqXHR.responseJSON.message +
sanitizePrimitive(jqXHR.responseJSON.message) +
'<hr />' +
'<pre>' + jqXHR.responseJSON.log.join('\n') + '</pre>'
'<pre>' + sanitizePrimitive(jqXHR.responseJSON.log.join('\n')) + '</pre>'
)
.addClass(jqXHR.responseJSON.success ? 'alert-success' : 'alert-danger')
.show();
Expand All @@ -248,7 +268,7 @@ function uploadFiles(event) {
error: function(jqXHR, textStatus, errorThrown) {
console.log(jqXHR);
$('#upload-message')
.html(jqXHR.responseText)
.html(sanitizePrimitive(jqXHR.responseText))
.addClass('alert-danger')
.show();
}
Expand Down
12 changes: 12 additions & 0 deletions collector-admin/site/js/downloads.js
Expand Up @@ -13,6 +13,12 @@ function loadPageData() {

// load collector state
$.getJSON('data/status.json', function(state) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json
* HTTP GET response that contains HTML tags is not allowed!
* state is always an JSON object
*/
state = sanitizeJSON(state);
hideErrorMessage();

if ((state.collector_state == 'NOT INSTALLED') ||
Expand Down Expand Up @@ -43,6 +49,12 @@ function loadPageData() {

// load ballot box state
$.getJSON('cgi/ballot-box-state', function(state) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/cgi/ballot-box-state
* HTTP GET response that contains HTML tags is not allowed!
* state is always an JSON object
*/
state = sanitizeJSON(state);
$('#panel-download-ballot-box').remove();

if (state.data.length === 0) {
Expand Down
8 changes: 7 additions & 1 deletion collector-admin/site/js/index.js
Expand Up @@ -31,6 +31,12 @@ function loadPageData() {

// load collector state
$.getJSON('data/status.json', function(state) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json
* HTTP GET response that contains HTML tags is not allowed!
* state is always an JSON object
*/
state = sanitizeJSON(state);
hideErrorMessage();

// election ID
Expand Down Expand Up @@ -152,7 +158,7 @@ function loadPageData() {
var listStatus = voterListStateDescriptions.get(state['list'][iStr + '-state']);
$('#list-list').append(
'<li class="list-group-item list-group-item-success" style="padding-left:25px">' +
(changeset_no + 1) + '. ' + listStatus + ': ' + state['list'][iStr] +
(changeset_no + 1) + '. ' + sanitizePrimitive(listStatus) + ': ' + state['list'][iStr] +
'</li>'
);
}
Expand Down
63 changes: 58 additions & 5 deletions collector-admin/site/js/ivxv.js
Expand Up @@ -15,6 +15,12 @@ function getContextData() {
// query page context data
console.debug('Loading context data');
$.getJSON('cgi/context.json', function(context) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/cgi/context.json
* HTTP GET response that contains HTML tags is not allowed!
* context is always an JSON object
*/
context = sanitizeJSON(context);
pageContext = context.data;
userContext = pageContext['current-user'];
console.debug('Current user: ' + userContext['cn'] +
Expand Down Expand Up @@ -78,7 +84,7 @@ function showErrorMessage(msg, retain_content) {
if (!retain_content) {
$('#page-wrapper').find('.row').hide('slow');
}
$('#common-error-msg').find('p').html(msg);
$('#common-error-msg').find('p').text(msg);
$('#common-error-msg').show('slow');
$('#page-wrapper').css({
'background-color': 'rgba(217, 83, 79, 0.2)'
Expand Down Expand Up @@ -115,7 +121,7 @@ function copyObjectToHtml(object_val, targetPrefix) {
if ('undefined' === typeof(targetPrefix))
targetPrefix = '';
$.each(object_val, function(key, val) {
$('#' + targetPrefix + key).html(val);
$('#' + sanitizePrimitive(targetPrefix) + sanitizePrimitive(key)).text(val);
});
}

Expand All @@ -142,11 +148,14 @@ function formatTime(dateTime, offset) {
*
* Add link to download config file command package.
*
* @param {str} selector - DOM selector
* @param {str} cfg_type - config type
* @param {obj} cfg - config data from status.json
* @param {string} selector - DOM selector
* @param {string} cfg_type - config type
* @param {Object} cfg - config data from status.json
*/
function outputCmdVersion(selector, cfg_type, cfg) {
selector = sanitizePrimitive(selector);
cfg = sanitizeJSON(cfg);

if ((cfg_type === 'trust') ||
(cfg_type === 'technical') ||
(cfg_type === 'election')) {
Expand Down Expand Up @@ -192,3 +201,47 @@ function fillVoterListStateCounters(list_state) {
$('#list-voters-skipped').text(list_state['voters-list-skipped']);
$('#list-voters-available').text(list_state['voters-list-available']);
}

/**
* If Object (JSON!) contains XSS vulnerable content like HTML attributes
* or HTML context, it will be replaced and XSS-free Object (JSON!) is returned.
*
* If somehow Object type data parsing fails - empty {} is returned.
* Note, that even if Object data contains XSS vulnerabilities it
* doesn't automatically mean, that it isn't a valid JavaScript Object,
* however if data isn't valid Object, then it doesn't make sense to
* proceed with XSS validation at all.
*
* @param {Object} context - JSON object
* @return {Object} - XSS-free JSON object
*/
function sanitizeJSON(context) {
try {
return JSON.parse(JSON.stringify(context).replaceAll('<', '&lt;'));
} catch (err) {
console.error(err);
return {};
}
}

/**
* If primitive type contains XSS vulnerable content like HTML attributes
* or HTML context, it will be replaced and returned as XSS-free string.
*
* If somehow primitive type data parsing fails - empty string is returned.
* Note, that even if primitive data contains XSS vulnerabilities it
* doesn't automatically mean, that it isn't a valid JavaScript primitive,
* however if data isn't valid primitive, then it doesn't make sense to
* proceed with XSS validation at all.
*
* @param {string | number | boolean} context - primitive data
* @return {string} - XSS-free string
*/
function sanitizePrimitive(context) {
try {
return String(context).replaceAll('<', '&lt;');
} catch (err) {
console.error(err);
return '';
}
}
20 changes: 13 additions & 7 deletions collector-admin/site/js/lists.js
Expand Up @@ -13,6 +13,12 @@ function loadPageData() {

// load collector state
$.getJSON('data/status.json', function(state) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json
* HTTP GET response that contains HTML tags is not allowed!
* state is always an JSON object
*/
state = sanitizeJSON(state);
hideErrorMessage();

// choices list
Expand Down Expand Up @@ -53,7 +59,7 @@ function loadPageData() {
var listStatus = voterListStateDescriptions.get(state['list'][iStr + '-state']);
$('#list-list').append(
'<li class="list-group-item" style="padding-left:25px">' +
(changeset_no + 1) + '. ' + listStatus + ': ' + state['list'][iStr] +
(changeset_no + 1) + '. ' + sanitizePrimitive(listStatus) + ': ' + state['list'][iStr] +
'</li>'
);
}
Expand Down Expand Up @@ -120,12 +126,12 @@ function uploadFiles(event) {
// Create a formdata object and add the files
var data = new FormData();
data.append('upload', files[0]);
data.append('type', $('#drop').find(':selected').val());
data.append('type', sanitizePrimitive($('#drop').find(':selected').val()));

var form = $('#config-upload-form');
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
url: encodeURI(form.attr('action')),
type: sanitizePrimitive(form.attr('method')),
data: data,
cache: false,
dataType: 'json',
Expand All @@ -136,9 +142,9 @@ function uploadFiles(event) {
console.log(jqXHR.responseJSON.message);
$('#upload-message')
.html(
jqXHR.responseJSON.message +
sanitizePrimitive(jqXHR.responseJSON.message) +
'<hr />' +
'<pre>' + jqXHR.responseJSON.log.join('\n') + '</pre>'
'<pre>' + sanitizePrimitive(jqXHR.responseJSON.log.join('\n')) + '</pre>'
)
.addClass(jqXHR.responseJSON.success ? 'alert-success' : 'alert-danger')
.show();
Expand All @@ -148,7 +154,7 @@ function uploadFiles(event) {
error: function(jqXHR, textStatus, errorThrown) {
console.log(jqXHR);
$('#upload-message')
.html(jqXHR.responseText)
.html(sanitizePrimitive(jqXHR.responseText))
.addClass('alert-danger')
.show();
}
Expand Down
8 changes: 8 additions & 0 deletions collector-admin/site/js/services.js
Expand Up @@ -23,15 +23,23 @@ function loadPageData() {
'backup': 'Varundusteenus',
'choices': 'Nimekirjateenus',
'mid': 'Mobiil-ID abiteenus',
'smartid': 'Smart-ID abiteenus',
'proxy': 'Vahendusteenus',
'storage': 'Talletusteenus',
'log': 'Logikogumisteenus',
'votesorder': 'Järjekorrateenus',
'voting': 'Hääletamisteenus',
'verification': 'Kontrollteenus'
};

// load collector state
$.getJSON('data/status.json', function(state) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json
* HTTP GET response that contains HTML tags is not allowed!
* state is always an JSON object
*/
state = sanitizeJSON(state);
hideErrorMessage();

var i = 1;
Expand Down
30 changes: 21 additions & 9 deletions collector-admin/site/js/stats.js
Expand Up @@ -14,6 +14,12 @@ function loadPageData() {
// manage districts selection
if ($('#districts option').length == 1) {
$.getJSON('data/districts.json', function(data) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/districts.json
* HTTP GET response that contains HTML tags is not allowed!
* data is always an JSON object
*/
data = sanitizeJSON(data);
var dropdown = $('#districts');
$.each(data, function() {
dropdown.append($('<option />').val(this[0]).text(this[1]));
Expand All @@ -26,6 +32,12 @@ function loadPageData() {

// fill page with stats
$.getJSON('data/stats.json', function(data) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/stats.json
* HTTP GET response that contains HTML tags is not allowed!
* data is always an JSON object
*/
data = sanitizeJSON(data)
$('#stats-error').hide();
$('#stats-error-msg').html();
$('#auth-os').empty();
Expand Down Expand Up @@ -59,43 +71,43 @@ function loadPageData() {
if (stats_key === 'authentication-methods') {
var method = 'ID-kaart';
if (stats_table_val[0] === 'ticket') {
method = 'Mobiil-ID';
method = 'Mobiil-ID/Smart-ID';
}
$('#auth-os').append(
'<tr><td>' +
method +
'</td><td>' +
stats_table_val[1] +
sanitizePrimitive(stats_table_val[1]) +
'</td></tr>'
)
} else if (stats_key === 'operating-systems') {
$('#auth-os').append(
'<tr><td>' +
stats_table_val[0] +
sanitizePrimitive(stats_table_val[0]) +
'</td><td>' +
stats_table_val[1] +
sanitizePrimitive(stats_table_val[1]) +
'</td></tr>'
)
} else if (stats_key === 'top-10-revoters') {
$('#table-revoters').append(
'<tr><td>' +
stats_table_val[0] +
sanitizePrimitive(stats_table_val[0]) +
'</td><td>' +
stats_table_val[1] +
sanitizePrimitive(stats_table_val[1]) +
'</td></tr>'
)
} else if (stats_key === 'votes-by-country') {
$('#table-countries').append(
'<tr><td>' +
stats_table_val[0] +
sanitizePrimitive(stats_table_val[0]) +
'</td><td>' +
stats_table_val[1] +
sanitizePrimitive(stats_table_val[1]) +
'</td></tr>'
)
}
});
} else {
$('#' + stats_key).html(stats_val);
$('#' + sanitizePrimitive(stats_key)).text(stats_val);
}
});
} else if (key === 'error') {
Expand Down
16 changes: 11 additions & 5 deletions collector-admin/site/js/users.js
Expand Up @@ -13,6 +13,12 @@ function loadPageData() {

// load collector state
$.getJSON('data/status.json', function(state) {
/*
* HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json
* HTTP GET response that contains HTML tags is not allowed!
* state is always an JSON object
*/
state = sanitizeJSON(state);
hideErrorMessage();

var i = 1;
Expand Down Expand Up @@ -87,8 +93,8 @@ function uploadFiles(event) {

var form = $('#config-upload-form');
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
url: encodeURI(form.attr('action')),
type: sanitizePrimitive(form.attr('method')),
data: data,
cache: false,
dataType: 'json',
Expand All @@ -100,9 +106,9 @@ function uploadFiles(event) {
console.log(jqXHR.responseJSON.message);
$('#upload-message')
.html(
jqXHR.responseJSON.message +
sanitizePrimitive(jqXHR.responseJSON.message) +
'<hr />' +
'<pre>' + jqXHR.responseJSON.log.join('\n') + '</pre>'
'<pre>' + sanitizePrimitive(jqXHR.responseJSON.log.join('\n')) + '</pre>'
)
.addClass(jqXHR.responseJSON.success ? 'alert-success' : 'alert-danger')
.show();
Expand All @@ -114,7 +120,7 @@ function uploadFiles(event) {
error: function(jqXHR, textStatus, errorThrown) {
console.log(jqXHR);
$('#upload-message')
.html(jqXHR.responseText)
.html(sanitizePrimitive(jqXHR.responseText))
.addClass('alert-danger')
.show();
}
Expand Down
47 changes: 47 additions & 0 deletions common/collector/README.md
@@ -0,0 +1,47 @@
### collector/cmd/verifier

Can verify documents that are signed with:

- id-card
- mobile-id
- smart-id

#### If and only if you have the following trust.yaml configuration:
```
# Usaldusjuure seadistus YAML-struktuurina
container:
bdoc:
bdocsize: 104857600 # 100 MiB
filesize: 104857600 # 100 MiB
roots:
- !container devel_root.crt
- !container sk_test_root.crt
- !container EE-GovCA2018.crt
- !container EE_Certification_Centre_Root_CA.crt
intermediates:
- !container devel_intermediate.crt
- !container sk_test_intermediate.crt
- !container ESTEID-SK_2015.crt
- !container esteid2018.crt
- !container EID-SK_2016.pem.crt
profile: TS
ocsp:
responders:
- !container sk_test_ocsp.crt
- !container SK_OCSP_RESPONDER_2011.crt
- !container EID-SK_2016_OCSP_RESPONDER_2018.pem.cer
tsp:
signers:
- !container sk_test_tsa.crt
- !container SK_TIMESTAMPING_AUTHORITY_2019.crt
- !container SK_TIMESTAMPING_AUTHORITY_2020.crt
- !container SK_TIMESTAMPING_AUTHORITY_2021.crt
- !container SK_TIMESTAMPING_AUTHORITY_2022.crt
delaytime: 10
authorizations:
- PEREKONNANIMI,NIMI,ISIKUKOOD
```

#### P.S Don't forget to include all these listed certificates into container
21 changes: 21 additions & 0 deletions common/collector/src/ivxv.ee/auth/auth.go
Expand Up @@ -65,6 +65,14 @@ type VoteIdentifier interface {
VoteIdentifier(token []byte) (voteID []byte, err error)
}

// TokenData is an optional additional interface that Verifiers can
// implement. If a Verifier is also a VoteIdentifier, then it should be used to
// retrieve the vote identifier for storing the vote submitted using the
// authentication token.
type TokenData interface {
TokenData(token []byte) (data []byte, err error)
}

// NewFunc is the type of functions that an authentication verifier with a
// specified configuration.
type NewFunc func(yaml.Node) (Verifier, error)
Expand Down Expand Up @@ -140,3 +148,16 @@ func (a Auther) Verify(ctx context.Context, t Type, token []byte) (
}
return
}

func (a Auther) Data(t Type, token []byte) (data []byte, err error) {
v, ok := a[t]
if !ok {
return nil, UnconfiguredDataTypeError{Type: t}
}
if vid, ok := v.(TokenData); ok {
if data, err = vid.TokenData(token); err != nil {
return nil, err
}
}
return
}
24 changes: 22 additions & 2 deletions common/collector/src/ivxv.ee/auth/ticket/ticket.go
Expand Up @@ -106,10 +106,22 @@ func (t *T) VoteIdentifier(token []byte) (voteID []byte, err error) {
return ticket.VoteID, nil
}

// CreateData issues a new ticket for the data.
func (t *T) CreateData(plain []byte) (ticket []byte, err error) {
return t.cookie.Create(plain), nil
}

// TokenData implements the ivxv.ee/auth.TokenData interface. The
// token must be a ticket issued with the same cookie key as it was configured
// with.
func (t *T) TokenData(token []byte) (data []byte, err error) {
return t.openplain(token)
}

func (t *T) open(token []byte) (ticket tt, err error) {
plain, err := t.cookie.Open(token)
plain, err := t.openplain(token)
if err != nil {
return ticket, OpenTicketError{Err: err}
return ticket, err
}
rest, err := asn1.Unmarshal(plain, &ticket)
if err != nil {
Expand All @@ -120,3 +132,11 @@ func (t *T) open(token []byte) (ticket tt, err error) {
}
return
}

func (t *T) openplain(token []byte) (plain []byte, err error) {
plain, err = t.cookie.Open(token)
if err != nil {
return nil, OpenTicketError{Err: err}
}
return
}
10 changes: 9 additions & 1 deletion common/collector/src/ivxv.ee/cmd/verifier/main.go
Expand Up @@ -5,6 +5,8 @@ import (
"flag"
"fmt"
"os"
"regexp"
"strings"
"time"

"ivxv.ee/command"
Expand Down Expand Up @@ -74,7 +76,13 @@ options:`)
defer c.Close()

for _, s := range c.Signatures() {
fmt.Println(s.Signer.Subject.CommonName, s.SigningTime.Format(time.RFC3339))
pattern := regexp.MustCompile("[0-9]+")
if pattern.FindString(s.Signer.Subject.CommonName) == "" {
personalCode := strings.TrimPrefix(s.Signer.Subject.SerialNumber, "PNOEE-")
fmt.Println(s.Signer.Subject.CommonName+","+personalCode, s.SigningTime.Format(time.RFC3339))
} else {
fmt.Println(s.Signer.Subject.CommonName, s.SigningTime.Format(time.RFC3339))
}
}

return exit.OK, nil
Expand Down
10 changes: 10 additions & 0 deletions common/collector/src/ivxv.ee/conf/conf.go
Expand Up @@ -26,6 +26,7 @@ import (
"ivxv.ee/mid"
"ivxv.ee/q11n"
"ivxv.ee/server"
"ivxv.ee/smartid"
"ivxv.ee/storage"
"ivxv.ee/yaml"
)
Expand Down Expand Up @@ -79,13 +80,18 @@ type Election struct {
Key string // PEM-encoding of the public key used to verify voter list signatures.
}

XRoad struct {
CA string // PEM-encoded authentication certificate.
}

// Composited configuration structures defined in other packages.
Auth auth.Conf
Identity identity.Type
Age age.Conf
Vote container.Conf
DDS dds.Conf
MID mid.Conf
SmartID smartid.Conf
Qualification q11n.Conf
}

Expand Down Expand Up @@ -122,6 +128,8 @@ func (e Election) VoterForeignEHAKDefault() string {
type Technical struct {
Debug bool // Should debug logging be enabled?

SniDomain string

Network []struct {
ID string // Network segment identifier.
Services Services // Configured services in this segment.
Expand All @@ -137,10 +145,12 @@ type Services struct {
Proxy []*Service
DDS []*Service
MID []*Service
SmartID []*Service
Choices []*Service
Voting []*Service
Verification []*Service
Storage []*Service
VotesOrder []*Service
}

// Services finds the configured services for the requested network segment.
Expand Down
140 changes: 140 additions & 0 deletions common/collector/src/ivxv.ee/conf/testdata/election.dummy
Expand Up @@ -44,6 +44,27 @@ data:
1QIDAQAB
-----END PUBLIC KEY-----
xroad:
ca: |
-----BEGIN CERTIFICATE-----
MIIC8DCCAdigAwIBAgIUGkfCoWPHJ0tZDNzd98p0bV9coVgwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDQwNDA3NDIzOFoXDTIyMDUw
NDA3NDIzOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAwQamYbWYsJp7pxxG6nEvFbqe/tItwSTGQnvepSRD+3nv
MOJlkBjAQ+S7Yec6ay2/ZytMXnxwXMK9/l2tjAtRE4pkDoikTEwn6XPztha+lFnX
ewAC9TTbQ4O0UCdKUqp0lAPs49jdCI8V03iLWFF+7iJTuc9rERS1iuA3RQOhZ/I/
IQdruXZ9FBdUR8I0QZg7jaPjkpCiM38lcd39zRwXXEFdqOVgsUBvN2KYSyXR1u/c
3UWdzAquYKt583mUuortXfmdlEB+HSRRrw0wXlp708NS8qlfdm8nDbT7B8KXWdTV
EOAbRmY1ZRFiuGOpp3Ry7RKq6YibAwXy79p0w4EjfQIDAQABozowODAUBgNVHREE
DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB
MA0GCSqGSIb3DQEBCwUAA4IBAQB0Dmvb4OskZz+9BzbmAJhtz/yxatxFWdRHipqq
pUQexAgPSu1iUQMECAnya5cquQSXjHAXDqdsD4Yg+1r4zCcWwpec5UPjOg37GZid
8K5BYTawltywvWRLJ1sDWFVZENiDgP2Lxmq/PkG4rg2tG5C9IdfIlnAuop1XLcNc
1sfMyhMG1WAktvTil+acJmGtnlBAO5kovM31sAXr9isOrOGLsJo8mxiGZVWecqKQ
NoHGPflxCwXVXI5W2Cj9oXjZIzsJtu4zb4hM40prin9gBqQGku7wYDrRH0e0+C8z
NeDVbEWIbU7ZDBTIKAIkcSGZuB6lz1lxNRbD0Y9kaIXUpnxd
-----END CERTIFICATE-----
auth:
dummy:
authenticated:
Expand Down Expand Up @@ -217,6 +238,125 @@ data:
dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==
-----END CERTIFICATE-----
smartid:
url: https://sid.demo.sk.ee/smart-id-rp/v2/
relyingpartyuuid: 00000000-0000-0000-0000-000000000000
relyingpartyname: DEMO
certificatelevel: QUALIFIED
authinteractionsorder:
- type: verificationCodeChoice
displayText60: authenticating
- type: displayTextAndPIN
displayText60: authenticating
signinteractionsorder:
- type: verificationCodeChoice
displayText60: signing
- type: displayTextAndPIN
displayText60: signing
authchallengesize: 64
statustimeoutms: 5000
roots:
- |
-----BEGIN CERTIFICATE-----
MIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9
MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1
czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290
IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa
GA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl
cnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm
aWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq
xRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM
ywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7
MmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT
VNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D
kV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu
pwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
A1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD
AgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF
BwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0
I+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy
Bmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR
SnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S
tH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ
SfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI
-----END CERTIFICATE-----
intermediates:
- |
-----BEGIN CERTIFICATE-----
MIIG+DCCBeCgAwIBAgIQUkCP5k8r59RXxWzfbx+GsjANBgkqhkiG9w0BAQwFADB9
MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1
czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290
IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTYwODMwMTEyNDE1WhgP
MjAzMDEyMTcyMzU5NTlaMGgxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0
aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEcMBoG
A1UEAwwTVEVTVCBvZiBFSUQtU0sgMjAxNjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
ADCCAgoCggIBAOrKOByrJqS1QsKD4tXhqkZafPMd5sfxem6iVbMAAHKpvOs4Ia2o
XdSvJ2FjrMl5szeT4lpHyzfECzO3nx7pvRLKHufi6lMwMGjtSI6DK8BiH9z7Lm+k
NLunNFdIir0hPijjbIkjg9iwfaeST9Fi5502LsK7duhKuCnH7O0uMrS/MynJ4StA
NGY13X2FvPW4qkrtbwsmhdN0Btro72O6/3O+0vbnq/yCWtcQrBGv3+8XEBdCqH5S
/Rt0EugKX4UlVy5l0QUc8IrjGtdMsr9KDtvmVwlefXYKoLqkC7guMGOUNf6Y4AYG
sPqfY4dG3N5YNp5FHDL7IO93h7TpRV3gyR38LiJsPHk5nES5mdPkNuEkCyg0zEKI
7uJ4LUuBbjzZPp2gP7PN8Iqi9GP7V2NCz8vUVN3WpHvctsf0DMvZdV5pxqLY5ojy
fhMsU4aMcGSQA9EK8ES3O1zBK1DW+btjbQjUFW1SIwCkB2yofFxge+vvzZGbvt2U
GOE8oAL8/JzNxi9FbjTAbycrGWgEMQ0sM1fKc+OsvoaSy9m3ZQGph0+dbsouQpl3
kpJvjDMzxxkrMqxdhlVMreLKGCMMxJMAGQEwVS5P93Nnmz8UbkmeomUJr3NrBo4+
V9L5S4Kx1vTvD0p72xRYFyfifLOjs8qs7lR3yhkcBPQI78ERqxv31FWDAgMBAAGj
ggKFMIICgTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNVHQ4E
FgQUrrDq4Tb4JqulzAtmVf46HQK/ErQwDgYDVR0PAQH/BAQDAgEGMIHEBgNVHSAE
gbwwgbkwPAYHBACL7EABAjAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5l
ZS9yZXBvc2l0b29yaXVtL0NQUzA8BgcEAIvsQAEAMDEwLwYIKwYBBQUHAgEWI2h0
dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0vQ1BTMDsGBgQAj3oBAjAxMC8G
CCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b29yaXVtL0NQUzAS
BgNVHRMBAf8ECDAGAQH/AgEAMCcGA1UdJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcD
AgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEEcDBuMCAGCCsGAQUFBzABhhRodHRwOi8v
b2NzcC5zay5lZS9DQTBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0
cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwQQYDVR0e
BDowOKE2MASCAiIiMAqHCAAAAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvs
SQEBMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRv
cnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQAiw1VN
xp1Ho7FwcPlFqlLl6zb225IvpNelFX2QMbq1SPe41LuBW7WRZIV4b6bRQug55k8l
Am8eX3zEXL9I+4Bzai/IBlMSTYNpqAQGNVImQVwMa64uN8DWo8LNWSYNYYxQzO7s
TnqsqxLPWeKZRMkREI0RaVNoIPsciJvid9iBKTcGnMVkbrgyLzlXblLMU4I0pL2R
Wlfs2tr+XtCtWAvJPFskM2QZ2NnLjW8WroZr8TooocRA1vl/ruIAPC3FxW7zebKc
A2B66j4tW7uyF2kPx4WWA3xgR5QZnn4ePEAYjJdu1eWd9KbeAbxPCfFOST43t0fm
20HfV2Wp2PMEq4b2
-----END CERTIFICATE-----
ocsp:
url: http://demo.sk.ee/ocsp
responders:
- |
-----BEGIN CERTIFICATE-----
MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9
MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1
czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290
IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN
MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp
Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg
b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr
LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik
gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd
r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs
z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9
OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb
wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg
RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE
FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D
AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A
aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w
JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME
GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw
czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN
BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR
aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo
MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA
nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24
mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0
dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==
-----END CERTIFICATE-----
qualification:
- protocol: tspreg
conf:
Expand Down
9 changes: 9 additions & 0 deletions common/collector/src/ivxv.ee/conf/testdata/technical.dummy
Expand Up @@ -16,6 +16,8 @@ data:
debug: true
snidomain: inttest.ivxv.ee
filter:
tls:
handshaketimeout: 10
Expand Down Expand Up @@ -48,10 +50,17 @@ data:
verification:
- id: verification@localhost
address: localhost:4444
smartid:
- id: smartid@localhost
address: localhost:4445
votesorder:
- id: votesorder@localhost
address: localhost:4446
storage:
protocol: file
conf:
wd: testdata/storage
ordertimeout: 10
# vim: set ft=yaml sw=2:
2 changes: 0 additions & 2 deletions common/collector/src/ivxv.ee/container/bdoc/asice.go
Expand Up @@ -85,8 +85,6 @@ func openASiCE(r io.Reader, zipLimit, fileLimit int64, readSigs bool) (
var manifest *asiceFile
var signatures int
for i, file := range rzip.File {
// XXX: Upper limit on number of files allowed in ZIP? Or is
// the size limit enforced on the entire archive good enough?

// Check that we have not seen this file yet.
if _, ok := seen[file.Name]; ok {
Expand Down
2 changes: 1 addition & 1 deletion common/collector/src/ivxv.ee/cryptoutil/rdn.go
Expand Up @@ -233,7 +233,7 @@ func decodeATV(encoded string) (atv pkix.AttributeTypeAndValue, rest string, err
return
}

// XXX: A hexstring must be used if the attribute type was a
// A hexstring must be used if the attribute type was a
// numericoid, but we allow it to be a string anyway. This
// should be OK.

Expand Down
2 changes: 1 addition & 1 deletion common/collector/src/ivxv.ee/dds/soap.go
Expand Up @@ -92,7 +92,7 @@ func soapRequest(ctx context.Context, url string, req interface{}, resp interfac
}
log.Debug(ctx, HTTPResponse{Response: string(respDump)})

// XXX: Does encoding/xml.Unmarshal retain any references to the
// Does encoding/xml.Unmarshal retain any references to the
// original byte slice in the unmarshaled structure? If not, then
// instead of allocating a new byte slice here we could reuse pooled
// buffers for temporarily storing the XML between reading and
Expand Down
2 changes: 1 addition & 1 deletion common/collector/src/ivxv.ee/mid/http.go
Expand Up @@ -74,7 +74,7 @@ func httpDo(ctx context.Context, tag string, httpReq *http.Request, resp interfa
}
log.Debug(ctx, HTTPResponse{Response: string(respDump)})

// XXX: Does encoding/json.Unmarshal retain any references to the
// Does encoding/json.Unmarshal retain any references to the
// original byte slice in the unmarshaled structure? If not, then
// instead of allocating a new byte slice here we could reuse pooled
// buffers for temporarily storing the JSON between reading and
Expand Down
9 changes: 5 additions & 4 deletions common/collector/src/ivxv.ee/mid/mid_test.go
Expand Up @@ -17,8 +17,9 @@ import (
)

const (
testID = "60001019906"
testPhone = "+37200000766"
testID = "60001018800"
testSerial = "PNOEE-60001018800"
testPhone = "+37200000566"
)

var (
Expand Down Expand Up @@ -83,7 +84,7 @@ func TestAuthentication(t *testing.T) {
}
}

if cert.Subject.SerialNumber != testID {
if cert.Subject.SerialNumber != testSerial {
t.Error("unexpected subject serial number:", cert.Subject.SerialNumber)
}

Expand All @@ -104,7 +105,7 @@ func TestCertificate(t *testing.T) {
t.Fatal("failed to get signing certificate:", err)
}

if cert.Subject.SerialNumber != testID {
if cert.Subject.SerialNumber != testSerial {
t.Error("unexpected subject serial number:", cert.Subject.SerialNumber)
}
}
Expand Down
61 changes: 30 additions & 31 deletions common/collector/src/ivxv.ee/mid/testdata/signer.pem
@@ -1,35 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIGJDCCBAygAwIBAgIQBNsLtTIpnmNbbE4+laSLaTANBgkqhkiG9w0BAQsFADBr
MIIF8TCCA9mgAwIBAgIQUsz4AdR7FcpcrHiyXaldkjANBgkqhkiG9w0BAQsFADBr
MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1
czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2YgRVNU
RUlELVNLIDIwMTUwHhcNMTgwODA5MTQyMjU0WhcNMjIxMjExMjE1OTU5WjCB2DEL
MAkGA1UEBhMCRUUxGzAZBgNVBAoMEkVTVEVJRCAoTU9CSUlMLUlEKTEaMBgGA1UE
CwwRZGlnaXRhbCBzaWduYXR1cmUxPTA7BgNVBAMMNE/igJlDT05ORcW9LcWgVVNM
SUsgVEVTVE5VTUJFUixNQVJZIMOETk4sNjAwMDEwMTk5MDYxJzAlBgNVBAQMHk/i
gJlDT05ORcW9LcWgVVNMSUsgVEVTVE5VTUJFUjESMBAGA1UEKgwJTUFSWSDDhE5O
MRQwEgYDVQQFEws2MDAwMTAxOTkwNjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
BEG8MxwPLh5qmCfkkAPMw+8nKf4cqDETMoWiFiVOGu3cdI61ARLdRQUfa9wpzFDQ
GtmKuScHrLE25ZPZWEozK72jggIfMIICGzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQE
AwIGQDB1BgNVHSAEbjBsMF8GCisGAQQBzh8DAQMwUTAeBggrBgEFBQcCAjASDBBP
bmx5IGZvciBURVNUSU5HMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9y
ZXBvc2l0b29yaXVtL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBRYUZMh7LjBd2Op
IXrj0YnUK1hPJzCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQw
UQYGBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRp
dGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjATBgYEAI5GAQYwCQYH
BACORgEGATAfBgNVHSMEGDAWgBRJwPJEOWXVm0Y7DThgg7HWLSiGpjCBgwYIKwYB
BQUHAQEEdzB1MCwGCCsGAQUFBzABhiBodHRwOi8vYWlhLmRlbW8uc2suZWUvZXN0
ZWlkMjAxNTBFBggrBgEFBQcwAoY5aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMv
VEVTVF9vZl9FU1RFSUQtU0tfMjAxNS5kZXIuY3J0MDQGA1UdHwQtMCswKaAnoCWG
I2h0dHBzOi8vYy5zay5lZS90ZXN0X2VzdGVpZDIwMTUuY3JsMA0GCSqGSIb3DQEB
CwUAA4ICAQBlyeibA6wovyMM0ohySJZdH1H5TkCkudr9/b0yVjHJtyfVdGPRh4BV
mm0ZnLNjc5ose594vtovrSVO2q7Vk0XnW1xDVGHev/1MK4uDuCaDXFSmSGjJgUGW
dE73eQKH92XJcEgm5rAuZplddVhJt8/mOJ3sgLS+K+4gy1HZg5RmHsa/zuGitC+v
jysgKT+xzb5mMmGC5kf+640xJUeDUE2Lb4GjdPaqfZgNrYl2WJ3ptp8LL+GhKWYa
6T4EX7OIJq5pSNJVeI57vdlHZiB7EDuvZAHKUIAjrwqhMCXEHx+OI6oOWAsn32zv
2ZeVd7YwQ9l9V7TnEuTky1cQuF7+0F6cKlqyMZdq69JfXINcMEiWxy6ttS5BhKLN
tVEaD3dlo4E2dFlLkOJxRs6ZrzkhDeUtlrFdPrOFqTVw/K3d8fxe0gw34UZFGJrO
HogE7zW5AaNRRXZs9O2nWj3vwRLJstNfSNiSlhKqzktwV0WnM07FAZofHxRLCwMq
ba7f7P66zapN6Ly/FAWAMbRgaKpYRgTA+A4GN/mtdqQMJTFDW13GWlbCzV6wOSE9
z7/wzWx1M3h62mQaTW5nd5p6Q0DwHazIMTPpMt+ryRUuZsBDIzaGLD5NfLjYVcwb
qK9kEZ3QQBd4fxjWD4zeZV9Q4qP0DfFZEfUSSmeOZO5sw5eELuotVA==
RUlELVNLIDIwMTUwHhcNMTkwNDA5MTA0OTIyWhcNMzAxMjExMjE1OTU5WjCBpTEL
MAkGA1UEBhMCRUUxPTA7BgNVBAMMNE/igJlDT05ORcW9LcWgVVNMSUsgVEVTVE5V
TUJFUixNQVJZIMOETk4sNjAwMDEwMTg4MDAxJzAlBgNVBAQMHk/igJlDT05ORcW9
LcWgVVNMSUsgVEVTVE5VTUJFUjESMBAGA1UEKgwJTUFSWSDDhE5OMRowGAYDVQQF
ExFQTk9FRS02MDAwMTAxODgwMDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEG8
MxwPLh5qmCfkkAPMw+8nKf4cqDETMoWiFiVOGu3cdI61ARLdRQUfa9wpzFDQGtmK
uScHrLE25ZPZWEozK72jggIfMIICGzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIG
QDB1BgNVHSAEbjBsMF8GCisGAQQBzh8DAQMwUTAeBggrBgEFBQcCAjASDBBPbmx5
IGZvciBURVNUSU5HMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBv
c2l0b29yaXVtL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBRYUZMh7LjBd2OpIXrj
0YnUK1hPJzCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwUQYG
BACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlv
bnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjATBgYEAI5GAQYwCQYHBACO
RgEGATAfBgNVHSMEGDAWgBRJwPJEOWXVm0Y7DThgg7HWLSiGpjCBgwYIKwYBBQUH
AQEEdzB1MCwGCCsGAQUFBzABhiBodHRwOi8vYWlhLmRlbW8uc2suZWUvZXN0ZWlk
MjAxNTBFBggrBgEFBQcwAoY5aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVT
VF9vZl9FU1RFSUQtU0tfMjAxNS5kZXIuY3J0MDQGA1UdHwQtMCswKaAnoCWGI2h0
dHBzOi8vYy5zay5lZS90ZXN0X2VzdGVpZDIwMTUuY3JsMA0GCSqGSIb3DQEBCwUA
A4ICAQAWPebd0D8hssTj7Cdzp6zCFtHsZjmcgn21hLzsVDYqSde+/M7aLJ9WrCNl
SvjldScWRXBBhwH7SmePxVvmi061fmlb2Mg22XCOqWOp+Eyt9LtTtHlSi21v5VNh
nVX0RRPlCGseXHAyYjLtIGx744LCsZ/nblYtfAYrh2fJnYBOJddiUctKfb96M/sZ
NFlfAXbejBpoi0a+wXmL8fTY/PNAWT2UNhsUWA3XgQpsca0TBb6m4rVc9VfnjH0v
V9gaboH8jJL0M5bfPa4oE676Uw4YhtbRp2gXFdMKjb/5KpdAdfb5EhGSk8+rWZnW
BgfsRfHw8YnrLO3sOhPYWlXdIkJ4TPsxK/StJVIlIye5UBESxR1J4wZ0iI4wvwQU
TQ7xuIS89XgjlLm9/R7Qy86GN4lj6J0lD89dnFckduN/Hk5vMA+sGvPtIsV9q/fi
Wl6+SCYMmH6D+FpVpxYhq/VQoabwSWlhsgnjE+RddP6H3pWdICwJ6r7Iyx48SUc4
j8/lh6dxg6NR3TPQSyPQg1bJjaNO3L79pgxevX3LFOJ0eLhLmY2gvT5MvjkOjULc
XspKSjyNqyg8uJgT33SSBjArppWKMHpxybsaiY6X5jwdyO9qV32We94snptiqOrZ
aJ/LJ0RgOE7t7yBlDCdaUHohuUGB8+UD6efOBnP48L+FQz0D+w==
-----END CERTIFICATE-----
15 changes: 15 additions & 0 deletions common/collector/src/ivxv.ee/server/context.go
Expand Up @@ -38,6 +38,11 @@ type Header struct {
// verifiers to authenticate the client. It may be omitted, depending
// on the authentication method. Not included in the response.
AuthToken []byte `json:",omitempty" size:"16000"`

// DataToken is a data token used for keeping authentication
// data. It may be omitted, depending on the authentication
// method. Not included in the response.
DataToken []byte `json:",omitempty" size:"16000"`
}

// header is an unexported interface to check if a message contains a Header.
Expand All @@ -57,6 +62,7 @@ const (
authClientKey // Context key for authenticated client's distinguished name.
voteIDKey // Context key for vote identifier from authentication token.
voterIDKey // Context key for authenticated client's unique identifier.
voterIDNumber // Context key for authenticated client's unique number.

// Keys only used internally.
addrKey // Context key for connection's remote address.
Expand Down Expand Up @@ -102,3 +108,12 @@ func VoterIdentity(ctx context.Context) string {
}
return ""
}

// VoterNumber returns the unique number of the authenticated client or
// empty string if no authentication was done in this context.
func VoterNumber(ctx context.Context) string {
if val := ctx.Value(voterIDNumber); val != nil {
return val.(string)
}
return ""
}
8 changes: 8 additions & 0 deletions common/collector/src/ivxv.ee/server/errors.go
Expand Up @@ -23,6 +23,14 @@ var (
ErrMIDNotUser = errors.New("MID_NOT_USER")
ErrMIDOperator = errors.New("MID_OPERATOR")

// Smart ID errors.
ErrSmartIDCanceled = errors.New("SMARTID_CANCELED")
ErrSmartIDCertificate = errors.New("SMARTID_BAD_CERTIFICATE")
ErrSmartIDExpired = errors.New("SMARTID_EXPIRED")
ErrSmartIDGeneral = errors.New("SMARTID_GENERAL")
ErrSmartIDVerification = errors.New("SMARTID_VERIFICATION")
ErrSmartIDAccount = errors.New("SMARTID_ACCOUNT")

// Vote errors.
ErrIdentityMismatch = errors.New("IDENTITY_MISMATCH")
ErrOutdatedChoices = errors.New("OUTDATED_CHOICES")
Expand Down
33 changes: 30 additions & 3 deletions common/collector/src/ivxv.ee/server/filter.go
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"io"
"net"
"net/rpc"
"regexp"
"time"

"ivxv.ee/age"
Expand Down Expand Up @@ -86,13 +88,17 @@ type FilterConf struct {
}

// newFilters returns a new chain of mandatory filters.
func newFilters(conf *FilterConf, r *rpc.Server, cert tls.Certificate, end time.Time) (
func newFilters(conf *FilterConf, r *rpc.Server, cert tls.Certificate, end time.Time, certPool *x509.CertPool) (
connFilters, error) {

tlsFilter, err := newTLSFilter(&conf.TLS, cert)
if err != nil {
return nil, TLSConfError{Err: err}
}
if certPool != nil {
tlsFilter.tlsConf.ClientCAs = certPool
tlsFilter.tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
}
return connFilters{
connFilterFunc(logFilter),
connFilterFunc(connIDFilter),
Expand Down Expand Up @@ -185,7 +191,7 @@ func proxyFilter(ctx context.Context, c net.Conn, chain connFilters) context.Con
log.Log(ctx, PROXYProtocol{Address: addr})
}

// XXX: Put remote address into context for addrFilter. Remove once
// Put remote address into context for addrFilter. Remove once
// addrFilter is no longer necessary.
ctx = context.WithValue(ctx, addrKey, c.RemoteAddr())
if addr != nil {
Expand Down Expand Up @@ -355,6 +361,13 @@ func sessIDFilter(header *Header, chain headerFilters) error {
entry = ReadSessionID{}
}

// True if header.SessionID is not a valid HEX
invalidSessionID, _ := regexp.MatchString("[^0-9A-Fa-f]", header.SessionID)
if invalidSessionID {
log.Error(header.Ctx, InvalidSessionID{Value: header.SessionID})
return ErrBadRequest
}

// Set session ID in logging context and log.
header.Ctx = log.WithSessionID(header.Ctx, header.SessionID)
log.Log(header.Ctx, entry)
Expand All @@ -364,7 +377,7 @@ func sessIDFilter(header *Header, chain headerFilters) error {
// addrFilter re-logs the remote address of the connection after we have a
// SessionID.
//
// XXX: This is a temporary filter until the log monitor is capable of
// This is a temporary filter until the log monitor is capable of
// extracting the address based on ConnectionID.
func addrFilter(header *Header, chain headerFilters) error {
log.Log(header.Ctx, RemoteAddress{Address: header.Ctx.Value(addrKey)})
Expand Down Expand Up @@ -414,10 +427,24 @@ func (a authFilter) filter(header *Header, chain headerFilters) error {
header.Ctx = context.WithValue(header.Ctx, voterIDKey, voteid)
log.Log(header.Ctx, AuthenticationVoteID{VoteID: voteid})
}
if header.DataToken != nil {
log.Log(header.Ctx, AuthData{
Token: log.Sensitive(header.DataToken),
})
data, err := auth.Auther(a).Data(auth.Type(header.AuthMethod), header.DataToken)
if err != nil {
log.Error(header.Ctx, AuthenticationDataError{Err: err})
return ErrInternal
}
header.Ctx = context.WithValue(header.Ctx, voterIDNumber, string(data))
log.Log(header.Ctx, AuthenticationData{Number: string(data)})
}

}

header.AuthMethod = ""
header.AuthToken = nil
header.DataToken = nil
return chain.next(header)
}

Expand Down
2 changes: 0 additions & 2 deletions common/collector/src/ivxv.ee/server/proxy.go
Expand Up @@ -73,8 +73,6 @@ func readPROXY(c net.Conn) (wc net.Conn, addr net.Addr, health bool, err error)
// be reset by HAProxy. For actual connections we should be able to
// read at minimum the TLS ClientHello that HAProxy used to dispatch
// the connection.
// XXX: Can we determine if this was a health check without having to
// use prefixConn to put back the read byte?
one := []byte{0}
if err = readTimeout(c, one); err != nil {
if ne, ok := err.(*net.OpError); ok {
Expand Down
14 changes: 12 additions & 2 deletions common/collector/src/ivxv.ee/server/server.go
Expand Up @@ -6,6 +6,7 @@ package server // import "ivxv.ee/server"
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/rpc"
Expand All @@ -16,6 +17,7 @@ import (
"ivxv.ee/age"
"ivxv.ee/auth"
"ivxv.ee/conf/version"
"ivxv.ee/cryptoutil"
"ivxv.ee/identity"
"ivxv.ee/log"
)
Expand Down Expand Up @@ -45,6 +47,8 @@ type Conf struct {

Filter *FilterConf
Version *version.V // Necessary for reporting server status.

ClientCA string
}

// New creates a new server with the provided configuration and handler.
Expand All @@ -64,9 +68,15 @@ func New(c *Conf, handler interface{}) (*S, error) {
if err != nil {
return nil, TLSKeyPairError{Err: err}
}

var certPool *x509.CertPool
if c.ClientCA != "" {
certPool, err = cryptoutil.PEMCertificatePool(c.ClientCA)
if err != nil {
return nil, ClientCAParsingError{Err: err}
}
}
// Setup the chain of filters that serves a connection using r.
if s.filters, err = newFilters(c.Filter, r, tlsCert, c.End); err != nil {
if s.filters, err = newFilters(c.Filter, r, tlsCert, c.End, certPool); err != nil {
return nil, FilterConfError{Err: err}
}

Expand Down
135 changes: 135 additions & 0 deletions common/collector/src/ivxv.ee/smartid/authenticate.go
@@ -0,0 +1,135 @@
package smartid

import (
"context"
"crypto/rand"
"crypto/x509"
)

// Authenticate starts a Smart-ID authentication session.
func (c *Client) Authenticate(ctx context.Context, identifer string) (
sesscode string, challengeRnd []byte, challenge []byte, err error) {

// Generate random authentication challenge to sign. Although we could
// use the challenge directly, we pass it through the hash function to
// simplify VerifyAuthenticationSignature which requires the pre-image
// of the signed data.
challengeRnd = make([]byte, c.authHashFunction.Size())
if _, err = rand.Read(challengeRnd); err != nil {
err = GenerateAuthenticationChallengeError{Err: err}
return
}
d := c.authHashFunction.New()
d.Write(challengeRnd)
challenge = d.Sum(nil)

hashType := hashFunctionNames[c.authHashFunction]

sesscode, err = c.startSession(ctx, sessAuth, convertToETSI(identifer), challenge, hashType)
if err != nil {
err = AuthenticateError{Err: err}
return
}

return
}

// GetAuthenticateStatus queries the status of a Smart-ID authentication
// session. If err is nil and signature is empty, then the transaction is still
// outstanding. If err is nil and signature is non-nil, then the user is
// authenticated, although callers should use VerifyAuthenticationSignature to
// double-check.
func (c *Client) GetAuthenticateStatus(ctx context.Context, sesscode string) (
cert *x509.Certificate, algorithm string, signature []byte, err error) {

var certDER []byte
_, algorithm, signature, certDER, err = c.getSessionStatus(ctx, sesscode)
if err != nil {
err = GetAuthenticateStatusError{Err: err}
return
}

if certDER != nil {
cert, err = c.parseAndVerify(ctx, certDER)
}

return
}

// parseAndVerify is a helper function to parse and verify the authentication
// certificate.
func (c *Client) parseAndVerify(ctx context.Context, certDER []byte) (
cert *x509.Certificate, err error) {
// Parse the authentication certificate.
if cert, err = x509.ParseCertificate(certDER); err != nil {
err = ParseAuthenticationCertificateError{
Certificate: certDER,
Err: err,
}
return
}

// Verify the authentication certificate and get the issuer.
opts := x509.VerifyOptions{
Roots: c.rpool,
Intermediates: c.ipool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
chains, err := cert.Verify(opts)
if err != nil {
var certerr CertificateError
certerr.Err = AuthenticationCertificateVerificationError{
Certificate: cert,
Err: err,
}
err = certerr
return
}
issuer := cert
if len(chains[0]) > 1 { // At least one chain is guaranteed.
issuer = chains[0][1]
}

// Check OCSP status.
status, err := c.ocsp.Check(ctx, cert, issuer, nil)
if err != nil {
err = CheckAuthenticationCertOCSPResponsError{
Response: status,
Err: err,
}
return
}
if !status.Good {
var certerr CertificateError
certerr.Err = AuthenticationCertificateRevokedError{
Reason: status.RevocationReason,
}
err = certerr
return
}

return
}

// VerifyAuthenticationSignature verifies the certificate signature on the
// authentication challenge.
func VerifyAuthenticationSignature(cert *x509.Certificate, algorithm string,
signed, signature []byte) (err error) {

sigalg, ok := signatureAlgs[algorithm]
if !ok {
return SigAlgorithmNotSupportedError{
Algorithm: algorithm,
}
}

if err = cert.CheckSignature(sigalg, signed, signature); err != nil {
return VerifyAuthenticationSignatureError{Err: err}
}
return nil
}

// convertToETSI is a helper function to make identifier to ETSI identifier.
func convertToETSI(identifier string) string {
return "PNOEE-" + identifier
}
60 changes: 60 additions & 0 deletions common/collector/src/ivxv.ee/smartid/certificate.go
@@ -0,0 +1,60 @@
package smartid

import (
"context"
"crypto/x509"
)

// https://github.com/SK-EID/smart-id-documentation#2384-request-parameters
type getCertificate struct {
RelyingPartyUUID string `json:"relyingPartyUUID"`
RelyingPartyName string `json:"relyingPartyName"`
CertificateLevel string `json:"certificateLevel,omitempty"`
Nonce string `json:"nonce,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
}

// GetCertificateChoice starts a Smart-ID certificate choice session.
func (c *Client) GetCertificateChoice(ctx context.Context, identifier string) (sess string, err error) {

// We cannot use a struct literal, because gen would report it
// as a duplicate error type.
var input InputError

if len(identifier) == 0 {
input.Err = GetCertificateNoIDCodeError{}
err = input
}
if err != nil {
return
}

var resp startSessionResponse
if err = httpPost(ctx, c.url+"certificatechoice/etsi/"+convertToETSI(identifier), getCertificate{
RelyingPartyUUID: c.conf.RelyingPartyUUID,
RelyingPartyName: c.conf.RelyingPartyName,
CertificateLevel: c.conf.CertificateLevel,
}, &resp); err != nil {
return "", GetMobileCertificateError{Err: err}
}

return resp.SessionID, nil
}

// GetCertificateChoiceStatus queries for a Smart-ID certificate choice session.
func (c *Client) GetCertificateChoiceStatus(ctx context.Context, sesscode string) (
documentno string, cert *x509.Certificate, err error) {

var certDER []byte
documentno, _, _, certDER, err = c.getSessionStatus(ctx, sesscode)
if err != nil {
err = GetMobileCertificateStatusError{Err: err}
return
}

if certDER != nil {
cert, err = c.parseAndVerify(ctx, certDER)
}

return
}
107 changes: 107 additions & 0 deletions common/collector/src/ivxv.ee/smartid/http.go
@@ -0,0 +1,107 @@
package smartid

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"

"ivxv.ee/log"
"ivxv.ee/safereader"
)

const maxResponseSize = 10240 // 10 KiB.

// https://github.com/SK-EID/smart-id-documentation#2383-error-conditions
type errorResponse struct {
SessionID string `json:"sessionID"`
}

func httpGet(ctx context.Context, url string, resp interface{}) error {
httpReq, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return CreateHTTPGetRequestError{URL: url, Err: err}
}
httpReq = httpReq.WithContext(ctx)

return httpDo(ctx, "", httpReq, resp)
}

func httpPost(ctx context.Context, url string, req interface{}, resp interface{}) error {
jsonReq, err := json.Marshal(req)
if err != nil {
return MarshalJSONRequestError{Err: err}
}

httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonReq))
if err != nil {
return CreateHTTPPostRequestError{URL: url, Err: err}
}
httpReq = httpReq.WithContext(ctx)

httpReq.Header.Set("Content-Type", "application/json")

return httpDo(ctx, fmt.Sprintf("%T", req), httpReq, resp)
}

func httpDo(ctx context.Context, tag string, httpReq *http.Request, resp interface{}) error {
reqDump, err := httputil.DumpRequestOut(httpReq, true)
if err != nil {
return DumpHTTPRequestError{Err: err}
}
log.Debug(ctx, HTTPRequest{Request: string(reqDump)})

log.Log(ctx, SendingRequest{URL: httpReq.URL, Method: httpReq.Method, BodyType: tag})
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return log.Alert(SendRequestError{Err: err})
}
defer func() {
if cerr := httpResp.Body.Close(); cerr != nil && err == nil {
err = ResponseBodyCloseError{Err: cerr}
}
}()
log.Log(ctx, ReceivedResponse{})

respDump, err := httputil.DumpResponse(httpResp, false)
if err != nil {
return DumpHTTPResponseError{Err: err}
}
log.Debug(ctx, HTTPResponse{Response: string(respDump)})

// Does encoding/json.Unmarshal retain any references to the
// original byte slice in the unmarshaled structure? If not, then
// instead of allocating a new byte slice here we could reuse pooled
// buffers for temporarily storing the JSON between reading and
// decoding.
body, err := ioutil.ReadAll(safereader.New(httpResp.Body, maxResponseSize))
if err != nil {
return ReadHTTPResponseBodyError{Err: err}
}
log.Debug(ctx, HTTPResponseBody{Body: string(body)})

if httpResp.StatusCode != http.StatusOK {
if len(body) > 0 {
var jsonErr errorResponse
if err = json.Unmarshal(body, &jsonErr); err != nil {
return UnmarshalJSONErrorResponseError{Err: err}
}

err = ErrorResponseError{
HTTPStatus: httpResp.Status,
SessionID: jsonErr.SessionID,
}
return err

}
return HTTPStatusError{Status: httpResp.Status}
}

if err = json.Unmarshal(body, &resp); err != nil {
return UnmarshalJSONResponseError{Err: err}
}
return nil
}
192 changes: 192 additions & 0 deletions common/collector/src/ivxv.ee/smartid/session.go
@@ -0,0 +1,192 @@
package smartid

import (
"context"
"crypto"
"crypto/x509"
"fmt"
)

type sessType string

const (
sessAuth sessType = "authentication/etsi"
sessSign sessType = "signature/document"
QSCD string = "QSCD"
QUALIFIED string = "QUALIFIED"
)

var (
// hashFunctionNames is map from hash algorithm to it's name for Smart-ID
// REST API.
hashFunctionNames = map[crypto.Hash]string{
crypto.SHA256: "SHA256",
crypto.SHA384: "SHA384",
crypto.SHA512: "SHA512",
}

// signatureAlgs is map from signature algorithm name in Smart-ID REST API
// to algorithm type.
signatureAlgs = map[string]x509.SignatureAlgorithm{
"sha256WithRSAEncryption": x509.SHA256WithRSA,
"sha384WithRSAEncryption": x509.SHA384WithRSA,
"sha512WithRSAEncryption": x509.SHA512WithRSA,
}
)

// https://github.com/SK-EID/smart-id-documentation#2394-request-parameters
type startSessionRequest struct {
RelyingPartyUUID string `json:"relyingPartyUUID"`
RelyingPartyName string `json:"relyingPartyName"`
CertificateLevel string `json:"certificateLevel,omitempty"`
Hash []byte `json:"hash"`
HashType string `json:"hashType"`
AllowedInteractionsOrder []allowedInteractionsOrder `json:"allowedInteractionsOrder"`
Nonce string `json:"nonce,omitempty"`
RequestProperties []byte `json:"requestProperties,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
}

type allowedInteractionsOrder struct {
Type string `json:"type"`
DisplayText60 string `json:"displayText60,omitempty"`
DisplayText200 string `json:"displayText200,omitempty"`
}

// https://github.com/SK-EID/smart-id-documentation#2395-example-response
type startSessionResponse struct {
SessionID string `json:"sessionID"`
}

// startSession is helper function to start either authentication or signing
// dialog with the user.
func (c *Client) startSession(ctx context.Context, t sessType, identifier string,
hash []byte, hashType string) (sesscode string, err error) {

// We cannot use a struct literal, because gen would report it
// as a duplicate error type.
var input InputError
switch {

case len(hash) == 0:
input.Err = StartSessionNoHashError{}
err = input
case len(hashType) == 0:
input.Err = StartSessionNoHashTypeError{}
err = input
}
if err != nil {
return
}

interactionsOrder := c.conf.SignInteractionsOrder
certLevel := c.conf.CertificateLevel
if t == sessAuth {
interactionsOrder = c.conf.AuthInteractionsOrder
if certLevel == QSCD {
certLevel = QUALIFIED
}
}

var resp startSessionResponse
if err = httpPost(ctx, c.url+string(t)+"/"+identifier, startSessionRequest{
RelyingPartyUUID: c.conf.RelyingPartyUUID,
RelyingPartyName: c.conf.RelyingPartyName,
Hash: hash,
HashType: hashType,
CertificateLevel: certLevel,
AllowedInteractionsOrder: interactionsOrder,
}, &resp); err != nil {
err = StartSessionError{Err: err}
return
}
sesscode = resp.SessionID

return
}

// https://github.com/SK-EID/smart-id-documentation#23114-response-structure
type sessionStatusResponse struct {
State string `json:"state"`
Result resultResponse `json:"result"`
Signature signatureResponse `json:"signature"`
Cert certResponse `json:"cert"`
IgnoredProperties []string `json:"ignoredProperties"`
InteractionFlowUsed string `json:"interactionFlowUsed"`
DeviceIPAddress string `json:"deviceIpAddress"`
}

type resultResponse struct {
EndResult string `json:"endResult"`
DocumentNumber string `json:"documentNumber"`
}

type certResponse struct {
Value []byte `json:"value"`
CertificateLevel string `json:"certificateLevel"`
}

type signatureResponse struct {
Value []byte `json:"value"`
Algorithm string `json:"algorithm"`
}

// getSessionStatus is helper function to get the status of either
// authentication or signing dialog with the user.
func (c *Client) getSessionStatus(ctx context.Context, sesscode string) (
documentNo string, algorithm string, signature []byte, certDER []byte, err error) {

var resp sessionStatusResponse
url := fmt.Sprintf("%ssession/%s?timeoutMs=%d", c.url, sesscode, c.conf.StatusTimeoutMS)
if err = httpGet(ctx, url, &resp); err != nil {
return "", "", nil, nil, GetSessionStatusError{Err: err}
}

switch resp.State {
case "RUNNING":
return
case "COMPLETE":
default:
var status StatusError
status.Err = UnexpectedSessionStateError{State: resp.State}
return "", "", nil, nil, status
}
switch resp.Result.EndResult {
case "OK":
return resp.Result.DocumentNumber, resp.Signature.Algorithm, resp.Signature.Value, resp.Cert.Value, nil
case "TIMEOUT":
var expired ExpiredError
return "", "", nil, nil, expired
case "DOCUMENT_UNUSABLE":
var account AccountError
return "", "", nil, nil, account
case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP":
var account AccountError
return "", "", nil, nil, account
case "WRONG_VC":
var verification VerificationError
return "", "", nil, nil, verification
case "USER_REFUSED":
var canceled CanceledError
return "", "", nil, nil, canceled
case "USER_REFUSED_CERT_CHOICE":
var canceled CanceledError
return "", "", nil, nil, canceled
case "USER_REFUSED_DISPLAYTEXTANDPIN":
var canceled CanceledError
return "", "", nil, nil, canceled
case "USER_REFUSED_VC_CHOICE":
var canceled CanceledError
return "", "", nil, nil, canceled
case "USER_REFUSED_CONFIRMATIONMESSAGE":
var canceled CanceledError
return "", "", nil, nil, canceled
case "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE":
var canceled CanceledError
return "", "", nil, nil, canceled
default:
var status StatusError
status.Err = UnexpectedSessionResultError{Result: resp.Result}
return "", "", nil, nil, status
}
}
32 changes: 32 additions & 0 deletions common/collector/src/ivxv.ee/smartid/sign.go
@@ -0,0 +1,32 @@
package smartid

import (
"context"
)

// SignHash starts a Smart-ID signing session to sigh hash.
func (c *Client) SignHash(ctx context.Context, documentno string, hash []byte,
hashType string) (sesscode string, err error) {

sesscode, err = c.startSession(ctx, sessSign, documentno, hash, hashType)
if err != nil {
err = SignHashError{Err: err}
return
}

return
}

// GetSignHashStatus queries the status of a Smart-ID signing session.
// If err is nil and signature is empty, then the transaction is still
// outstanding.
func (c *Client) GetSignHashStatus(ctx context.Context, sesscode string) (
algorithm string, signature []byte, err error) {

_, algorithm, signature, _, err = c.getSessionStatus(ctx, sesscode)
if err != nil {
err = GetSignHashStatusError{Err: err}
return
}
return
}
126 changes: 126 additions & 0 deletions common/collector/src/ivxv.ee/smartid/smartid.go
@@ -0,0 +1,126 @@
/*
Package smartid provides client for the Smart-ID REST service.

https://github.com/SK-EID/smart-id-documentation
*/
package smartid

import (
"crypto"
"crypto/x509"
"strings"

"ivxv.ee/cryptoutil"
"ivxv.ee/ocsp"
)

var (
// InputError wraps errors which are caused by bad input to smartid functions.
_ = InputError{Err: nil}

// VerificationError is the error returned if the voter 3 different code
// was displayed in app and selected wrong code.
_ = VerificationError{}

// AccountError is the error returned that are caused by user account configuration.
_ = AccountError{}

// CanceledError is the error returned if the voter canceled the
// operation.
_ = CanceledError{}

// ExpiredError is the error returned if the session expired
// before the voter did any action.
_ = ExpiredError{}

// CertificateError wraps errors which are caused by errors with the
// voter's certificate (revoked, suspended, not activated, etc).
_ = CertificateError{Err: nil}

// StatusError wraps errors which are caused by an unexpected session
// status: this is a catch-all for other types of Smart-ID problems.
_ = StatusError{Err: nil}

// allowedAuthHashFunctions is list of hash functions allowed for
// authentication. Since the hash function is determined by it's hash
// size (Conf.AuthChallengeSize), the functions should have hash sizes
// unique in the list. If Conf.AuthChallengeSize is zero, then the
// first one from this list is used.
allowedAuthHashFunctions = []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512}
)

// Conf contains the configurable options for the Smart-ID REST API client. It
// only contains serialized values such that it can easily be unmarshaled from
// a file.
type Conf struct {
URL string // URL of Smart-ID REST API.
RelyingPartyUUID string // The UUID of the relying party, i.e service consumer.
RelyingPartyName string // The name of the relying party, i.e service consumer.

CertificateLevel string // Certificate level for requests
AuthInteractionsOrder []allowedInteractionsOrder // Interactions to display during authentication.
SignInteractionsOrder []allowedInteractionsOrder // Interactions to display during signing.

AuthChallengeSize int64 // The authentication challenge size.
StatusTimeoutMS int64 // The long-polling timeout for authentication/signing status request.

Roots []string // PEM-encoded authentication certificate verification roots.
Intermediates []string // PEM-encoded authentication certificate verification intermediates.
OCSP ocsp.Conf // OCSP configuration for checking authentication certificate revocation.
}

// Client implements Smart-ID REST API authentication and signing.
type Client struct {
conf Conf
rpool *x509.CertPool
ipool *x509.CertPool
ocsp *ocsp.Client
// authHashFunction is the hash function to use for authentication.
// Determined by Conf.AuthChallengeSize.
authHashFunction crypto.Hash
// url is the same as 'conf.URL', but guaranteed to end with slash.
url string
}

// New returns a new Smart-ID REST API client with the provided configuration.
func New(conf *Conf) (c *Client, err error) {
if len(conf.Roots) == 0 {
return nil, UnconfiguredRootsError{}
}

c = &Client{conf: *conf} // Save a copy of conf so it cannot be changed.
if c.rpool, err = cryptoutil.PEMCertificatePool(c.conf.Roots...); err != nil {
return nil, RootsParsingError{Err: err}
}
if c.ipool, err = cryptoutil.PEMCertificatePool(c.conf.Intermediates...); err != nil {
return nil, IntermediatesParsingError{Err: err}
}
if c.ocsp, err = ocsp.New(&c.conf.OCSP); err != nil {
return nil, OCSPClientError{Err: err}
}
if c.authHashFunction, err = findAuthHashFunction(c.conf.AuthChallengeSize); err != nil {
return nil, err
}
c.url = conf.URL
if !strings.HasSuffix(c.url, "/") {
c.url += "/"
}
return
}

// findAuthHashFunction is helper function to find allowed authentication hash
// algorithm by it's hash size. If the size is not configured, the first value
// in the allowedAuthHashFunctions is used.
func findAuthHashFunction(size int64) (crypto.Hash, error) {
if size == 0 {
return allowedAuthHashFunctions[0], nil
}
var sizes []int
for _, hf := range allowedAuthHashFunctions {
if hf.Size() == int(size) {
return hf, nil
}
sizes = append(sizes, hf.Size())
}
return 0, AuthChallengeSizeError{Size: size, AllowedSizes: sizes}
}